4. 固件完整性度量:哈希链构建、SM3哈希与SM2签名的结合、度量根(RTM)设计

好,咱们进入第四章。这一章讲的是固件完整性度量,说白了就是怎么证明你的固件没被人动过手脚。我做了这么多年嵌入式安全,见过太多因为没做完整性校验而被攻破的案例。有一次,一个客户的产品在野外被刷了恶意固件,整批设备变成了僵尸网络的一部分。原因?就是他们只做了签名校验,没做链式度量。嗯,咱们今天就把这个坑填上。

4.1 为什么需要哈希链?

你想想看,一个固件通常不是一整块二进制。它包含Bootloader、OS Kernel、文件系统、应用分区等等。如果只给整个固件打一个签名,那启动时就得把整个固件加载完才能验签。这太慢了,而且不安全——万一中间某个阶段被替换了,你根本不知道。

哈希链的思路是:逐级度量,逐级验证。每一级在启动下一级之前,先计算下一级的哈希值,然后用自己的私钥对这个哈希签名。下一级启动时,先验签,再计算自己的哈希比对。这样环环相扣,任何一级被篡改,链条就断了。

核心原则:信任不能传递,只能度量。每一级只信任上一级度量过的内容。

4.2 SM3哈希与SM2签名的结合

国密标准里,SM3负责算哈希,SM2负责签名和验签。这两者怎么配合?我习惯用一个简单的流程:

  1. 度量阶段:当前级(比如Bootloader)读取下一级(比如OS Kernel)的二进制镜像,用SM3算出256位的哈希值H。
  2. 签名阶段:当前级用自己的SM2私钥对H签名,生成签名值S。把S和H一起存到安全存储区,或者直接附加在镜像后面。
  3. 验证阶段:下一级启动时,先读取签名值S,用上一级的SM2公钥验签。验签通过后,再自己算一遍SM3哈希,比对是否等于H。

这里有个细节:签名的是哈希,不是整个镜像。SM2签名算法对输入长度有限制,而且签名整个镜像效率太低。哈希一下,再签名,这是标准做法。

我的习惯:在签名时,我会把哈希值H和当前级的版本号、时间戳一起打包签名。这样能防止重放攻击——有人拿旧版本的合法固件来替换新版本。

4.2.1 代码示例:SM3哈希计算与SM2签名

下面是一个简化的C代码片段,展示如何在Bootloader里计算下一级镜像的SM3哈希,并用SM2签名。实际项目中,SM2的密钥对需要从HSM或安全元件中读取,这里为了演示直接传参。

#include "sm3.h"
#include "sm2.h"

// 计算镜像的SM3哈希
int compute_image_hash(uint8_t *image, uint32_t image_len, uint8_t hash[32]) {
    sm3_context ctx;
    sm3_init(&ctx);
    sm3_update(&ctx, image, image_len);
    sm3_final(&ctx, hash);
    return 0;
}

// 用SM2私钥签名哈希
int sign_hash(uint8_t *hash, uint32_t hash_len, 
              uint8_t *private_key, uint32_t key_len,
              uint8_t signature[64]) {
    sm2_signature sig;
    sm2_sign(private_key, hash, hash_len, &sig);
    // 将R,S拼接成64字节签名
    memcpy(signature, sig.r, 32);
    memcpy(signature + 32, sig.s, 32);
    return 0;
}

// 验证签名
int verify_signature(uint8_t *hash, uint32_t hash_len,
                     uint8_t *public_key, uint32_t key_len,
                     uint8_t signature[64]) {
    sm2_signature sig;
    memcpy(sig.r, signature, 32);
    memcpy(sig.s, signature + 32, 32);
    return sm2_verify(public_key, hash, hash_len, &sig);
}

注意:SM2签名验签的输入哈希长度必须是32字节(256位)。SM3的输出正好是32字节,完美匹配。如果你用其他哈希算法,记得做截断或填充。

4.3 度量根(RTM)设计

度量根,Root of Trust for Measurement,是整个信任链的起点。它必须是不可篡改的。我见过一些设计把RTM放在BootROM里,这是最安全的做法——BootROM是只读的,物理上改不了。

RTM的职责很简单:

  • 上电后第一个执行。
  • 度量下一级(通常是Bootloader)的完整性。
  • 把度量结果存到平台配置寄存器(PCR)里。
  • 如果度量失败,直接停机或进入恢复模式。

为什么会强调PCR?因为PCR的值只能扩展,不能重置(除非硬件复位)。这样就能保证整个度量链的完整性——你没法跳过某一级去伪造度量结果。

4.3.1 RTM的两种实现方式

实现方式 优点 缺点 适用场景
硬件RTM(BootROM) 不可篡改,安全性最高 灵活性差,升级困难 高安全等级设备(金融、军工)
软件RTM(安全启动代码) 可升级,灵活性好 依赖硬件保护(如OTP) 消费类IoT设备

我个人更倾向于硬件RTM。虽然升级麻烦,但安全收益是实打实的。有一次,一个客户坚持用软件RTM,结果攻击者通过电压故障注入跳过了RTM的度量代码,直接加载了恶意Bootloader。嗯,从那以后,我再也不敢在关键设备上推荐纯软件RTM了。

4.4 完整的度量链流程

咱们把整个流程串起来,看看一个完整的度量链长什么样。下面这张SVG图展示了从RTM到应用层的四级度量链。

固件完整性度量链流程图 固件完整性度量链 RTM (BootROM) 度量根,不可篡改 Bootloader 度量OS Kernel OS Kernel 度量文件系统 文件系统/应用 运行时完整性校验 度量方式 1. SM3计算哈希 2. SM2签名哈希 3. 存储到PCR 失败处理 • 停机/死循环 • 进入恢复模式 • 记录安全事件

从这张图你能看到,每一级只度量下一级,信任链是单向的。RTM是起点,它度量Bootloader;Bootloader度量OS Kernel;OS Kernel度量文件系统。任何一级度量失败,整个启动过程就终止。

4.5 避坑指南

我在项目里踩过不少坑,挑几个典型的说说:

  • 哈希链断裂:有一次,Bootloader升级后忘了更新签名,结果下一级验签失败,设备变砖。后来我加了个机制:如果验签失败,回退到上一个已知良好的版本。
  • PCR值被篡改:软件实现的PCR容易被攻击。我建议用硬件TPM或安全元件来存储PCR值,至少也要用OTP(一次性可编程)存储器。
  • 度量顺序错误:有些设计先加载镜像再度量,这等于先执行后检查,毫无意义。正确的顺序是:先度量,再加载,最后执行。

曾经有一次:我在一个项目里发现,攻击者通过DMA直接读取了Flash中的固件镜像,绕过了度量链。解决方案是在RTM阶段就把DMA控制器锁住,直到度量完成才释放。这个坑让我意识到,度量链不仅要防篡改,还要防旁路。

4.6 小结

固件完整性度量不是可选项,而是必选项。哈希链保证了每一级都经过验证,SM3+SM2的组合提供了国密标准的密码学保障,RTM则是整个信任链的基石。设计时记住一句话:信任不能传递,只能度量。下一级是否可信,取决于上一级是否正确地度量了它。

嗯,这一章就到这里。内容不少,但都是实战中必须掌握的。如果你在实现中遇到问题,欢迎随时交流。