| // Copyright 2024 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "hoth.hpp" |
| |
| #include "google3/ec_commands.h" |
| #include "google3/host_commands.h" |
| |
| #include "ec_util_interface.hpp" |
| #include "firmware_mtd_updater.hpp" |
| #include "message_util.hpp" |
| #include "payload_update_interface.hpp" |
| |
| #include <stdplus/print.hpp> |
| #include <stdplus/raw.hpp> |
| #include <xyz/openbmc_project/Control/Hoth/error.hpp> |
| |
| #include <array> |
| #include <chrono> |
| #include <cstddef> |
| #include <cstdint> |
| #include <exception> |
| #include <future> |
| #include <span> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| namespace google |
| { |
| namespace hoth |
| { |
| namespace |
| { |
| |
| // Command to be filtered |
| // Value should be consistent to security/crypta/firmware/app/commands/ |
| |
| // Ideally the crypta command should be moved to some common location so |
| // we don't need to hardcode them here |
| // major commands |
| constexpr uint8_t kCryptaProdidMajorCommand = 0x05; |
| constexpr uint8_t kCryptaCimMajorCommand = 0x01; |
| |
| // minor commands |
| constexpr uint8_t kCryptaProdidBeginUnwrapType1BlobMinorCommand = 0x00; |
| constexpr uint8_t kCryptaProdidLoadTokenMinorCommand = 0x05; |
| |
| constexpr uint8_t kCryptaCimLoadCbkMinorCommand = 0x0A; |
| constexpr uint8_t kCryptaCimLoadCbkDataMinorCommand = 0x0B; |
| constexpr uint8_t kCryptaCimLockCbkSlotMinorCommand = 0x15; |
| constexpr uint8_t kCryptaCimIncrementCbkCounterCommand = 0x17; |
| |
| constexpr uint8_t kCryptaHostCommandVersion = 0x00; |
| } // namespace |
| |
| using sdbusplus::error::xyz::openbmc_project::control::hoth::CommandFailure; |
| using sdbusplus::error::xyz::openbmc_project::control::hoth::ExpectedInfoNotFound; |
| using sdbusplus::error::xyz::openbmc_project::control::hoth::FirmwareFailure; |
| using sdbusplus::error::xyz::openbmc_project::control::hoth::InterfaceError; |
| using sdbusplus::error::xyz::openbmc_project::control::hoth::ResponseFailure; |
| using internal::CryptaHeader; |
| using internal::CBKSlotCmd; |
| using internal::EC_RES_ACCESS_DENIED; |
| using internal::FirmwareUpdater; |
| using internal::LoadTokenCmd; |
| using internal::PayloadUpdate; |
| using internal::ReqHeader; |
| using internal::SUPPORTED_STRUCT_VERSION; |
| |
| Hoth::FirmwareUpdateStatus Hoth::getAsyncStatus(std::future<void>* asyncFuture) |
| { |
| std::future_status asyncStatus = |
| asyncFuture->wait_for(std::chrono::seconds(0)); |
| if (asyncStatus == std::future_status::timeout) |
| { |
| return Hoth::FirmwareUpdateStatus::InProgress; |
| } |
| // std::future_status can only be "timeout", "ready" and "deferred". |
| // Our async threads should never be "deferred" so they must be "ready" |
| // if they managed to reach here. |
| try |
| { |
| asyncFuture->get(); |
| // If we did not throw an exception while doing a get, async thread must |
| // have successfully completed and exited |
| return Hoth::FirmwareUpdateStatus::Done; |
| } |
| catch (const std::exception& e) |
| { |
| stdplus::print(stderr, "The polled async thread had an exception: {}\n", |
| e.what()); |
| return Hoth::FirmwareUpdateStatus::Error; |
| } |
| } |
| |
| void Hoth::checkForOngoingPayload() |
| { |
| // If any of the payload future objects are valid, it means there is an |
| // async payload process in the background. |
| if (futInitiatePayload.valid()) |
| { |
| stdplus::print(stderr, |
| "There is an initiate payload command in progress\n"); |
| } |
| else if (futSendPayload.valid()) |
| { |
| stdplus::print(stderr, "There is a send payload command in progress\n"); |
| } |
| else if (futVerifyPayload.valid()) |
| { |
| stdplus::print(stderr, |
| "There is a verify payload command in progress\n"); |
| } |
| else |
| { |
| // No ongoing payload async thread detected |
| return; |
| } |
| |
| throw InterfaceError(); |
| } |
| |
| static bool isCryptaCommand(std::span<const uint8_t> command) |
| { |
| try |
| { |
| auto& ecHeader = stdplus::raw::refFrom<ReqHeader>(command); |
| // Struct version doesn't match |
| if (ecHeader.struct_version != SUPPORTED_STRUCT_VERSION) |
| { |
| return false; |
| } |
| // Not a Crypta Command |
| if (ecHeader.command != |
| EC_CMD_BOARD_SPECIFIC_BASE + EC_PRV_CMD_HOTH_CRYPTA) |
| { |
| return false; |
| } |
| // Command version doesn't match |
| if (ecHeader.command_version != kCryptaHostCommandVersion) |
| { |
| return false; |
| } |
| } |
| catch (...) |
| { |
| // Command is too short, cannot be a crypta command |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static bool isSensitiveSlotSpecificCommand(const CryptaHeader& hdr) |
| { |
| if (hdr.major == kCryptaProdidMajorCommand && |
| hdr.minor == kCryptaProdidBeginUnwrapType1BlobMinorCommand) |
| { |
| return true; |
| } |
| |
| if (hdr.major == kCryptaCimMajorCommand && |
| (hdr.minor == kCryptaCimLoadCbkMinorCommand || |
| hdr.minor == kCryptaCimLoadCbkDataMinorCommand || |
| hdr.minor == kCryptaCimLockCbkSlotMinorCommand || |
| hdr.minor == kCryptaCimIncrementCbkCounterCommand)) |
| { |
| return true; |
| } |
| return false; |
| } |
| |
| static bool isLoadTokensCommand(std::span<const uint8_t> command) |
| { |
| try |
| { |
| auto& loadTokenCmd = stdplus::raw::refFrom<LoadTokenCmd>(command); |
| return (loadTokenCmd.cryptaHdr.major == kCryptaProdidMajorCommand) && |
| (loadTokenCmd.cryptaHdr.minor == |
| kCryptaProdidLoadTokenMinorCommand); |
| } |
| catch (...) |
| { |
| // command too short to be a load token command |
| } |
| return false; |
| } |
| |
| static bool isCBKSlot2Command(std::span<const uint8_t> command) |
| { |
| try |
| { |
| auto& cbkSlotCmd = stdplus::raw::refFrom<CBKSlotCmd>(command); |
| if (isSensitiveSlotSpecificCommand(cbkSlotCmd.cryptaHdr)) |
| { |
| // all commands here has slot id as the 1st param |
| // param count should be at least 1 for this msg, but here we won't |
| // guard the case, crypta will drop the message anyway |
| if (cbkSlotCmd.cryptaHdr.count == 0) |
| { |
| return false; |
| } |
| |
| // slotid size, ideally should be 4, again we don't guard it here |
| if (cbkSlotCmd.cryptaParam.size != 4) |
| { |
| return false; |
| } |
| |
| // check if slotid is 2 |
| if (cbkSlotCmd.slotId == 2) |
| { |
| return true; |
| } |
| } |
| } |
| catch (...) |
| { |
| // command too short to be a cbk slot 2 command |
| } |
| |
| return false; |
| } |
| |
| static bool isHostForbiddenCommand(std::span<const uint8_t> command) |
| { |
| if (!isCryptaCommand(command)) |
| { |
| return false; |
| } |
| return isLoadTokensCommand(command) || isCBKSlot2Command(command); |
| } |
| |
| std::vector<uint8_t> Hoth::sendHostCommand(const std::vector<uint8_t> command) |
| { |
| if (isHostForbiddenCommand(command)) |
| { |
| stdplus::print(stderr, |
| "The Command is forbidden in SendHostCommand!\n"); |
| return generateErrorResponse(EC_RES_ACCESS_DENIED); |
| } |
| return hostCmd->sendCommand(command); |
| } |
| |
| std::vector<uint8_t> |
| Hoth::sendTrustedHostCommand(const std::vector<uint8_t> command) |
| { |
| return hostCmd->sendCommand(command); |
| } |
| |
| uint64_t Hoth::sendHostCommandAsync(const std::vector<uint8_t> command) |
| { |
| return hostCmd->sendCommandAsync(command); |
| } |
| |
| std::vector<uint8_t> Hoth::getHostCommandResponse(uint64_t callToken) |
| { |
| return hostCmd->getResponse(callToken); |
| } |
| |
| void Hoth::checkForOngoingSpiWrite() |
| { |
| if (futSpiWrite.valid()) |
| { |
| std::future_status status = |
| futSpiWrite.wait_for(std::chrono::seconds(0)); |
| if (status == std::future_status::timeout) |
| { |
| stdplus::print(stderr, |
| "There is already an SPI write in progress\n"); |
| throw FirmwareFailure(); |
| } |
| } |
| } |
| |
| void Hoth::updateFirmware(std::vector<uint8_t> firmwareData) |
| { |
| checkForOngoingSpiWrite(); |
| futSpiWrite = std::async(std::launch::async, &FirmwareUpdater::update, |
| firmwareUpdater, std::move(firmwareData)); |
| } |
| |
| void Hoth::spiWrite(uint32_t address, std::vector<uint8_t> data) |
| { |
| checkForOngoingSpiWrite(); |
| futSpiWrite = std::async(std::launch::async, &FirmwareUpdater::spiWrite, |
| firmwareUpdater, address, std::move(data)); |
| } |
| |
| Hoth::FirmwareUpdateStatus Hoth::getFirmwareUpdateStatus() |
| { |
| if (!futSpiWrite.valid()) |
| { |
| // If there is no instance of futFirmwareUpdate, it means an update has |
| // already finished or that the update has not been started. |
| // Return the last valid status |
| return lastFirmwareUpdateStatus; |
| } |
| |
| lastFirmwareUpdateStatus = getAsyncStatus(&futSpiWrite); |
| return lastFirmwareUpdateStatus; |
| } |
| |
| void Hoth::initiatePayload() |
| { |
| checkForOngoingPayload(); |
| |
| futInitiatePayload = |
| std::async(std::launch::async, &PayloadUpdate::initiate, payloadUpdate); |
| } |
| |
| Hoth::FirmwareUpdateStatus Hoth::getInitiatePayloadStatus() |
| { |
| if (!futInitiatePayload.valid()) |
| { |
| // If there is no instance of futInitiatePayload, it means |
| // initialization has already finished or that the initialization has |
| // not been started. Return the last valid status |
| return lastInitiatePayloadStatus; |
| } |
| |
| lastInitiatePayloadStatus = getAsyncStatus(&futInitiatePayload); |
| return lastInitiatePayloadStatus; |
| } |
| |
| void Hoth::sendPayload(std::string imagePath) |
| { |
| checkForOngoingPayload(); |
| |
| if (imagePath.empty()) |
| { |
| stdplus::print(stderr, "No image path was specified\n"); |
| throw FirmwareFailure(); |
| } |
| |
| futSendPayload = std::async(std::launch::async, &PayloadUpdate::send, |
| payloadUpdate, std::move(imagePath)); |
| } |
| |
| Hoth::FirmwareUpdateStatus Hoth::getSendPayloadStatus() |
| { |
| if (!futSendPayload.valid()) |
| { |
| // If there is no instance of futSendPayload, it means send payload has |
| // already finished or that the transfer has not been started. |
| // Return the last valid status |
| return lastSendPayloadStatus; |
| } |
| |
| lastSendPayloadStatus = getAsyncStatus(&futSendPayload); |
| return lastSendPayloadStatus; |
| } |
| |
| void Hoth::erasePayload(uint32_t offset, uint32_t size) |
| { |
| static constexpr uint32_t sectorSize = 4096; |
| |
| checkForOngoingPayload(); |
| |
| // Offset and size must be aligned to the sector size |
| if (offset % sectorSize) |
| { |
| stdplus::print(stderr, |
| "Erase offset must be divisible by the sector size\n"); |
| throw CommandFailure(); |
| } |
| |
| if (size % sectorSize) |
| { |
| stdplus::print(stderr, |
| "Erase size must be divisible by the sector size\n"); |
| throw CommandFailure(); |
| } |
| |
| payloadUpdate->erase(offset, size); |
| } |
| |
| void Hoth::eraseAndSendStaticWPPayload(std::string imagePath) |
| { |
| checkForOngoingPayload(); |
| |
| if (imagePath.empty()) |
| { |
| stdplus::print(stderr, "No image path was specified\n"); |
| throw FirmwareFailure(); |
| } |
| |
| futSendPayload = |
| std::async(std::launch::async, &PayloadUpdate::eraseAndSendStaticWP, |
| payloadUpdate, std::move(imagePath)); |
| } |
| |
| void Hoth::verifyPayload() |
| { |
| checkForOngoingPayload(); |
| |
| futVerifyPayload = |
| std::async(std::launch::async, &PayloadUpdate::verify, payloadUpdate); |
| } |
| |
| Hoth::FirmwareUpdateStatus Hoth::getVerifyPayloadStatus() |
| { |
| if (!futVerifyPayload.valid()) |
| { |
| // If there is no instance of futVerifyPayload, it means verify payload |
| // has already finished or that the transfer has not been started. |
| // Return the last valid status |
| return lastVerifyPayloadStatus; |
| } |
| |
| lastVerifyPayloadStatus = getAsyncStatus(&futVerifyPayload); |
| return lastVerifyPayloadStatus; |
| } |
| |
| void Hoth::activatePayload(bool makePersistent) |
| { |
| checkForOngoingPayload(); |
| |
| // Activate the non-active (staging) partition with persistence level |
| // defined by makePersistent |
| payload_update_status response = payloadUpdate->getStatus(); |
| payloadUpdate->activate(internal::Side(!response.active_half), |
| internal::Persistence(makePersistent)); |
| } |
| |
| void Hoth::deactivatePayload() |
| { |
| checkForOngoingPayload(); |
| |
| // Deactivate the non-active (staging) partition with persistence level |
| // defined by makePersistent by making current active_half as active. |
| payload_update_status response = payloadUpdate->getStatus(); |
| payloadUpdate->activate(internal::Side(response.active_half), |
| internal::Persistence(true)); |
| } |
| |
| uint32_t Hoth::getPayloadSize() |
| { |
| uint32_t right = 1; |
| std::array<uint8_t, 1> buf; |
| try |
| { |
| while (true) |
| { |
| payloadUpdate->read(right, buf); |
| right <<= 1; |
| } |
| } |
| catch (const ResponseFailure&) |
| {} |
| |
| // Binary search for the size boundary by checking to see if reading |
| // a single byte works for an offset. The size should be one offset greater |
| // than the largest succeeding offset. We maintain the invariant inside the |
| // loop that the left is always a valid readable offset and the right will |
| // always fail to read. The loop terminates when the left becomes invalid |
| // and represents the size. |
| uint32_t left = right >> 1; |
| while (left < right) |
| { |
| uint32_t mid = (left + right) >> 1; |
| try |
| { |
| payloadUpdate->read(mid, buf); |
| left = mid + 1; |
| } |
| catch (const ResponseFailure&) |
| { |
| right = mid; |
| } |
| } |
| |
| if (left == 0) |
| { |
| stdplus::print(stderr, "Failed to get flash size from device\n"); |
| throw InterfaceError(); |
| } |
| |
| return left; |
| } |
| |
| void Hoth::confirm() |
| { |
| // Get current time to be used for Hoth's cookie |
| auto now = std::chrono::system_clock::now(); |
| auto now_ms = std::chrono::time_point_cast<std::chrono::milliseconds>(now); |
| auto cookie = now_ms.time_since_epoch(); |
| |
| payloadUpdate->confirm(payload_update_confirm_option::Confirm, 0, |
| cookie.count()); |
| } |
| |
| uint32_t Hoth::getTotalBootTime() |
| { |
| ec_response_statistics statistic = ecUtil->getHothStatistics(); |
| if (statistic.valid_words < |
| (offsetof(decltype(statistic), boot_timing_total) + |
| sizeof(statistic.boot_timing_total)) / |
| 4) |
| { |
| stdplus::print(stderr, |
| "Timing information is not included in the response\n"); |
| throw ExpectedInfoNotFound(); |
| } |
| return statistic.boot_timing_total.end_us - |
| statistic.boot_timing_total.start_us; |
| } |
| |
| uint32_t Hoth::getFirmwareUpdateTime() |
| { |
| ec_response_statistics statistic = ecUtil->getHothStatistics(); |
| if (statistic.valid_words < |
| (offsetof(decltype(statistic), boot_timing_firmware_update) + |
| sizeof(statistic.boot_timing_firmware_update)) / |
| 4) |
| { |
| stdplus::print(stderr, |
| "Timing information is not included in the response\n"); |
| throw ExpectedInfoNotFound(); |
| } |
| return statistic.boot_timing_firmware_update.end_us - |
| statistic.boot_timing_firmware_update.start_us; |
| } |
| |
| uint32_t Hoth::getFirmwareMirroringTime() |
| { |
| ec_response_statistics statistic = ecUtil->getHothStatistics(); |
| if (statistic.valid_words < |
| (offsetof(decltype(statistic), boot_timing_firmware_mirroring) + |
| sizeof(statistic.boot_timing_firmware_mirroring)) / |
| 4) |
| { |
| stdplus::print(stderr, |
| "Timing information is not included in the response\n"); |
| throw ExpectedInfoNotFound(); |
| } |
| return statistic.boot_timing_firmware_mirroring.end_us - |
| statistic.boot_timing_firmware_mirroring.start_us; |
| } |
| |
| uint32_t Hoth::getPayloadValidationTime() |
| { |
| ec_response_statistics statistic = ecUtil->getHothStatistics(); |
| if (statistic.valid_words < |
| (offsetof(decltype(statistic), boot_timing_payload_validation) + |
| sizeof(statistic.boot_timing_payload_validation)) / |
| 4) |
| { |
| stdplus::print(stderr, |
| "Timing information is not included in the response\n"); |
| throw ExpectedInfoNotFound(); |
| } |
| return statistic.boot_timing_payload_validation.end_us - |
| statistic.boot_timing_payload_validation.start_us; |
| } |
| |
| void Hoth::collectHothLogs() |
| { |
| hostCmd->collectHothLogsAsync(true); |
| } |
| |
| void Hoth::collectUartLogs() |
| { |
| hostCmd->collectUartLogsAsync(); |
| } |
| |
| void Hoth::stopUartLogs() |
| { |
| hostCmd->stopUartLogs(); |
| } |
| |
| } // namespace hoth |
| |
| } // namespace google |