| // Copyright 2025 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 "config.h" |
| |
| #include "flashupdate/validator/key_rotate_helper.hpp" |
| |
| #include "flashupdate/info.hpp" |
| #include "flashupdate/logging.hpp" |
| |
| #include <libcr51sign/cr51_image_descriptor.h> |
| #include <libcr51sign/libcr51sign.h> |
| #include <libhoth/protocol/key_rotation.h> |
| #include <libhoth/transports/libhoth_dbus.h> |
| |
| #include <cstdint> |
| #include <filesystem> |
| #include <format> |
| #include <fstream> |
| #include <string_view> |
| |
| namespace |
| { |
| std::string HothId{}; |
| |
| struct libhoth_device* hothDevice() |
| { |
| static struct libhoth_device* hoth_device = nullptr; |
| if (hoth_device) |
| { |
| return hoth_device; |
| } |
| |
| struct libhoth_dbus_device_init_options opts = {.hoth_id = HothId.c_str()}; |
| libhoth_error rv = libhoth_dbus_open(&opts, &hoth_device); |
| if (rv != LIBHOTH_OK) |
| { |
| LOG(flashupdate::LogLevel::Error, "libhoth_dbus_open failed {}", rv); |
| return nullptr; |
| } |
| return hoth_device; |
| } |
| |
| inline std::string rotConfigChunkName(uint32_t chunk_typecode) |
| { |
| switch (chunk_typecode) |
| { |
| case KEY_ROTATION_CHUNK_TYPE_CODE_PKEY: |
| return std::string{"KEY_ROTATION_CHUNK_TYPE_CODE_PKEY"}; |
| case KEY_ROTATION_CHUNK_TYPE_CODE_HASH: |
| return std::string{"KEY_ROTATION_CHUNK_TYPE_CODE_HASH"}; |
| case KEY_ROTATION_CHUNK_TYPE_CODE_BKEY: |
| return std::string{"KEY_ROTATION_CHUNK_TYPE_CODE_BKEY"}; |
| case KEY_ROTATION_CHUNK_TYPE_CODE_BASH: |
| return std::string{"KEY_ROTATION_CHUNK_TYPE_CODE_BASH"}; |
| default: |
| return std::format("Unknown {:X}", chunk_typecode); |
| } |
| } |
| |
| __attribute__((nonnull)) int chunkCountInHothRoTConfig( |
| struct libhoth_device* hoth_device, uint32_t chunk_typecode) |
| { |
| uint16_t chunk_count = 0; |
| libhoth_error ret_code = libhoth_key_rotation_chunk_type_count( |
| hoth_device, chunk_typecode, &chunk_count); |
| if (ret_code != LIBHOTH_OK) |
| { |
| LOG(flashupdate::LogLevel::Error, "Get {} chunk count failed: {}", |
| rotConfigChunkName(chunk_typecode), ret_code); |
| return -1; |
| } |
| return chunk_count; |
| } |
| |
| struct BiosKeyInfo |
| { |
| const uint8_t* e_bytes; |
| size_t e_size; |
| const uint8_t* n_bytes; |
| size_t n_size; |
| }; |
| |
| template <typename T> |
| BiosKeyInfo |
| biosKeyInfoFromCr51Signature(T* cr51_signature, size_t cr51_signature_size) |
| { |
| size_t expected_signature_size = sizeof(T); |
| if (expected_signature_size != cr51_signature_size) |
| { |
| LOG(flashupdate::LogLevel::Error, |
| "signture structure has invalid size({}), expect ({})", |
| cr51_signature_size, expected_signature_size); |
| return {nullptr, 0, nullptr, 0}; |
| } |
| return {reinterpret_cast<const uint8_t*>(&cr51_signature->exponent), |
| sizeof(cr51_signature->exponent), |
| reinterpret_cast<const uint8_t*>(&cr51_signature->modulus), |
| sizeof(cr51_signature->modulus)}; |
| } |
| |
| std::string_view cr51SignSchemeName(enum signature_scheme scheme) |
| { |
| switch (scheme) |
| { |
| case SIGNATURE_RSA2048_PKCS15: |
| return "SIGNATURE_RSA2048_PKCS15"; |
| case SIGNATURE_RSA3072_PKCS15: |
| return "SIGNATURE_RSA3072_PKCS15"; |
| case SIGNATURE_RSA4096_PKCS15: |
| return "SIGNATURE_RSA4096_PKCS15"; |
| case SIGNATURE_RSA4096_PKCS15_SHA512: |
| return "SIGNATURE_RSA4096_PKCS15_SHA512"; |
| default: |
| return "UNKNOWN"; |
| } |
| } |
| |
| __attribute__((nonnull)) bool fingerPrintOfKeyInCr51Signature( |
| void* ctx, enum signature_scheme scheme, const void* cr51_signature, |
| size_t cr51_signature_size, sha256& finger_print) |
| { |
| BiosKeyInfo bios_key_info{}; |
| |
| LOG(flashupdate::LogLevel::Notice, |
| "Calculating fingerprint of key in CR51 signature structure with scheme({}), size({})", |
| cr51SignSchemeName(scheme), cr51_signature_size); |
| switch (scheme) |
| { |
| case SIGNATURE_RSA2048_PKCS15: |
| { |
| bios_key_info = biosKeyInfoFromCr51Signature( |
| reinterpret_cast<const struct signature_rsa2048_pkcs15*>( |
| cr51_signature), |
| cr51_signature_size); |
| } |
| break; |
| case SIGNATURE_RSA3072_PKCS15: |
| { |
| bios_key_info = biosKeyInfoFromCr51Signature( |
| reinterpret_cast<const struct signature_rsa3072_pkcs15*>( |
| cr51_signature), |
| cr51_signature_size); |
| } |
| break; |
| case SIGNATURE_RSA4096_PKCS15: |
| case SIGNATURE_RSA4096_PKCS15_SHA512: |
| { |
| bios_key_info = biosKeyInfoFromCr51Signature( |
| reinterpret_cast<const struct signature_rsa4096_pkcs15*>( |
| cr51_signature), |
| cr51_signature_size); |
| } |
| break; |
| default: |
| { |
| LOG(flashupdate::LogLevel::Error, "Not supported scheme({})", |
| static_cast<int>(scheme)); |
| } |
| return false; |
| } |
| |
| // BIOS Key finger print is sha2_256(e || n) |
| int ec = hash_init(ctx, HASH_SHA2_256); |
| if (ec) |
| { |
| LOG(flashupdate::LogLevel::Error, "CR51 Hash init error: ({})", ec); |
| return false; |
| } |
| |
| ec = hash_update(ctx, bios_key_info.e_bytes, bios_key_info.e_size); |
| if (ec) |
| { |
| LOG(flashupdate::LogLevel::Error, |
| "CR51 Hash update e_bytes error: ({})", ec); |
| return false; |
| } |
| |
| ec = hash_update(ctx, bios_key_info.n_bytes, bios_key_info.n_size); |
| if (ec) |
| { |
| LOG(flashupdate::LogLevel::Error, |
| "CR51 Hash update n_bytes error: ({})", ec); |
| return false; |
| } |
| |
| ec = hash_final(ctx, finger_print); |
| if (ec) |
| { |
| LOG(flashupdate::LogLevel::Error, "CR51 Hash final error: ({})", ec); |
| return false; |
| } |
| |
| LOG(flashupdate::LogLevel::Notice, |
| "fingerprint of key in CR51 signature: {}", |
| flashupdate::info::bytesToHex(finger_print)); |
| return true; |
| } |
| |
| inline bool sha256Equal(const sha256& left, const sha256& right) |
| { |
| return (memcmp(&left, &right, sizeof(sha256)) == 0); |
| } |
| |
| __attribute__((nonnull)) bool readRoTConfigChunk( |
| struct libhoth_device* hoth_device, uint32_t chunk_typecode, |
| int chunk_index, |
| struct hoth_response_key_rotation_record_read* read_response, |
| size_t expected_data_size, uint16_t* response_size) |
| { |
| libhoth_error ret_read = libhoth_key_rotation_read_chunk_type( |
| hoth_device, chunk_typecode, chunk_index, |
| sizeof(key_rotation_chunk_header) /* skip the trunk header */, |
| 0 /* read whole chunk data */, read_response, response_size); |
| if (ret_read != LIBHOTH_OK) |
| { |
| LOG(flashupdate::LogLevel::Error, "Read {}_{} failed: ({})", |
| rotConfigChunkName(chunk_typecode), chunk_index, ret_read); |
| return false; |
| } |
| // To be backward compatible allow in the future appending new fields in |
| // the Chunk, here only make sure fetched enough data the code understand |
| // also we need support variable length Chunk like bios_allowed_list |
| if (*response_size < expected_data_size) |
| { |
| LOG(flashupdate::LogLevel::Warning, |
| "Ignore {}_{}, as size ({}) is less than expected ({})", |
| rotConfigChunkName(chunk_typecode), chunk_index, *response_size, |
| expected_data_size); |
| return false; |
| } |
| return true; |
| } |
| |
| void writeBiosKeyMetricFiles(std::string_view validation_method, |
| uint32_t image_family, uint32_t version, |
| uint64_t validation_key_data) |
| { |
| std::string dir = BIOS_KEY_METRICS_DIR; |
| std::string tmp_dir = dir + ".tmp"; |
| std::error_code ec; |
| |
| // 1. Create and write to a .tmp directory first. We use the |
| // write-to-tmp-and-rename pattern to ensure that the metrics update is |
| // atomic. This prevents a telemetry reader from experiencing race |
| // conditions, such as reading partially-written files (partial writes) or a |
| // mix of old and new files (group inconsistency). |
| std::filesystem::create_directories(tmp_dir, ec); |
| if (ec) |
| { |
| LOG(flashupdate::LogLevel::Error, "Failed to create directory {}: {}", |
| tmp_dir, ec.message()); |
| return; |
| } |
| |
| auto write_file = [](const std::filesystem::path& path, |
| const std::string_view content) { |
| std::ofstream file(path, std::ios::out | std::ios::trunc); |
| if (!file.is_open()) |
| { |
| LOG(flashupdate::LogLevel::Error, |
| "Failed to open file for writing: {}", path.string()); |
| return; |
| } |
| file << content << "\n"; |
| }; |
| |
| std::filesystem::path tmp_dir_path(tmp_dir); |
| write_file(tmp_dir_path / "bios_key_validation_method", validation_method); |
| write_file(tmp_dir_path / "bios_key_image_family", |
| std::to_string(image_family)); |
| write_file(tmp_dir_path / "bios_key_version", std::to_string(version)); |
| write_file(tmp_dir_path / "bios_key_validation_key_data", |
| std::format("0x{:016x}", validation_key_data)); |
| |
| // 2. Delete the old target directory |
| std::filesystem::remove_all(dir, ec); |
| if (ec && ec != std::errc::no_such_file_or_directory) |
| { |
| LOG(flashupdate::LogLevel::Error, |
| "Failed to remove old directory {}: {}", dir, ec.message()); |
| return; |
| } |
| |
| // 3. Rename tmp directory to target directory |
| std::filesystem::rename(tmp_dir, dir, ec); |
| if (ec) |
| { |
| LOG(flashupdate::LogLevel::Error, |
| "Failed to rename directory {} to {}: {}", tmp_dir, dir, |
| ec.message()); |
| return; |
| } |
| |
| LOG(flashupdate::LogLevel::Notice, |
| "Write bios key metric files: {} {} {} {}", validation_method, |
| image_family, version, std::format("0x{:016x}", validation_key_data)); |
| } |
| |
| void clearBiosKeyMetricFiles() |
| { |
| std::string dir = BIOS_KEY_METRICS_DIR; |
| std::error_code ec; |
| std::filesystem::remove_all(dir, ec); |
| if (ec && ec != std::errc::no_such_file_or_directory) |
| { |
| LOG(flashupdate::LogLevel::Error, |
| "Failed to clear metrics directory {}: {}", dir, ec.message()); |
| } |
| } |
| |
| const uint8_t* getModulusFromCr51Signature(enum signature_scheme scheme, |
| const void* cr51_signature) |
| { |
| if (!cr51_signature) |
| { |
| return nullptr; |
| } |
| switch (scheme) |
| { |
| case SIGNATURE_RSA2048_PKCS15: |
| return reinterpret_cast<const struct signature_rsa2048_pkcs15*>( |
| cr51_signature) |
| ->modulus; |
| case SIGNATURE_RSA3072_PKCS15: |
| return reinterpret_cast<const struct signature_rsa3072_pkcs15*>( |
| cr51_signature) |
| ->modulus; |
| case SIGNATURE_RSA4096_PKCS15: |
| case SIGNATURE_RSA4096_PKCS15_SHA512: |
| return reinterpret_cast<const struct signature_rsa4096_pkcs15*>( |
| cr51_signature) |
| ->modulus; |
| default: |
| return nullptr; |
| } |
| } |
| |
| } // namespace |
| |
| namespace google |
| { |
| namespace cr51 |
| { |
| void setHothId(std::string_view hoth_id) |
| { |
| HothId = hoth_id; |
| } |
| |
| bool trustDescriptorHash(const void*, const uint8_t* descriptor_hash, |
| size_t hash_size) |
| { |
| // Currently all platforms BIOS updated or validatored by BMC are using |
| // sha256 |
| if (hash_size != sizeof(sha256)) |
| { |
| LOG(flashupdate::LogLevel::Notice, |
| "All trusted descriptor hash are sha256, " |
| "the input descriptor hash is not sha256 " |
| "based on hash size {}, so not trust it", |
| hash_size); |
| return false; |
| } |
| LOG(flashupdate::LogLevel::Notice, "check cr51hash: {}", |
| flashupdate::info::bytesToHex( |
| std::span<const uint8_t>(descriptor_hash, hash_size))); |
| |
| struct libhoth_device* hoth_device = hothDevice(); |
| if (!hoth_device) |
| { |
| return false; |
| } |
| |
| int trusted_bios_hash_chunk_count = chunkCountInHothRoTConfig( |
| hoth_device, KEY_ROTATION_CHUNK_TYPE_CODE_BASH); |
| if (trusted_bios_hash_chunk_count < 0) |
| { |
| return false; |
| } |
| |
| LOG(flashupdate::LogLevel::Notice, "Hoth RoT config defined {} {}", |
| trusted_bios_hash_chunk_count, |
| rotConfigChunkName(KEY_ROTATION_CHUNK_TYPE_CODE_BASH)); |
| |
| for (int chunk_index = 0; chunk_index < trusted_bios_hash_chunk_count; |
| ++chunk_index) |
| { |
| uint16_t response_size = 0; |
| struct hoth_response_key_rotation_record_read read_response; |
| if (!readRoTConfigChunk(hoth_device, KEY_ROTATION_CHUNK_TYPE_CODE_BASH, |
| chunk_index, &read_response, |
| sizeof(struct bios_allowed_hash_list), |
| &response_size)) |
| { |
| continue; |
| } |
| const struct bios_allowed_hash_list* trusted_bios_hash_list = |
| reinterpret_cast<const struct bios_allowed_hash_list*>( |
| &read_response.data); |
| uint16_t expected_data_size = |
| sizeof(struct bios_allowed_hash_list) + |
| sizeof(sha256) * trusted_bios_hash_list->hash_count; |
| if (response_size < expected_data_size) |
| { |
| LOG(flashupdate::LogLevel::Warning, |
| "Ignore variable length {}_{}, as size ({}) is less than expected ({})", |
| rotConfigChunkName(KEY_ROTATION_CHUNK_TYPE_CODE_BASH), |
| chunk_index, response_size, expected_data_size); |
| } |
| for (uint32_t hash_index = 0; |
| hash_index < trusted_bios_hash_list->hash_count; ++hash_index) |
| { |
| LOG(flashupdate::LogLevel::Notice, "allowed hash[{}] = {{ {} }}", |
| hash_index, |
| flashupdate::info::bytesToHex( |
| trusted_bios_hash_list->hash_list[hash_index])); |
| if (sha256Equal(trusted_bios_hash_list->hash_list[hash_index], |
| *reinterpret_cast<const sha256*>(descriptor_hash))) |
| { |
| LOG(flashupdate::LogLevel::Notice, |
| "Match allowed hash #{} in {}_{}", hash_index, |
| rotConfigChunkName(KEY_ROTATION_CHUNK_TYPE_CODE_BKEY), |
| chunk_index); |
| return true; |
| } |
| } |
| } |
| |
| LOG(flashupdate::LogLevel::Notice, |
| "Not match any trusted bios allowed hash"); |
| return false; |
| } |
| |
| bool trustKeyInCr51Signature(void* ctx, enum signature_scheme scheme, |
| const void* cr51_signature, |
| size_t cr51_signature_size) |
| { |
| if (!ctx || !cr51_signature) |
| { |
| LOG(flashupdate::LogLevel::Alert, |
| "input ctx or cr51_signature is NULL"); |
| return false; |
| } |
| sha256 key_fingerprint; |
| if (!fingerPrintOfKeyInCr51Signature(ctx, scheme, cr51_signature, |
| cr51_signature_size, key_fingerprint)) |
| { |
| return false; |
| } |
| struct libhoth_device* hoth_device = hothDevice(); |
| if (!hoth_device) |
| { |
| return false; |
| } |
| |
| int trusted_bios_key_count = chunkCountInHothRoTConfig( |
| hoth_device, KEY_ROTATION_CHUNK_TYPE_CODE_BKEY); |
| if (trusted_bios_key_count < 0) |
| { |
| writeBiosKeyMetricFiles("EMBEDDED_KEY", 0, 0, 0); |
| return false; |
| } |
| |
| LOG(flashupdate::LogLevel::Notice, "Hoth RoT config defined {} {}", |
| trusted_bios_key_count, |
| rotConfigChunkName(KEY_ROTATION_CHUNK_TYPE_CODE_BKEY)); |
| struct libcr51sign_ctx* cr51signCtx = |
| reinterpret_cast<struct libcr51sign_ctx*>(ctx); |
| for (int chunk_index = 0; chunk_index < trusted_bios_key_count; |
| ++chunk_index) |
| { |
| uint16_t response_size = 0; |
| struct hoth_response_key_rotation_record_read read_response; |
| if (!readRoTConfigChunk(hoth_device, KEY_ROTATION_CHUNK_TYPE_CODE_BKEY, |
| chunk_index, &read_response, |
| sizeof(struct bios_verifiction_key_fingerprint), |
| &response_size)) |
| { |
| continue; |
| } |
| const struct bios_verifiction_key_fingerprint* trusted_bios_key = |
| reinterpret_cast<const struct bios_verifiction_key_fingerprint*>( |
| &read_response.data); |
| LOG(flashupdate::LogLevel::Notice, |
| "trusted bios key finger print in {}_{}: {}", |
| rotConfigChunkName(KEY_ROTATION_CHUNK_TYPE_CODE_BKEY), chunk_index, |
| flashupdate::info::bytesToHex(trusted_bios_key->key_fingerprint)); |
| if (sha256Equal(trusted_bios_key->key_fingerprint, key_fingerprint)) |
| { |
| uint32_t imageFamily = cr51signCtx->descriptor.image_family; |
| if (imageFamily != trusted_bios_key->image_family && |
| imageFamily != IMAGE_FAMILY_ALL && |
| trusted_bios_key->image_family != IMAGE_FAMILY_ALL) |
| { |
| LOG(flashupdate::LogLevel::Warning, |
| "Match trusted bios key finger print in {}_{}, but mismatch IMAGE_FAMILY {} vs. {}", |
| rotConfigChunkName(KEY_ROTATION_CHUNK_TYPE_CODE_BKEY), |
| chunk_index, imageFamily, trusted_bios_key->image_family); |
| continue; |
| } |
| LOG(flashupdate::LogLevel::Notice, |
| "Match trusted bios key finger print in {}_{}", |
| rotConfigChunkName(KEY_ROTATION_CHUNK_TYPE_CODE_BKEY), |
| chunk_index); |
| |
| uint32_t version = 0; |
| struct hoth_response_key_rotation_record_version record_version; |
| libhoth_error version_err = |
| libhoth_key_rotation_get_version(hoth_device, &record_version); |
| if (version_err == LIBHOTH_OK) |
| { |
| version = record_version.version; |
| } |
| else |
| { |
| LOG(flashupdate::LogLevel::Error, |
| "Get key rotation version failed: {}", version_err); |
| } |
| |
| uint64_t val_key_data = 0; |
| const uint8_t* modulus = |
| getModulusFromCr51Signature(scheme, cr51_signature); |
| if (modulus) |
| { |
| for (size_t i = 0; i < 8; ++i) |
| { |
| val_key_data = (val_key_data << 8) | modulus[i]; |
| } |
| } |
| |
| writeBiosKeyMetricFiles("PAYLOAD_KEY", |
| trusted_bios_key->image_family, version, |
| val_key_data); |
| |
| return true; |
| } |
| } |
| |
| LOG(flashupdate::LogLevel::Notice, "Not match any trusted bios key"); |
| writeBiosKeyMetricFiles("EMBEDDED_KEY", 0, 0, 0); |
| |
| return false; |
| } |
| |
| // For Testing or debug |
| bool alwaysTrustDescriptorHash(const void* ctx, const uint8_t* descriptor_hash, |
| size_t hash_size) |
| { |
| bool trust_by_rot = trustDescriptorHash(ctx, descriptor_hash, hash_size); |
| LOG(flashupdate::LogLevel::Notice, |
| "Override RoT decision {} to always trust", trust_by_rot); |
| return true; |
| } |
| |
| bool alwaysTrustKeyInCr51Signature(void* ctx, enum signature_scheme scheme, |
| const void* cr51_signature, |
| size_t cr51_signature_size) |
| { |
| bool trust_by_rot = trustKeyInCr51Signature(ctx, scheme, cr51_signature, |
| cr51_signature_size); |
| LOG(flashupdate::LogLevel::Notice, |
| "Override RoT decision {} to always trust", trust_by_rot); |
| return true; |
| } |
| |
| bool neverTrustDescriptorHash(const void* ctx, const uint8_t* descriptor_hash, |
| size_t hash_size) |
| { |
| bool trust_by_rot = trustDescriptorHash(ctx, descriptor_hash, hash_size); |
| LOG(flashupdate::LogLevel::Notice, |
| "Override RoT decision {} to never trust", trust_by_rot); |
| return false; |
| } |
| bool neverTrustKeyInCr51Signature(void* ctx, enum signature_scheme scheme, |
| const void* cr51_signature, |
| size_t cr51_signature_size) |
| { |
| bool trust_by_rot = trustKeyInCr51Signature(ctx, scheme, cr51_signature, |
| cr51_signature_size); |
| LOG(flashupdate::LogLevel::Notice, |
| "Override RoT decision {} to never trust", trust_by_rot); |
| return false; |
| } |
| |
| bool isBiosKeyRotationSupport() |
| { |
| struct libhoth_device* hoth_device = hothDevice(); |
| |
| // Clear any stale metric files written by previous runs. |
| clearBiosKeyMetricFiles(); |
| |
| if (hoth_device && chunkCountInHothRoTConfig( |
| hoth_device, KEY_ROTATION_CHUNK_TYPE_CODE_BKEY) > 0) |
| { |
| return true; |
| } |
| // If rotation is not supported, we immediately record that we are |
| // using the EMBEDDED_KEY |
| writeBiosKeyMetricFiles("EMBEDDED_KEY", 0, 0, 0); |
| return false; |
| } |
| } // namespace cr51 |
| } // namespace google |