哈希算法在签名链中的应用

哈希算法,说白了就是给数据算一个「指纹」。这个指纹长度固定,而且只要数据变一点点,指纹就完全不一样。我在固件签名链里折腾了这么多年,可以负责任地告诉你——哈希是整个信任链的基石。没有它,签名、校验、链式验证全都不成立。

SHA-256 算法原理

SHA-256 是 SHA-2 家族里最常用的成员。它把任意长度的输入,压缩成 256 位(32 字节)的摘要。我习惯把它理解成一个「单向黑盒」:你塞进去一个固件镜像,出来一串看起来随机的十六进制数。但这个过程其实有严格的数学步骤。

简单说,SHA-256 做了这几件事:

  1. 填充:在消息末尾补一个 '1' 位,然后补 '0',直到长度模 512 等于 448。最后附加 64 位的原始长度信息。
  2. 分块:把填充后的消息切成 512 位(64 字节)的块。
  3. 初始化:设置 8 个 32 位的初始哈希值(H0~H7),这些值是前 8 个质数的平方根的小数部分取前 32 位。
  4. 压缩循环:每个 512 位块经过 64 轮迭代运算,更新 8 个状态寄存器。
  5. 输出:最后把 8 个 32 位寄存器拼接成 256 位的摘要。

关键点:SHA-256 的 64 轮压缩函数里用到了 6 个逻辑函数(Ch、Maj、Σ0、Σ1、σ0、σ1),以及 64 个固定的常数 K。这些常数来自前 64 个质数的立方根的小数部分。设计得相当精巧,抗碰撞性很强。

我在项目中遇到过一件事:有个同事想自己「优化」SHA-256,把 64 轮减到 32 轮,觉得「够用就行」。结果我们用 HashClash 工具跑了一晚上,就找到了碰撞。嗯,从那以后我再也不敢动标准算法的轮数了。

哈希链构建

签名链里的哈希链,不是简单地把多个哈希值串起来。它更像一个「嵌套」结构。我举个例子你就明白了:

假设我们有三级固件:BootROM → Bootloader → 应用固件。哈希链是这样构建的:

  • BootROM 里硬编码了 Bootloader 的哈希值 H(BL)。
  • Bootloader 里包含了应用固件的哈希值 H(App)。
  • 启动时,BootROM 先计算加载进来的 Bootloader 的哈希,跟硬编码的 H(BL) 比对。一致后,Bootloader 再计算应用固件的哈希,跟它内部存储的 H(App) 比对。

你看,每一级都「信任」下一级的哈希值。这个信任传递就是哈希链的核心。

我的习惯:在实际项目中,我通常会在哈希链的每个节点里同时存储「当前固件的哈希」和「下一级固件的哈希」。这样即使某一级被篡改,也能在上一级被检测出来,不会让攻击者跳过校验。

为什么会这样设计?因为攻击者如果只篡改 Bootloader,但没改 BootROM 里的哈希值,那 BootROM 一算就能发现不对。如果攻击者同时篡改 Bootloader 和 BootROM 里的哈希值……嗯,那 BootROM 本身是只读的,改不了。这就是硬件信任根的威力。

哈希值校验流程

校验流程其实不复杂,但细节里全是坑。我总结了一个标准流程:

  1. 读取参考哈希:从当前固件中提取出预先存储的哈希值(比如从签名证书或固件头里读)。
  2. 计算实际哈希:对下一级固件的完整镜像做 SHA-256 计算。
  3. 比对:两个哈希值逐字节比较。
  4. 决策:一致则继续启动,不一致则进入错误处理(比如进入恢复模式或直接锁死)。

我曾经踩过的坑:有一次,固件头里存的哈希是小端序,但我计算出来的哈希是大端序。直接 memcmp 比对,死活不通过。排查了整整两天才发现是字节序问题。所以,我建议你在设计哈希存储格式时,统一用大端序(网络字节序),并且在代码里显式做转换,不要依赖编译器行为。

另外,校验时要注意「时间恒定比较」。普通 memcmp 在发现第一个不匹配的字节时就返回了,攻击者可以利用这个时间差做侧信道攻击。我习惯自己写一个恒定时间的比较函数:

int constant_time_memcmp(const void *a, const void *b, size_t len) {
    const uint8_t *pa = (const uint8_t *)a;
    const uint8_t *pb = (const uint8_t *)b;
    uint8_t result = 0;
    for (size_t i = 0; i < len; i++) {
        result |= pa[i] ^ pb[i];
    }
    return result; // 0 表示相等
}

你想想看,如果攻击者能通过测量校验时间来推断哈希值,那签名链的安全性就大打折扣了。这个函数虽然慢一点点,但值得。

常见哈希算法对比

市面上哈希算法不少,但真正适合固件签名链的其实没几个。我整理了一张对比表:

算法 摘要长度 安全性 速度(软件) 硬件加速 推荐场景
SHA-1 160 位 已破解(SHAttered) 快 常见 ❌ 不推荐
SHA-256 256 位 安全(2025 年) 中等 常见 ✅ 通用首选
SHA-384 384 位 安全 较慢 部分支持 高安全场景
SHA-512 512 位 安全 64 位 CPU 上快 部分支持 服务器端
SHA-3-256 256 位 安全(新标准) 较慢 较少 未来兼容
BLAKE2s 可变(常用 256) 安全 极快 较少 性能敏感场景

我个人习惯在嵌入式固件签名链里首选 SHA-256。原因有三:

  • 安全性足够:256 位的抗碰撞强度,目前没有实际可行的攻击。
  • 硬件支持好:大多数 MCU 和 SoC 都有 SHA-256 硬件加速引擎,算起来很快。
  • 生态成熟:各种工具链、库、标准都优先支持 SHA-256。

一个小建议:如果你的芯片支持 SHA-512 硬件加速,而且你的签名链对安全性有更高要求(比如车规级或金融级),可以考虑用 SHA-512。但要注意,摘要长度翻倍意味着存储和传输开销也翻倍。在资源受限的嵌入式环境里,这个代价有时候承受不起。

至于 SHA-1,我建议你直接放弃。2017 年 Google 和 CWI 研究所已经展示了实际的碰撞攻击(SHAttered 攻击)。虽然生成碰撞需要大量计算资源,但对于攻击者来说,只要成本低于攻击收益,就一定会有人干。我在 2019 年参与的一个项目里,客户坚持用 SHA-1,结果安全审计直接给了个 Critical 漏洞。后来全部迁移到 SHA-256,多花了一个月工期。嗯,早该换的。

下面这张图展示了哈希链在三级固件签名链中的核心位置:

哈希链在固件签名链中的核心位置 BootROM 存储 H(Bootloader) Bootloader 存储 H(App Firmware) 应用固件 实际运行代码 校验 SHA-256 校验 SHA-256 哈希链构建逻辑 BootROM 硬编码 H(BL) → 启动时计算实际 H(BL') 并比对 Bootloader 硬编码 H(App) → 加载时计算实际 H(App') 并比对 攻击者视角:为什么哈希链有效? 篡改 Bootloader → BootROM 校验 H(BL) 失败 → 启动终止 篡改应用固件 → Bootloader 校验 H(App) 失败 → 加载终止

从这张图可以看得很清楚:哈希链的本质就是「每一级都锁死下一级的身份」。攻击者想绕过校验,除非他能同时篡改硬件只读区的 BootROM——这在物理上几乎不可能。所以,哈希链配合硬件信任根,是目前最可靠的固件安全方案之一。