flashupdate: Support key rotation

If the RoT-Config support BIOS Key rotation, the Cr51Validator will
use BIOS trust bundle information fetched from RoT config to validate
the BIOS CR51 descriptor.

The BIOS trust bundle will be:
* the trusted signature verification (public) key's finger printer.
* the allowed cr51 descriptor hash

If the RoT-Config does not support, will fallback to use key information
built-in gBMC firmware.

RoT-Config support BIOS key rotation means the RoT-Config record contain
at least one public key finger printer chunk.

For dev signed bios, the dev signature verification key will only be
built-in gBMC image. So when dev signed bios is allowed, the built-in
key will be tried if image cannot be valided by keys within RoT-Config.

Tested:
with testing bios RoT configraution

Google-Bug-Id: 421797519
Change-Id: I7ee686b94c49113ee8f406f19628851de21868b6
Signed-off-by: Dan Zhang <zhdaniel@google.com>
diff --git a/subprojects/flashupdate/include/flashupdate/validator/cr51.hpp b/subprojects/flashupdate/include/flashupdate/validator/cr51.hpp
index f32ad6c..20c5e3d 100644
--- a/subprojects/flashupdate/include/flashupdate/validator/cr51.hpp
+++ b/subprojects/flashupdate/include/flashupdate/validator/cr51.hpp
@@ -21,6 +21,7 @@
 #include <flashupdate/config.hpp>
 #include <flashupdate/validator.hpp>
 
+#include <memory>
 #include <optional>
 #include <span>
 #include <string>
@@ -33,7 +34,7 @@
 {
 namespace cr51
 {
-
+constexpr std::string_view kRoTConfigKeyRing = "RoT-Config";
 /** @class Cr51SignValidator
  *  @brief Validate the firmware with CR51 signature.
  */
@@ -56,13 +57,25 @@
     /** @brief Validate the CR51 Sign Descriptor and return the list of image
      * regions
      *
-     * @param[in] ctx         Cr51 signature context
+     * @param[in] ctx           Cr51 signature context
+     * @param[in] useRoTConfig  Whether use trusted information (keys and
+     *                          allowed list) in RoT Config
      *
      * @return std::nullopt if failed to validate the descriptor, otherwise
      * return the validated regions
      */
     virtual std::optional<struct libcr51sign_validated_regions>
-        validateDescriptor(struct libcr51sign_ctx* ctx) = 0;
+        validateDescriptor(struct libcr51sign_ctx* ctx, bool useRoTConfig) = 0;
+
+    /** @brief Check if BIOS key rotation is supported by the RoT.
+     * @return true if supported, false otherwise.
+     */
+    virtual bool isBiosKeyRotationSupport() = 0;
+
+    /** @brief Check if dev signed image is allowed.
+     * @return true if allowed, false otherwise.
+     */
+    virtual bool isDevSignedImageAllowed() = 0;
 };
 
 class Cr51SignValidatorIpml : public Cr51SignValidator
@@ -79,7 +92,11 @@
                        std::span<std::byte> imageDescriptor) override;
 
     std::optional<struct libcr51sign_validated_regions>
-        validateDescriptor(struct libcr51sign_ctx* ctx) override;
+        validateDescriptor(struct libcr51sign_ctx* ctx, bool useRoT) override;
+
+    bool isBiosKeyRotationSupport() override;
+
+    bool isDevSignedImageAllowed() override;
 
   private:
     bool prodToDev;
@@ -103,13 +120,26 @@
 class Cr51 : public Validator
 {
   public:
-    Cr51(Config::Cr51 config = Config::Cr51()) :
+    // Production constructor
+    explicit Cr51(Config::Cr51 config = Config::Cr51()) :
         imageFamily(
             config.imageFamily.value_or(image_family::IMAGE_FAMILY_ALL)),
         hash(std::vector<uint8_t>(SHA256_DIGEST_LENGTH)),
-        cr51Validator(
-            config.prodToDev, config.productionMode,
-            config.imageFamily.value_or(image_family::IMAGE_FAMILY_ALL)) {};
+        cr51ValidatorOwner(
+            std::make_unique<google::cr51::Cr51SignValidatorIpml>(
+                config.prodToDev, config.productionMode,
+                config.imageFamily.value_or(image_family::IMAGE_FAMILY_ALL))),
+        cr51Validator(*cr51ValidatorOwner)
+    {}
+
+    // Constructor for testing with a mock validator.
+    Cr51(google::cr51::Cr51SignValidator& validator,
+         Config::Cr51 config = Config::Cr51()) :
+        imageFamily(
+            config.imageFamily.value_or(image_family::IMAGE_FAMILY_ALL)),
+        hash(std::vector<uint8_t>(SHA256_DIGEST_LENGTH)),
+        cr51Validator(validator)
+    {}
     virtual ~Cr51() = default;
 
     bool validateImage(flasher::Reader& reader,
@@ -142,6 +172,9 @@
     }
 
   private:
+    std::optional<libcr51sign_validated_regions>
+        validateImageWithBuiltInKeys(std::string& usedKeys);
+
     struct image_descriptor imageDescriptor(flasher::ModArgs mod,
                                             uint32_t offset, uint32_t size);
 
@@ -160,7 +193,8 @@
     std::vector<ImageRegion> persistentRegion;
 
     // CR51 Variables
-    google::cr51::Cr51SignValidatorIpml cr51Validator;
+    std::unique_ptr<google::cr51::Cr51SignValidator> cr51ValidatorOwner;
+    google::cr51::Cr51SignValidator& cr51Validator;
     static constexpr int kSignatureRsa4096Pkcs15KeyLength = 512;
 
     struct hash_ctx shaContext;
diff --git a/subprojects/flashupdate/include/flashupdate/validator/key_rotate_helper.hpp b/subprojects/flashupdate/include/flashupdate/validator/key_rotate_helper.hpp
new file mode 100644
index 0000000..77d22bf
--- /dev/null
+++ b/subprojects/flashupdate/include/flashupdate/validator/key_rotate_helper.hpp
@@ -0,0 +1,43 @@
+// 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.
+
+#pragma once
+
+#include <libcr51sign/cr51_image_descriptor.h>
+
+#include <string_view>
+
+namespace google
+{
+namespace cr51
+{
+void setHothId(std::string_view hoth_id);
+bool trustDescriptorHash(const void*, const uint8_t*, size_t);
+bool trustKeyInCr51Signature(void* ctx, enum signature_scheme scheme,
+                             const void* cr51_signature,
+                             size_t cr51_signature_size);
+
+bool alwaysTrustDescriptorHash(const void*, const uint8_t*, size_t);
+bool alwaysTrustKeyInCr51Signature(void* ctx, enum signature_scheme scheme,
+                                   const void* cr51_signature,
+                                   size_t cr51_signature_size);
+
+bool neverTrustDescriptorHash(const void*, const uint8_t*, size_t);
+bool neverTrustKeyInCr51Signature(void* ctx, enum signature_scheme scheme,
+                                  const void* cr51_signature,
+                                  size_t cr51_signature_size);
+
+bool isBiosKeyRotationSupport();
+} // namespace cr51
+} // namespace google
diff --git a/subprojects/flashupdate/src/args.cpp b/subprojects/flashupdate/src/args.cpp
index 6dc72d3..7de4c8e 100644
--- a/subprojects/flashupdate/src/args.cpp
+++ b/subprojects/flashupdate/src/args.cpp
@@ -97,7 +97,8 @@
 
 Args::Args()
 {
-    validatorHelperPtr = std::make_unique<validator::cr51::Cr51>();
+    config.cr51 = Config::Cr51();
+    validatorHelperPtr = std::make_unique<validator::cr51::Cr51>(*config.cr51);
     validatorHelper = validatorHelperPtr.get();
 
     flashHelperPtr = std::make_unique<flash::Flash>();
diff --git a/subprojects/flashupdate/src/meson.build b/subprojects/flashupdate/src/meson.build
index 89150af..9319b8f 100644
--- a/subprojects/flashupdate/src/meson.build
+++ b/subprojects/flashupdate/src/meson.build
@@ -35,11 +35,15 @@
   stdplus_dep,
 ]
 
+# Use libhoth to talk to hoth
+libhoth_dep = dependency('libhoth')
+
 libflashupdate_deps = [
   flasher_dep,
   json_dep,
   stdplus_dep,
   libcr51sign_dep,
+  libhoth_dep,
 ]
 
 conf_data = configuration_data()
@@ -71,6 +75,7 @@
   'ops/write.cpp',
   'ops/verify_staging.cpp',
   'validator/cr51.cpp',
+  'validator/key_rotate_helper.cpp',
   'logging.cpp',
   config_h,
   version: meson.project_version(),
diff --git a/subprojects/flashupdate/src/ops/write.cpp b/subprojects/flashupdate/src/ops/write.cpp
index 1f127de..7e0969f 100644
--- a/subprojects/flashupdate/src/ops/write.cpp
+++ b/subprojects/flashupdate/src/ops/write.cpp
@@ -270,7 +270,7 @@
     else
     // Update the Staged version the the State to STAGED.
     //
-    // Flashing to secondary flash will save the has of CR51 image descriptor
+    // Flashing to secondary flash will save the hash of CR51 image descriptor
     // to the EEPROM for the final check when write to primary flash.
     {
         info.stage = version::Version(helper->imageVersion());
diff --git a/subprojects/flashupdate/src/validator/cr51.cpp b/subprojects/flashupdate/src/validator/cr51.cpp
index bdc7d7e..8700aa3 100644
--- a/subprojects/flashupdate/src/validator/cr51.cpp
+++ b/subprojects/flashupdate/src/validator/cr51.cpp
@@ -14,6 +14,8 @@
 
 #include "config.h"
 
+#include "flashupdate/validator/key_rotate_helper.hpp"
+
 #include <fcntl.h>
 #include <libcr51sign/cr51_image_descriptor.h>
 #include <libcr51sign/libcr51sign.h>
@@ -219,7 +221,8 @@
 }
 
 std::optional<struct libcr51sign_validated_regions>
-    Cr51SignValidatorIpml::validateDescriptor(struct libcr51sign_ctx* ctx)
+    Cr51SignValidatorIpml::validateDescriptor(struct libcr51sign_ctx* ctx,
+                                              bool useRoTConfig)
 {
     // Disabled stderr for all info messages
     fpos_t pos;
@@ -246,6 +249,15 @@
     intf.read = &ReadFromFd;
     intf.retrieve_stored_image_mauv_data = &ReadImageMauv;
     intf.store_new_image_mauv_data = &WriteImageMauv;
+    if (useRoTConfig)
+    {
+        // use keys in RoT Config, make sure don't provide key in ctx
+        ctx->keyring = nullptr;
+        intf.trust_descriptor_hash = trustDescriptorHash;
+        intf.trust_key_in_signature_structure = trustKeyInCr51Signature;
+        intf.verify_rsa_signature_with_modulus_and_exponent =
+            &verify_rsa_signature_with_modulus_and_exponent;
+    }
 
     auto returnTrue = []() { return true; };
     auto returnFalse = []() { return false; };
@@ -275,6 +287,16 @@
     return imageRegions;
 }
 
+bool Cr51SignValidatorIpml::isBiosKeyRotationSupport()
+{
+    return google::cr51::isBiosKeyRotationSupport();
+}
+
+bool Cr51SignValidatorIpml::isDevSignedImageAllowed()
+{
+    return (prodToDev || !productionMode);
+}
+
 } // namespace cr51
 } // namespace google
 
@@ -303,6 +325,24 @@
                        descriptor.image_subpoint);
 }
 
+std::optional<libcr51sign_validated_regions>
+    Cr51::validateImageWithBuiltInKeys(std::string& usedKeys)
+{
+    std::optional<libcr51sign_validated_regions> maybeImageRegions;
+    for (const auto& key : keys)
+    {
+        context.keyring = key.data();
+        maybeImageRegions = cr51Validator.validateDescriptor(&context, false);
+        if (maybeImageRegions)
+        {
+            LOG(LogLevel::Notice, "CR51 sign is valid using {}", key);
+            break;
+        }
+        usedKeys += key + ",";
+    }
+    return maybeImageRegions;
+}
+
 bool Cr51::validateImage(flasher::Reader& reader,
                          const std::vector<std::string>& keys)
 {
@@ -331,11 +371,6 @@
 
     struct libcr51sign_validated_regions imageRegions;
 
-    if (keys.empty())
-    {
-        throw std::runtime_error("no valid validation key available");
-    }
-
     std::unique_ptr<flasher::File> mauv = nullptr;
     if (std::string(CR51_MAUV_PATH) != "")
     {
@@ -350,17 +385,35 @@
 
     std::optional<libcr51sign_validated_regions> maybeImageRegions;
     std::string usedKeys;
-    for (const auto& key : keys)
+
+    if (cr51Validator.isBiosKeyRotationSupport())
     {
-        context.keyring = key.data();
-        maybeImageRegions = cr51Validator.validateDescriptor(&context);
+        LOG(LogLevel::Notice, "RoT-Config support BIOS Key Rotation");
+        usedKeys = google::cr51::kRoTConfigKeyRing;
+        maybeImageRegions = cr51Validator.validateDescriptor(&context, true);
+
         if (maybeImageRegions)
         {
-            LOG(LogLevel::Notice, "CR51 sign is valid using {}", key);
-            break;
+            LOG(LogLevel::Notice, "CR51 sign is valid using {}",
+                google::cr51::kRoTConfigKeyRing);
         }
-        usedKeys += key + ",";
+        else if (cr51Validator.isDevSignedImageAllowed())
+        {
+            // gBMC dev build will normally contain bios dev key which will not
+            // be in RoT-Config
+            LOG(LogLevel::Notice,
+                "CR51 sign is NOT valid using {}, but dev signed image is allowed, try built-in keys",
+                google::cr51::kRoTConfigKeyRing);
+            maybeImageRegions = validateImageWithBuiltInKeys(usedKeys);
+        }
     }
+    else
+    {
+        LOG(LogLevel::Notice,
+            "RoT-config not support BIOS Key Rotation, try built-in keys");
+        maybeImageRegions = validateImageWithBuiltInKeys(usedKeys);
+    }
+
     mauvManager = nullptr;
 
     if (!maybeImageRegions)
diff --git a/subprojects/flashupdate/src/validator/key_rotate_helper.cpp b/subprojects/flashupdate/src/validator/key_rotate_helper.cpp
new file mode 100644
index 0000000..9636977
--- /dev/null
+++ b/subprojects/flashupdate/src/validator/key_rotate_helper.cpp
@@ -0,0 +1,453 @@
+// 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 "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 <format>
+#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()};
+    int rv = libhoth_dbus_open(&opts, &hoth_device);
+    if (rv)
+    {
+        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;
+    enum key_rotation_err ret_code = libhoth_key_rotation_chunk_type_count(
+        hoth_device, chunk_typecode, &chunk_count);
+    if (ret_code)
+    {
+        LOG(flashupdate::LogLevel::Error, "Get {} chunk count failed: {}",
+            rotConfigChunkName(chunk_typecode), std::to_underlying(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 singature 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 singature: {}",
+        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)
+{
+    enum key_rotation_err 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)
+    {
+        LOG(flashupdate::LogLevel::Error, "Read {}_{} failed: ({})",
+            rotConfigChunkName(chunk_typecode), chunk_index,
+            static_cast<int>(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;
+}
+} // 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)
+    {
+        return false;
+    }
+
+    LOG(flashupdate::LogLevel::Notice, "Hoth RoT config defined {} {}",
+        trusted_bios_key_count,
+        rotConfigChunkName(KEY_ROTATION_CHUNK_TYPE_CODE_BKEY));
+
+    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))
+        {
+            LOG(flashupdate::LogLevel::Notice,
+                "Match trusted bios key finger print in {}_{}",
+                rotConfigChunkName(KEY_ROTATION_CHUNK_TYPE_CODE_BKEY),
+                chunk_index);
+            return true;
+        }
+    }
+
+    LOG(flashupdate::LogLevel::Notice, "Not match any trusted bios key");
+    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();
+    if (!hoth_device)
+    {
+        return false;
+    }
+
+    return chunkCountInHothRoTConfig(hoth_device,
+                                     KEY_ROTATION_CHUNK_TYPE_CODE_BKEY) > 0;
+}
+} // namespace cr51
+} // namespace google
diff --git a/subprojects/flashupdate/test/meson.build b/subprojects/flashupdate/test/meson.build
index 5ebd841..6ae4cb6 100644
--- a/subprojects/flashupdate/test/meson.build
+++ b/subprojects/flashupdate/test/meson.build
@@ -74,21 +74,23 @@
         'Tests enabled but flashupdate_version_test cannot build')
 endif
 
-gtests = [
-  'info',
-  'invalidate',
-  'update_state',
-  'update_version',
-  'hash_descriptor',
-  'read',
-  'write',
-  'verify_staging',
-  'fetch_version',
-  'erase',
-]
+tests = {
+  'info': 'ops/info.cpp',
+  'invalidate': 'ops/invalidate.cpp',
+  'update_state': 'ops/update_state.cpp',
+  'update_version': 'ops/update_version.cpp',
+  'hash_descriptor': 'ops/hash_descriptor.cpp',
+  'read': 'ops/read.cpp',
+  'write': 'ops/write.cpp',
+  'verify_staging': 'ops/verify_staging.cpp',
+  'fetch_version': 'ops/fetch_version.cpp',
+  'erase': 'ops/erase.cpp',
+  'key_rotate_helper': 'validator/key_rotate_helper.cpp',
+  'cr51_validator': 'validator/cr51.cpp',
+}
 
-foreach t : gtests
-  test(t, executable(t.underscorify(), 'ops/' + t + '.cpp',
+foreach name, file : tests
+  test(name, executable(name.underscorify(), file,
                      build_by_default: false,
                      implicit_include_directories: false,
                      dependencies: [libflashupdate, stdplus_gtest_dep, gtest, gmock]))
diff --git a/subprojects/flashupdate/test/ops/write.cpp b/subprojects/flashupdate/test/ops/write.cpp
index 6e7bae9..b1876f2 100644
--- a/subprojects/flashupdate/test/ops/write.cpp
+++ b/subprojects/flashupdate/test/ops/write.cpp
@@ -190,7 +190,6 @@
     updateInfo.stagingIndex = 0;
 
     Args args;
-    args.config.cr51 = Config::Cr51();
     args.primary = true;
     args.file.emplace(createTestBin());
     args.stagingIndex = 0;
@@ -434,7 +433,6 @@
     createFakeEeprom(args, filename);
 
     // Allow unsigned image to dev signed installs
-    args.config.cr51 = Config::Cr51();
     args.config.cr51->unsignedToDev = true;
 
     validator::Mock validatorMockHelper;
diff --git a/subprojects/flashupdate/test/validator/cr51.cpp b/subprojects/flashupdate/test/validator/cr51.cpp
new file mode 100644
index 0000000..869e0da
--- /dev/null
+++ b/subprojects/flashupdate/test/validator/cr51.cpp
@@ -0,0 +1,191 @@
+// 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 <flasher/file/memory.hpp>
+#include <flashupdate/validator/cr51.hpp>
+
+#include <memory>
+#include <vector>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+using ::testing::_;
+using ::testing::Invoke;
+using ::testing::Return;
+
+namespace google::cr51
+{
+
+// Mock for the Cr51SignValidator dependency
+class MockCr51SignValidator : public Cr51SignValidator
+{
+  public:
+    MOCK_METHOD(std::span<const uint8_t>, hashDescriptor,
+                (struct libcr51sign_ctx*, std::span<std::byte>), (override));
+    MOCK_METHOD(std::optional<struct libcr51sign_validated_regions>,
+                validateDescriptor, (struct libcr51sign_ctx*, bool),
+                (override));
+    MOCK_METHOD(bool, isBiosKeyRotationSupport, (), (override));
+    MOCK_METHOD(bool, isDevSignedImageAllowed, (), (override));
+};
+
+} // namespace google::cr51
+
+namespace flashupdate::validator::cr51
+{
+
+class Cr51VerifyTest : public ::testing::Test
+{
+  protected:
+    Cr51VerifyTest() :
+        cr51ValidatorMock(
+            std::make_unique<
+                ::testing::StrictMock<google::cr51::MockCr51SignValidator>>()),
+        cr51(*cr51ValidatorMock)
+    {
+        // Provide some dummy data for the reader
+        std::vector<std::byte> imageData(1024, std::byte{0});
+        reader.writeAtExact(imageData, 0);
+    }
+
+    flasher::file::Memory reader;
+    const std::vector<std::string> fallbackKeys = {"key1.pem", "key2.pem"};
+    std::unique_ptr<google::cr51::MockCr51SignValidator> cr51ValidatorMock;
+    Cr51 cr51;
+};
+
+TEST_F(Cr51VerifyTest, RoTSupported_ValidationSucceeds)
+{
+    EXPECT_CALL(*cr51ValidatorMock, isBiosKeyRotationSupport())
+        .WillOnce(Return(true));
+
+    // Neither of these should be called if RoT validation succeeds.
+    EXPECT_CALL(*cr51ValidatorMock, isDevSignedImageAllowed()).Times(0);
+    EXPECT_CALL(*cr51ValidatorMock, validateDescriptor(_, false)).Times(0);
+
+    auto successfulValidation = [](libcr51sign_ctx* ctx, bool /*useRoT*/) {
+        ctx->descriptor.descriptor_offset = 0;
+        ctx->descriptor.descriptor_area_size = 128;
+        ctx->descriptor.image_major = 1;
+        ctx->descriptor.image_minor = 2;
+        ctx->descriptor.image_point = 3;
+        ctx->descriptor.image_subpoint = 4;
+        ctx->descriptor.hash_type = HASH_SHA2_256;
+        return libcr51sign_validated_regions{};
+    };
+
+    EXPECT_CALL(*cr51ValidatorMock, validateDescriptor(_, true))
+        .WillOnce(Invoke(successfulValidation));
+    EXPECT_CALL(*cr51ValidatorMock, hashDescriptor(_, _))
+        .WillOnce(Return(std::span<const uint8_t>{}));
+    EXPECT_TRUE(cr51.validateImage(reader, fallbackKeys));
+}
+
+TEST_F(Cr51VerifyTest, RoTSupported_RoTValidationFails_AndDevNotAllowed)
+{
+    EXPECT_CALL(*cr51ValidatorMock, isBiosKeyRotationSupport())
+        .WillOnce(Return(true));
+    EXPECT_CALL(*cr51ValidatorMock, validateDescriptor(_, true))
+        .WillOnce(Return(std::nullopt));
+    EXPECT_CALL(*cr51ValidatorMock, isDevSignedImageAllowed())
+        .WillOnce(Return(false));
+    // Fallback validation should not be called.
+    EXPECT_CALL(*cr51ValidatorMock, validateDescriptor(_, false)).Times(0);
+    EXPECT_FALSE(cr51.validateImage(reader, fallbackKeys));
+}
+
+TEST_F(Cr51VerifyTest,
+       RoTSupported_RoTValidationFails_DevAllowed_FallbackSucceeds)
+{
+    EXPECT_CALL(*cr51ValidatorMock, isBiosKeyRotationSupport())
+        .WillRepeatedly(Return(true));
+    EXPECT_CALL(*cr51ValidatorMock, validateDescriptor(_, true))
+        .WillOnce(Return(std::nullopt));
+    EXPECT_CALL(*cr51ValidatorMock, isDevSignedImageAllowed())
+        .WillOnce(Return(true));
+
+    auto successfulValidation = [](libcr51sign_ctx* ctx, bool /*useRoT*/) {
+        ctx->descriptor.descriptor_offset = 0;
+        ctx->descriptor.descriptor_area_size = 128;
+        ctx->descriptor.image_major = 1;
+        ctx->descriptor.image_minor = 2;
+        ctx->descriptor.image_point = 3;
+        ctx->descriptor.image_subpoint = 4;
+        ctx->descriptor.hash_type = HASH_SHA2_256;
+        return libcr51sign_validated_regions{};
+    };
+
+    EXPECT_CALL(*cr51ValidatorMock, validateDescriptor(_, false))
+        .WillOnce(Invoke(successfulValidation));
+    EXPECT_CALL(*cr51ValidatorMock, hashDescriptor(_, _))
+        .WillOnce(Return(std::span<const uint8_t>{}));
+    EXPECT_TRUE(cr51.validateImage(reader, fallbackKeys));
+}
+
+TEST_F(Cr51VerifyTest, RoTSupported_RoTValidationFails_DevAllowed_FallbackFails)
+{
+    EXPECT_CALL(*cr51ValidatorMock, isBiosKeyRotationSupport())
+        .WillRepeatedly(Return(true));
+    EXPECT_CALL(*cr51ValidatorMock, validateDescriptor(_, true))
+        .WillOnce(Return(std::nullopt));
+    EXPECT_CALL(*cr51ValidatorMock, isDevSignedImageAllowed())
+        .WillOnce(Return(true));
+
+    // Fallback validation also fails.
+    EXPECT_CALL(*cr51ValidatorMock, validateDescriptor(_, false))
+        .Times(fallbackKeys.size())
+        .WillRepeatedly(Return(std::nullopt));
+
+    EXPECT_FALSE(cr51.validateImage(reader, fallbackKeys));
+}
+
+TEST_F(Cr51VerifyTest, RoTNotSupported_FallbackSucceeds)
+{
+    EXPECT_CALL(*cr51ValidatorMock, isBiosKeyRotationSupport())
+        .WillRepeatedly(Return(false));
+    EXPECT_CALL(*cr51ValidatorMock, isDevSignedImageAllowed()).Times(0);
+
+    // The RoT path validateDescriptor(_, true) should not be called.
+    auto successfulValidation = [](libcr51sign_ctx* ctx, bool /*useRoT*/) {
+        ctx->descriptor.descriptor_offset = 0;
+        ctx->descriptor.descriptor_area_size = 128;
+        ctx->descriptor.image_major = 1;
+        ctx->descriptor.image_minor = 2;
+        ctx->descriptor.image_point = 3;
+        ctx->descriptor.image_subpoint = 4;
+        ctx->descriptor.hash_type = HASH_SHA2_256;
+        return libcr51sign_validated_regions{};
+    };
+
+    EXPECT_CALL(*cr51ValidatorMock, validateDescriptor(_, false))
+        .WillOnce(Invoke(successfulValidation));
+    EXPECT_CALL(*cr51ValidatorMock, hashDescriptor(_, _))
+        .WillOnce(Return(std::span<const uint8_t>{}));
+    EXPECT_TRUE(cr51.validateImage(reader, fallbackKeys));
+}
+
+TEST_F(Cr51VerifyTest, RoTNotSupported_FallbackFails)
+{
+    EXPECT_CALL(*cr51ValidatorMock, isBiosKeyRotationSupport())
+        .WillRepeatedly(Return(false));
+
+    // The RoT path validateDescriptor(_, true) should not be called.
+    EXPECT_CALL(*cr51ValidatorMock, validateDescriptor(_, false))
+        .Times(fallbackKeys.size())
+        .WillRepeatedly(Return(std::nullopt));
+    EXPECT_FALSE(cr51.validateImage(reader, fallbackKeys));
+}
+
+} // namespace flashupdate::validator::cr51
diff --git a/subprojects/flashupdate/test/validator/key_rotate_helper.cpp b/subprojects/flashupdate/test/validator/key_rotate_helper.cpp
new file mode 100644
index 0000000..308f077
--- /dev/null
+++ b/subprojects/flashupdate/test/validator/key_rotate_helper.cpp
@@ -0,0 +1,655 @@
+// 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 <libcr51sign/cr51_image_descriptor.h>
+#include <libcr51sign/libcr51sign.h>
+#include <libhoth/protocol/key_rotation.h>
+#include <libhoth/transports/libhoth_dbus.h>
+
+#include <flashupdate/validator/key_rotate_helper.hpp>
+
+#include <memory>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+using ::testing::_;
+using ::testing::DoAll;
+using ::testing::Return;
+using ::testing::SetArgPointee;
+using ::testing::SetArrayArgument;
+
+// Mocking C functions from libhoth and libcr51sign
+class MockApi
+{
+  public:
+    virtual ~MockApi() = default;
+    MOCK_METHOD(int, libhoth_dbus_open,
+                (const struct libhoth_dbus_device_init_options* opts,
+                 struct libhoth_device** hoth_device));
+    MOCK_METHOD(enum key_rotation_err, libhoth_key_rotation_chunk_type_count,
+                (struct libhoth_device * hoth_device, uint32_t chunk_typecode,
+                 uint16_t* chunk_count));
+    MOCK_METHOD(enum key_rotation_err, libhoth_key_rotation_read_chunk_type,
+                (struct libhoth_device * dev, uint32_t chunk_typecode,
+                 uint32_t chunk_index, uint16_t offset, uint16_t size,
+                 struct hoth_response_key_rotation_record_read* read_response,
+                 uint16_t* response_size));
+    MOCK_METHOD(int, hash_init, (void* ctx, enum hash_type type));
+    MOCK_METHOD(int, hash_update, (void* ctx, const uint8_t* data, size_t len));
+    MOCK_METHOD(int, hash_final, (void* ctx, uint8_t* digest));
+};
+
+// Global mock object
+static std::unique_ptr<MockApi> mock_api;
+
+// C function trampolines to the mock object
+extern "C"
+{
+int libhoth_dbus_open(const struct libhoth_dbus_device_init_options* opts,
+                      struct libhoth_device** hoth_device)
+{
+    if (mock_api)
+    {
+        return mock_api->libhoth_dbus_open(opts, hoth_device);
+    }
+    return -1; // Should not happen in tests
+}
+
+enum key_rotation_err libhoth_key_rotation_chunk_type_count(
+    struct libhoth_device* hoth_device, uint32_t chunk_typecode,
+    uint16_t* chunk_count)
+{
+    if (mock_api)
+    {
+        return mock_api->libhoth_key_rotation_chunk_type_count(
+            hoth_device, chunk_typecode, chunk_count);
+    }
+    return KEY_ROTATION_ERR;
+}
+
+enum key_rotation_err libhoth_key_rotation_read_chunk_type(
+    struct libhoth_device* dev, uint32_t chunk_typecode, uint32_t chunk_index,
+    uint16_t offset, uint16_t size,
+    struct hoth_response_key_rotation_record_read* read_response,
+    uint16_t* response_size)
+{
+    if (mock_api)
+    {
+        return mock_api->libhoth_key_rotation_read_chunk_type(
+            dev, chunk_typecode, chunk_index, offset, size, read_response,
+            response_size);
+    }
+    return KEY_ROTATION_ERR;
+}
+
+int hash_init(void* ctx, enum hash_type type)
+{
+    if (mock_api)
+    {
+        return mock_api->hash_init(ctx, type);
+    }
+    return -1;
+}
+
+int hash_update(void* ctx, const uint8_t* data, size_t len)
+{
+    if (mock_api)
+    {
+        return mock_api->hash_update(ctx, data, len);
+    }
+    return -1;
+}
+
+int hash_final(void* ctx, uint8_t* digest)
+{
+    if (mock_api)
+    {
+        return mock_api->hash_final(ctx, digest);
+    }
+    return -1;
+}
+} // extern "C"
+
+namespace google::cr51
+{
+
+class KeyRotateHelperTest : public ::testing::Test
+{
+  protected:
+    void SetUp() override
+    {
+        mock_api = std::make_unique<::testing::StrictMock<MockApi>>();
+    }
+
+    void TearDown() override
+    {
+        mock_api.reset();
+    }
+
+    // A dummy device pointer. The value doesn't matter as it's just passed
+    // around.
+    struct libhoth_device* dummy_hoth_device =
+        reinterpret_cast<struct libhoth_device*>(0xDEADBEEF);
+};
+
+TEST_F(KeyRotateHelperTest, SetHothId)
+{
+    std::string id = "my-hoth-device";
+    setHothId(id);
+
+    // We can't read back HothId, but we can check if it's used correctly.
+    // This test has to run before any other test that successfully opens a hoth
+    // device, due to the static variable in hothDevice().
+    EXPECT_CALL(
+        *mock_api,
+        libhoth_dbus_open(
+            ::testing::Truly(
+                [&](const struct libhoth_dbus_device_init_options* opts) {
+                    return strcmp(opts->hoth_id, id.c_str()) == 0;
+                }),
+            _))
+        .WillOnce(Return(-1)); // Return error to not cache the device.
+    sha256 hash{};
+    trustDescriptorHash(nullptr, reinterpret_cast<uint8_t*>(&hash),
+                        sizeof(hash));
+}
+
+TEST_F(KeyRotateHelperTest, TrustDescriptorHashWrongHashSize)
+{
+    sha256 hash{};
+    EXPECT_FALSE(trustDescriptorHash(nullptr, reinterpret_cast<uint8_t*>(&hash),
+                                     sizeof(hash) - 1));
+}
+
+TEST_F(KeyRotateHelperTest, TrustDescriptorHashHothOpenFails)
+{
+    sha256 hash{};
+    EXPECT_CALL(*mock_api, libhoth_dbus_open(_, _)).WillOnce(Return(-1));
+    EXPECT_FALSE(trustDescriptorHash(nullptr, reinterpret_cast<uint8_t*>(&hash),
+                                     sizeof(hash)));
+}
+
+// Grouping tests that need a valid hoth device to avoid issues with static
+// hoth_device in hothDevice().
+class KeyRotateHelperWithDeviceTest : public KeyRotateHelperTest
+{
+  protected:
+    // This static pointer is needed because SetUpTestSuite is static.
+    static struct libhoth_device* dummy_hoth_device_for_suite;
+
+    // SetUpTestSuite is called once before any tests in this suite are run.
+    static void SetUpTestSuite()
+    {
+        // Create the mock API once for the whole suite.
+        mock_api = std::make_unique<::testing::StrictMock<MockApi>>();
+
+        // Expect the dbus_open call once for the entire suite. This will be
+        // triggered by the first test that calls hothDevice().
+        EXPECT_CALL(*mock_api, libhoth_dbus_open(_, _))
+            .WillOnce(DoAll(SetArgPointee<1>(dummy_hoth_device_for_suite),
+                            Return(0)));
+    }
+
+    // TearDownTestSuite is called once after all tests in this suite are run.
+    static void TearDownTestSuite()
+    {
+        mock_api.reset();
+    }
+
+    // Override per-test SetUp and TearDown to do nothing, preventing
+    // interference with the suite-level setup.
+    void SetUp() override {}
+    void TearDown() override {}
+};
+
+struct libhoth_device*
+    KeyRotateHelperWithDeviceTest::dummy_hoth_device_for_suite =
+        reinterpret_cast<struct libhoth_device*>(0xDEADBEEF);
+
+TEST_F(KeyRotateHelperWithDeviceTest, TrustDescriptorHashGetChunkCountFails)
+{
+    sha256 hash{};
+    EXPECT_CALL(*mock_api, libhoth_key_rotation_chunk_type_count(
+                               dummy_hoth_device_for_suite,
+                               KEY_ROTATION_CHUNK_TYPE_CODE_BASH, _))
+        .WillOnce(Return(KEY_ROTATION_ERR));
+
+    EXPECT_FALSE(trustDescriptorHash(nullptr, reinterpret_cast<uint8_t*>(&hash),
+                                     sizeof(hash)));
+}
+
+TEST_F(KeyRotateHelperWithDeviceTest, TrustDescriptorHashNoChunks)
+{
+    sha256 hash{};
+    EXPECT_CALL(*mock_api, libhoth_key_rotation_chunk_type_count(
+                               dummy_hoth_device_for_suite,
+                               KEY_ROTATION_CHUNK_TYPE_CODE_BASH, _))
+        .WillOnce(DoAll(SetArgPointee<2>(0), Return(KEY_ROTATION_CMD_SUCCESS)));
+
+    EXPECT_FALSE(trustDescriptorHash(nullptr, reinterpret_cast<uint8_t*>(&hash),
+                                     sizeof(hash)));
+}
+
+TEST_F(KeyRotateHelperWithDeviceTest, TrustDescriptorHashMatch)
+{
+    sha256 hash{0x01, 0x02, 0x03, 0x04};
+    struct hoth_response_key_rotation_record_read response = {};
+    struct bios_allowed_hash_list* hash_list =
+        reinterpret_cast<struct bios_allowed_hash_list*>(&response.data);
+    hash_list->hash_count = 1;
+    memcpy(hash_list->hash_list[0], &hash, sizeof(hash));
+
+    uint16_t response_size =
+        sizeof(struct bios_allowed_hash_list) + sizeof(sha256);
+
+    EXPECT_CALL(*mock_api, libhoth_key_rotation_chunk_type_count(
+                               dummy_hoth_device_for_suite,
+                               KEY_ROTATION_CHUNK_TYPE_CODE_BASH, _))
+        .WillOnce(DoAll(SetArgPointee<2>(1), Return(KEY_ROTATION_CMD_SUCCESS)));
+    EXPECT_CALL(*mock_api,
+                libhoth_key_rotation_read_chunk_type(
+                    dummy_hoth_device_for_suite,
+                    KEY_ROTATION_CHUNK_TYPE_CODE_BASH, 0, _, _, _, _))
+        .WillOnce(
+            DoAll(SetArgPointee<5>(response), SetArgPointee<6>(response_size),
+                  Return(KEY_ROTATION_CMD_SUCCESS)));
+    EXPECT_TRUE(trustDescriptorHash(nullptr, reinterpret_cast<uint8_t*>(&hash),
+                                    sizeof(hash)));
+}
+
+TEST_F(KeyRotateHelperWithDeviceTest, TrustDescriptorHashMatchOnSecondChunk)
+{
+    sha256 hash{0x01, 0x02, 0x03, 0x04};
+
+    // Setup for chunk_count = 3
+    EXPECT_CALL(*mock_api, libhoth_key_rotation_chunk_type_count(
+                               dummy_hoth_device_for_suite,
+                               KEY_ROTATION_CHUNK_TYPE_CODE_BASH, _))
+        .WillOnce(DoAll(SetArgPointee<2>(3), Return(KEY_ROTATION_CMD_SUCCESS)));
+
+    // Setup for first chunk (non-matching)
+    struct hoth_response_key_rotation_record_read non_matching_response = {};
+    struct bios_allowed_hash_list* non_matching_hash_list =
+        reinterpret_cast<struct bios_allowed_hash_list*>(
+            &non_matching_response.data);
+    non_matching_hash_list->hash_count = 1;
+    sha256 non_matching_hash{0xff, 0xff, 0xff, 0xff};
+    memcpy(non_matching_hash_list->hash_list[0], &non_matching_hash,
+           sizeof(non_matching_hash));
+    uint16_t non_matching_response_size =
+        sizeof(struct bios_allowed_hash_list) + sizeof(sha256);
+
+    EXPECT_CALL(*mock_api, libhoth_key_rotation_read_chunk_type(
+                               dummy_hoth_device_for_suite,
+                               KEY_ROTATION_CHUNK_TYPE_CODE_BASH, 0,
+                               sizeof(key_rotation_chunk_header), 0, _, _))
+        .WillOnce(DoAll(SetArgPointee<5>(non_matching_response),
+                        SetArgPointee<6>(non_matching_response_size),
+                        Return(KEY_ROTATION_CMD_SUCCESS)));
+
+    // Setup for second chunk (matching)
+    struct hoth_response_key_rotation_record_read matching_response = {};
+    struct bios_allowed_hash_list* matching_hash_list =
+        reinterpret_cast<struct bios_allowed_hash_list*>(
+            &matching_response.data);
+    matching_hash_list->hash_count = 1;
+    memcpy(matching_hash_list->hash_list[0], &hash, sizeof(hash));
+    uint16_t matching_response_size =
+        sizeof(struct bios_allowed_hash_list) + sizeof(sha256);
+
+    EXPECT_CALL(*mock_api, libhoth_key_rotation_read_chunk_type(
+                               dummy_hoth_device_for_suite,
+                               KEY_ROTATION_CHUNK_TYPE_CODE_BASH, 1,
+                               sizeof(key_rotation_chunk_header), 0, _, _))
+        .WillOnce(DoAll(SetArgPointee<5>(matching_response),
+                        SetArgPointee<6>(matching_response_size),
+                        Return(KEY_ROTATION_CMD_SUCCESS)));
+
+    EXPECT_TRUE(trustDescriptorHash(nullptr, reinterpret_cast<uint8_t*>(&hash),
+                                    sizeof(hash)));
+}
+
+TEST_F(KeyRotateHelperWithDeviceTest, TrustDescriptorHashMatchOnThirdChunk)
+{
+    sha256 hash{0x01, 0x02, 0x03, 0x04};
+
+    // Setup for chunk_count = 3
+    EXPECT_CALL(*mock_api, libhoth_key_rotation_chunk_type_count(
+                               dummy_hoth_device_for_suite,
+                               KEY_ROTATION_CHUNK_TYPE_CODE_BASH, _))
+        .WillOnce(DoAll(SetArgPointee<2>(3), Return(KEY_ROTATION_CMD_SUCCESS)));
+
+    // Setup for first and second chunks (non-matching)
+    for (int i = 0; i < 2; ++i)
+    {
+        struct hoth_response_key_rotation_record_read non_matching_response =
+            {};
+        struct bios_allowed_hash_list* non_matching_hash_list =
+            reinterpret_cast<struct bios_allowed_hash_list*>(
+                &non_matching_response.data);
+        non_matching_hash_list->hash_count = 1;
+        sha256 non_matching_hash{0xff, 0xff, 0xff, (uint8_t)i};
+        memcpy(non_matching_hash_list->hash_list[0], &non_matching_hash,
+               sizeof(non_matching_hash));
+        uint16_t non_matching_response_size =
+            sizeof(struct bios_allowed_hash_list) + sizeof(sha256);
+
+        EXPECT_CALL(*mock_api,
+                    libhoth_key_rotation_read_chunk_type(
+                        dummy_hoth_device_for_suite,
+                        KEY_ROTATION_CHUNK_TYPE_CODE_BASH, i, _, _, _, _))
+            .WillOnce(DoAll(SetArgPointee<5>(non_matching_response),
+                            SetArgPointee<6>(non_matching_response_size),
+                            Return(KEY_ROTATION_CMD_SUCCESS)));
+    }
+
+    // Setup for third chunk (matching)
+    struct hoth_response_key_rotation_record_read matching_response = {};
+    struct bios_allowed_hash_list* matching_hash_list =
+        reinterpret_cast<struct bios_allowed_hash_list*>(
+            &matching_response.data);
+    matching_hash_list->hash_count = 1;
+    memcpy(matching_hash_list->hash_list[0], &hash, sizeof(hash));
+    uint16_t matching_response_size =
+        sizeof(struct bios_allowed_hash_list) + sizeof(sha256);
+
+    EXPECT_CALL(*mock_api, libhoth_key_rotation_read_chunk_type(
+                               dummy_hoth_device_for_suite,
+                               KEY_ROTATION_CHUNK_TYPE_CODE_BASH, 2,
+                               sizeof(key_rotation_chunk_header), 0, _, _))
+        .WillOnce(DoAll(SetArgPointee<5>(matching_response),
+                        SetArgPointee<6>(matching_response_size),
+                        Return(KEY_ROTATION_CMD_SUCCESS)));
+
+    EXPECT_TRUE(trustDescriptorHash(nullptr, reinterpret_cast<uint8_t*>(&hash),
+                                    sizeof(hash)));
+}
+
+TEST_F(KeyRotateHelperTest, TrustKeyInCr51SignatureNullArgs)
+{
+    EXPECT_FALSE(
+        trustKeyInCr51Signature(nullptr, SIGNATURE_RSA2048_PKCS15, nullptr, 0));
+    char dummy_ctx;
+    EXPECT_FALSE(trustKeyInCr51Signature(&dummy_ctx, SIGNATURE_RSA2048_PKCS15,
+                                         nullptr, 0)); // NOLINT
+    struct signature_rsa2048_pkcs15 sig;
+    EXPECT_FALSE(trustKeyInCr51Signature(nullptr, SIGNATURE_RSA2048_PKCS15,
+                                         &sig, sizeof(sig)));
+}
+
+TEST_F(KeyRotateHelperTest, TrustKeyInCr51SignatureUnsupportedScheme)
+{
+    char dummy_ctx;
+    struct signature_rsa2048_pkcs15 sig{};
+    EXPECT_FALSE(trustKeyInCr51Signature(
+        &dummy_ctx, static_cast<enum signature_scheme>(99), &sig, sizeof(sig)));
+}
+
+TEST_F(KeyRotateHelperTest, TrustKeyInCr51SignatureHashFails)
+{
+    char dummy_ctx;
+    struct signature_rsa2048_pkcs15 sig{};
+    EXPECT_CALL(*mock_api, hash_init(_, HASH_SHA2_256)).WillOnce(Return(1));
+    EXPECT_FALSE(trustKeyInCr51Signature(&dummy_ctx, SIGNATURE_RSA2048_PKCS15,
+                                         &sig, sizeof(sig)));
+}
+
+TEST_F(KeyRotateHelperWithDeviceTest, TrustKeyInCr51SignatureMatch)
+{
+    char dummy_ctx;
+    struct signature_rsa4096_pkcs15 sig = {};
+    sha256 key_fingerprint{0x01, 0x02, 0x03, 0x04};
+
+    EXPECT_CALL(*mock_api, hash_init(_, HASH_SHA2_256)).WillOnce(Return(0));
+    EXPECT_CALL(*mock_api, hash_update(_, _, _)).WillRepeatedly(Return(0));
+    EXPECT_CALL(*mock_api, hash_final(_, _))
+        .WillOnce(DoAll(
+            SetArrayArgument<1>(reinterpret_cast<uint8_t*>(&key_fingerprint),
+                                reinterpret_cast<uint8_t*>(&key_fingerprint) +
+                                    sizeof(key_fingerprint)),
+            Return(0)));
+
+    EXPECT_CALL(*mock_api, libhoth_key_rotation_chunk_type_count(
+                               dummy_hoth_device_for_suite,
+                               KEY_ROTATION_CHUNK_TYPE_CODE_BKEY, _))
+        .WillOnce(DoAll(SetArgPointee<2>(1), Return(KEY_ROTATION_CMD_SUCCESS)));
+    struct hoth_response_key_rotation_record_read response = {};
+    struct bios_verifiction_key_fingerprint* trusted_key =
+        reinterpret_cast<struct bios_verifiction_key_fingerprint*>(
+            &response.data);
+    memcpy(trusted_key->key_fingerprint, &key_fingerprint,
+           sizeof(key_fingerprint));
+    uint16_t response_size = sizeof(struct bios_verifiction_key_fingerprint);
+
+    EXPECT_CALL(*mock_api,
+                libhoth_key_rotation_read_chunk_type(
+                    dummy_hoth_device_for_suite,
+                    KEY_ROTATION_CHUNK_TYPE_CODE_BKEY, 0, _, _, _, _))
+        .WillOnce(
+            DoAll(SetArgPointee<5>(response), SetArgPointee<6>(response_size),
+                  Return(KEY_ROTATION_CMD_SUCCESS)));
+    EXPECT_TRUE(trustKeyInCr51Signature(&dummy_ctx, SIGNATURE_RSA4096_PKCS15,
+                                        &sig, sizeof(sig)));
+}
+
+TEST_F(KeyRotateHelperWithDeviceTest, TrustKeyInCr51SignatureMatchOnSecondChunk)
+{
+    char dummy_ctx;
+    struct signature_rsa4096_pkcs15 sig = {};
+    sha256 key_fingerprint{0x01, 0x02, 0x03, 0x04};
+
+    EXPECT_CALL(*mock_api, hash_init(_, HASH_SHA2_256)).WillOnce(Return(0));
+    EXPECT_CALL(*mock_api, hash_update(_, _, _)).WillRepeatedly(Return(0));
+    EXPECT_CALL(*mock_api, hash_final(_, _))
+        .WillOnce(DoAll(
+            SetArrayArgument<1>(reinterpret_cast<uint8_t*>(&key_fingerprint),
+                                reinterpret_cast<uint8_t*>(&key_fingerprint) +
+                                    sizeof(key_fingerprint)),
+            Return(0)));
+
+    EXPECT_CALL(*mock_api, libhoth_key_rotation_chunk_type_count(
+                               dummy_hoth_device_for_suite,
+                               KEY_ROTATION_CHUNK_TYPE_CODE_BKEY, _))
+        .WillOnce(DoAll(SetArgPointee<2>(3), Return(KEY_ROTATION_CMD_SUCCESS)));
+
+    // Setup for first chunk (non-matching)
+    struct hoth_response_key_rotation_record_read non_matching_response = {};
+    struct bios_verifiction_key_fingerprint* non_matching_key =
+        reinterpret_cast<struct bios_verifiction_key_fingerprint*>(
+            &non_matching_response.data);
+    sha256 non_matching_fingerprint{0xff, 0xff, 0xff, 0xff};
+    memcpy(non_matching_key->key_fingerprint, &non_matching_fingerprint,
+           sizeof(non_matching_fingerprint));
+    uint16_t non_matching_response_size =
+        sizeof(struct bios_verifiction_key_fingerprint);
+
+    EXPECT_CALL(*mock_api, libhoth_key_rotation_read_chunk_type(
+                               dummy_hoth_device_for_suite,
+                               KEY_ROTATION_CHUNK_TYPE_CODE_BKEY, 0,
+                               sizeof(key_rotation_chunk_header), 0, _, _))
+        .WillOnce(DoAll(SetArgPointee<5>(non_matching_response),
+                        SetArgPointee<6>(non_matching_response_size),
+                        Return(KEY_ROTATION_CMD_SUCCESS)));
+
+    // Setup for second chunk (matching)
+    struct hoth_response_key_rotation_record_read matching_response = {};
+    struct bios_verifiction_key_fingerprint* matching_key =
+        reinterpret_cast<struct bios_verifiction_key_fingerprint*>(
+            &matching_response.data);
+    memcpy(matching_key->key_fingerprint, &key_fingerprint,
+           sizeof(key_fingerprint));
+    uint16_t matching_response_size =
+        sizeof(struct bios_verifiction_key_fingerprint);
+
+    EXPECT_CALL(*mock_api, libhoth_key_rotation_read_chunk_type(
+                               dummy_hoth_device_for_suite,
+                               KEY_ROTATION_CHUNK_TYPE_CODE_BKEY, 1,
+                               sizeof(key_rotation_chunk_header), 0, _, _))
+        .WillOnce(DoAll(SetArgPointee<5>(matching_response),
+                        SetArgPointee<6>(matching_response_size),
+                        Return(KEY_ROTATION_CMD_SUCCESS)));
+
+    EXPECT_TRUE(trustKeyInCr51Signature(&dummy_ctx, SIGNATURE_RSA4096_PKCS15,
+                                        &sig, sizeof(sig)));
+}
+
+TEST_F(KeyRotateHelperWithDeviceTest, TrustKeyInCr51SignatureMatchOnThirdChunk)
+{
+    char dummy_ctx;
+    struct signature_rsa4096_pkcs15 sig = {};
+    sha256 key_fingerprint{0x01, 0x02, 0x03, 0x04};
+
+    EXPECT_CALL(*mock_api, hash_init(_, HASH_SHA2_256)).WillOnce(Return(0));
+    EXPECT_CALL(*mock_api, hash_update(_, _, _)).WillRepeatedly(Return(0));
+    EXPECT_CALL(*mock_api, hash_final(_, _))
+        .WillOnce(DoAll(
+            SetArrayArgument<1>(reinterpret_cast<uint8_t*>(&key_fingerprint),
+                                reinterpret_cast<uint8_t*>(&key_fingerprint) +
+                                    sizeof(key_fingerprint)),
+            Return(0)));
+
+    EXPECT_CALL(*mock_api, libhoth_key_rotation_chunk_type_count(
+                               dummy_hoth_device_for_suite,
+                               KEY_ROTATION_CHUNK_TYPE_CODE_BKEY, _))
+        .WillOnce(DoAll(SetArgPointee<2>(3), Return(KEY_ROTATION_CMD_SUCCESS)));
+
+    // Setup for first and second chunks (non-matching)
+    for (int i = 0; i < 2; ++i)
+    {
+        struct hoth_response_key_rotation_record_read non_matching_response =
+            {};
+        struct bios_verifiction_key_fingerprint* non_matching_key =
+            reinterpret_cast<struct bios_verifiction_key_fingerprint*>(
+                &non_matching_response.data);
+        sha256 non_matching_fingerprint{0xff, 0xff, 0xff, (uint8_t)i};
+        memcpy(non_matching_key->key_fingerprint, &non_matching_fingerprint,
+               sizeof(non_matching_fingerprint));
+        uint16_t non_matching_response_size =
+            sizeof(struct bios_verifiction_key_fingerprint);
+
+        EXPECT_CALL(*mock_api,
+                    libhoth_key_rotation_read_chunk_type(
+                        dummy_hoth_device_for_suite,
+                        KEY_ROTATION_CHUNK_TYPE_CODE_BKEY, i, _, _, _, _))
+            .WillOnce(DoAll(SetArgPointee<5>(non_matching_response),
+                            SetArgPointee<6>(non_matching_response_size),
+                            Return(KEY_ROTATION_CMD_SUCCESS)));
+    }
+
+    // Setup for third chunk (matching)
+    struct hoth_response_key_rotation_record_read matching_response = {};
+    struct bios_verifiction_key_fingerprint* matching_key =
+        reinterpret_cast<struct bios_verifiction_key_fingerprint*>(
+            &matching_response.data);
+    memcpy(matching_key->key_fingerprint, &key_fingerprint,
+           sizeof(key_fingerprint));
+    uint16_t matching_response_size =
+        sizeof(struct bios_verifiction_key_fingerprint);
+
+    EXPECT_CALL(*mock_api, libhoth_key_rotation_read_chunk_type(
+                               dummy_hoth_device_for_suite,
+                               KEY_ROTATION_CHUNK_TYPE_CODE_BKEY, 2,
+                               sizeof(key_rotation_chunk_header), 0, _, _))
+        .WillOnce(DoAll(SetArgPointee<5>(matching_response),
+                        SetArgPointee<6>(matching_response_size),
+                        Return(KEY_ROTATION_CMD_SUCCESS)));
+
+    EXPECT_TRUE(trustKeyInCr51Signature(&dummy_ctx, SIGNATURE_RSA4096_PKCS15,
+                                        &sig, sizeof(sig)));
+}
+
+TEST_F(KeyRotateHelperTest, AlwaysTrustDescriptorHash)
+{
+    sha256 hash{};
+    // It should return true even if the underlying function returns false.
+    EXPECT_CALL(*mock_api, libhoth_dbus_open(_, _)).WillOnce(Return(-1));
+    EXPECT_TRUE(alwaysTrustDescriptorHash(
+        nullptr, reinterpret_cast<uint8_t*>(&hash), sizeof(hash)));
+}
+
+TEST_F(KeyRotateHelperTest, AlwaysTrustKeyInCr51Signature)
+{
+    char dummy_ctx;
+    struct signature_rsa2048_pkcs15 sig{};
+    // It should return true even if the underlying function returns false.
+    EXPECT_CALL(*mock_api, hash_init(_, _)).WillOnce(Return(1)); // make it fail
+    EXPECT_TRUE(alwaysTrustKeyInCr51Signature(
+        &dummy_ctx, SIGNATURE_RSA2048_PKCS15, &sig, sizeof(sig)));
+}
+
+TEST_F(KeyRotateHelperWithDeviceTest, NeverTrustDescriptorHash)
+{
+    sha256 hash{0x01, 0x02, 0x03, 0x04};
+    // It should return false even if the underlying function returns true.
+    struct hoth_response_key_rotation_record_read response = {};
+    struct bios_allowed_hash_list* hash_list =
+        reinterpret_cast<struct bios_allowed_hash_list*>(&response.data);
+    hash_list->hash_count = 1;
+    memcpy(hash_list->hash_list[0], &hash, sizeof(hash));
+    uint16_t response_size =
+        sizeof(struct bios_allowed_hash_list) + sizeof(sha256);
+
+    EXPECT_CALL(*mock_api, libhoth_key_rotation_chunk_type_count(
+                               dummy_hoth_device_for_suite,
+                               KEY_ROTATION_CHUNK_TYPE_CODE_BASH, _))
+        .WillOnce(DoAll(SetArgPointee<2>(1), Return(KEY_ROTATION_CMD_SUCCESS)));
+    EXPECT_CALL(*mock_api,
+                libhoth_key_rotation_read_chunk_type(
+                    dummy_hoth_device_for_suite,
+                    KEY_ROTATION_CHUNK_TYPE_CODE_BASH, 0, _, _, _, _))
+        .WillOnce(
+            DoAll(SetArgPointee<5>(response), SetArgPointee<6>(response_size),
+                  Return(KEY_ROTATION_CMD_SUCCESS)));
+
+    EXPECT_FALSE(neverTrustDescriptorHash(
+        nullptr, reinterpret_cast<uint8_t*>(&hash), sizeof(hash)));
+}
+
+TEST_F(KeyRotateHelperTest, IsBiosKeyRotationSupportHothOpenFails)
+{
+    // This test must run before any test that successfully opens a hoth device
+    // due to the static hoth_device cache in the production code.
+    EXPECT_CALL(*mock_api, libhoth_dbus_open(_, _)).WillOnce(Return(-1));
+    EXPECT_FALSE(isBiosKeyRotationSupport());
+}
+
+TEST_F(KeyRotateHelperWithDeviceTest, IsBiosKeyRotationSupportChunkCountFails)
+{
+    EXPECT_CALL(*mock_api, libhoth_key_rotation_chunk_type_count(
+                               dummy_hoth_device_for_suite,
+                               KEY_ROTATION_CHUNK_TYPE_CODE_BKEY, _))
+        .WillOnce(Return(KEY_ROTATION_ERR));
+    EXPECT_FALSE(isBiosKeyRotationSupport());
+}
+
+TEST_F(KeyRotateHelperWithDeviceTest, IsBiosKeyRotationSupportNoChunks)
+{
+    EXPECT_CALL(*mock_api, libhoth_key_rotation_chunk_type_count(
+                               dummy_hoth_device_for_suite,
+                               KEY_ROTATION_CHUNK_TYPE_CODE_BKEY, _))
+        .WillOnce(DoAll(SetArgPointee<2>(0), Return(KEY_ROTATION_CMD_SUCCESS)));
+    EXPECT_FALSE(isBiosKeyRotationSupport());
+}
+
+TEST_F(KeyRotateHelperWithDeviceTest, IsBiosKeyRotationSupportSuccess)
+{
+    EXPECT_CALL(*mock_api, libhoth_key_rotation_chunk_type_count(
+                               dummy_hoth_device_for_suite,
+                               KEY_ROTATION_CHUNK_TYPE_CODE_BKEY, _))
+        .WillOnce(DoAll(SetArgPointee<2>(1), Return(KEY_ROTATION_CMD_SUCCESS)));
+    EXPECT_TRUE(isBiosKeyRotationSupport());
+}
+
+} // namespace google::cr51