哈希算法在签名链中的应用
哈希算法,说白了就是给数据算一个「指纹」。这个指纹长度固定,而且只要数据变一点点,指纹就完全不一样。我在固件签名链里折腾了这么多年,可以负责任地告诉你——哈希是整个信任链的基石。没有它,签名、校验、链式验证全都不成立。
SHA-256 算法原理
SHA-256 是 SHA-2 家族里最常用的成员。它把任意长度的输入,压缩成 256 位(32 字节)的摘要。我习惯把它理解成一个「单向黑盒」:你塞进去一个固件镜像,出来一串看起来随机的十六进制数。但这个过程其实有严格的数学步骤。
简单说,SHA-256 做了这几件事:
- 填充:在消息末尾补一个 '1' 位,然后补 '0',直到长度模 512 等于 448。最后附加 64 位的原始长度信息。
- 分块:把填充后的消息切成 512 位(64 字节)的块。
- 初始化:设置 8 个 32 位的初始哈希值(H0~H7),这些值是前 8 个质数的平方根的小数部分取前 32 位。
- 压缩循环:每个 512 位块经过 64 轮迭代运算,更新 8 个状态寄存器。
- 输出:最后把 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 本身是只读的,改不了。这就是硬件信任根的威力。
哈希值校验流程
校验流程其实不复杂,但细节里全是坑。我总结了一个标准流程:
- 读取参考哈希:从当前固件中提取出预先存储的哈希值(比如从签名证书或固件头里读)。
- 计算实际哈希:对下一级固件的完整镜像做 SHA-256 计算。
- 比对:两个哈希值逐字节比较。
- 决策:一致则继续启动,不一致则进入错误处理(比如进入恢复模式或直接锁死)。
我曾经踩过的坑:有一次,固件头里存的哈希是小端序,但我计算出来的哈希是大端序。直接 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——这在物理上几乎不可能。所以,哈希链配合硬件信任根,是目前最可靠的固件安全方案之一。