4. 签名与校验机制

各位工程师,咱们接着聊。上一章我们把烧录流程理清了,但光有流程还不够——你怎么保证烧进去的固件是“干净”的?没有被篡改过?版本是不是最新的?

这就是本章要啃的硬骨头:签名、校验、防回滚。这三板斧砍下去,攻击者想塞个假固件进来,基本没戏。

4.1 哈希校验:先给固件打个“指纹”

先说最基础的——哈希校验。说白了,就是给固件算一个唯一的摘要。哪怕你只改了一个 bit,算出来的哈希值都会天差地别。

我个人习惯用 SHA256。为什么?因为 MD5 和 SHA1 已经被证明有碰撞风险了,虽然实际攻击门槛很高,但做产品嘛,安全标准得往前看。

核心原则:哈希校验只能检测“是否被篡改”,但不能证明“是谁签的”。要解决身份认证问题,必须上签名。

实际项目中,我一般这样用:

  1. 编译完固件,立刻算 SHA256,得到一个 32 字节的哈希值。
  2. 把这个哈希值烧到芯片的 OTP(一次性可编程)区域,或者存到安全存储区。
  3. 启动时,Bootloader 重新计算当前固件的 SHA256,跟存储的值比对。

代码示例很简单,但要注意实现细节:

// 伪代码:Bootloader 中的哈希校验
uint8_t expected_hash[32];
uint8_t computed_hash[32];

// 从安全存储区读取预期哈希
read_secure_storage(EXPECTED_HASH_ADDR, expected_hash, 32);

// 计算当前固件的 SHA256
sha256_start();
sha256_update(firmware_addr, firmware_size);
sha256_finish(computed_hash);

// 比对
if (memcmp(expected_hash, computed_hash, 32) != 0) {
    // 校验失败,拒绝启动
    enter_error_mode();
}
小技巧:我建议把哈希校验放在 Bootloader 的最早期阶段,越早越好。因为一旦跳到固件主程序,攻击者可能已经 hook 了校验函数。

4.2 代码签名:RSA 与 ECDSA 的选择

哈希校验只能防“无意的损坏”,但防不住“恶意的篡改”。为什么?因为攻击者可以同时替换固件和哈希值。这时候就需要非对称签名了。

签名机制的核心逻辑:

  • 私钥签名:在开发环境(PC端)用私钥对固件哈希签名。
  • 公钥验签:在设备端用公钥验证签名是否匹配。

公钥是公开的,但私钥必须死死守住。我在项目中见过最惨的案例——有人把私钥直接放到了 Git 仓库里……嗯,那基本等于把家门钥匙挂在门外了。

4.2.1 RSA vs ECDSA:怎么选?

这两个算法各有千秋,我列个表方便你对比:

对比项 RSA (2048位) ECDSA (P-256)
签名长度 256 字节 64 字节
验签速度 较慢(模幂运算) 较快(椭圆曲线点乘)
密钥生成 慢 快
硬件加速 很多 MCU 内置 较新 MCU 才支持
安全性 成熟可靠 同等强度下密钥更短

我个人建议:如果你的 MCU 有硬件 RSA 加速器,用 RSA 2048 就够用了。如果追求签名速度或者存储空间紧张,选 ECDSA P-256。我在一个智能门锁项目里用过 ECDSA,验签一次只要 30ms,比 RSA 快了一倍多。

避坑指南:我曾经遇到过一个坑——ECDSA 的随机数生成器不够随机,导致签名泄露了私钥信息。后来我强制要求每次签名前都做一次熵源检查,才彻底解决。

4.3 签名验证流程:从编译到启动

完整的签名验证流程,我画了一张图,你一看就明白:

签名验证全流程 签名验证全流程 开发环境(PC端) ① 编译固件 → 得到 binary ② 计算 SHA256 哈希 ③ 私钥签名 → 生成签名数据 ④ 打包:固件 + 签名 + 元数据 烧录/OTA 设备端(MCU) ⑤ 接收固件包 ⑥ 提取签名,用公钥解密得哈希A ⑦ 计算固件 SHA256 得哈希B ⑧ 比对哈希A == 哈希B? 不匹配 → 拒绝启动 匹配 → 正常启动 关键点:私钥永远不出开发环境,设备端只存公钥

你看,整个流程其实不复杂。核心就一句话:私钥签名,公钥验签,哈希做桥梁。

4.4 防回滚机制:为什么需要它?

签名校验保证了固件是“正品”,但没保证是“最新版”。攻击者可以拿一个旧版本的合法固件(比如有已知漏洞的 v1.0)刷进去,然后利用旧版本的漏洞搞事情。

这就是回滚攻击。我见过一个真实案例:某 IoT 设备的老版本固件里有个缓冲区溢出漏洞,攻击者拿到旧固件签名后,强行降级,然后远程执行了恶意代码。

防回滚的常见做法:

  • 版本号寄存器:在 OTP 或安全存储区维护一个单调递增的版本号。
  • 版本号比对:烧录时检查新固件的版本号必须 ≥ 当前版本号。
  • 熔丝/efuse:一旦烧录了某个版本,物理熔断,永远无法回到更低版本。
关键设计:版本号必须和签名绑定在一起。也就是说,签名数据里要包含版本号字段,防止攻击者把高版本的签名挪到低版本固件上。

4.5 综合策略:把三件事串起来

在实际项目中,我通常这样设计安全启动链:

  1. BootROM(只读):验证 Bootloader 的签名和版本号。
  2. Bootloader:验证主固件的签名、哈希、版本号。
  3. 主固件:运行时可以验证应用分区的签名(比如 OTA 升级包)。

每一级都做签名+版本检查,形成一条信任链。攻击者只要攻破任何一级,整个链条就断了——但每一级都很难攻破。

我的习惯:我会在 Bootloader 里加一个“降级计数器”。如果连续 3 次签名校验失败,就永久锁死 OTA 功能,必须通过物理接口(比如 UART)才能恢复。这能有效防止暴力破解。

4.6 避坑总结

最后,我把这些年踩过的坑总结一下:

坑 后果 解决方案
私钥泄露 攻击者可签名任意固件 使用 HSM(硬件安全模块)管理私钥
随机数不够随机 ECDSA 私钥泄露 每次签名前做熵源检查
版本号未绑定签名 回滚攻击 版本号放在签名数据内
公钥存储在可写区域 攻击者可替换公钥 公钥烧入 OTP 或 efuse
校验时机太晚 攻击者绕过校验 在 BootROM 阶段完成校验

嗯,这一章的内容就到这儿。签名校验和防回滚,说白了就是“防伪+防旧”。把这两件事做好了,你的设备安全底线就立住了。