4. 固件签名与加密:固件完整性校验、数字签名原理、安全启动链

各位做 MCU 开发的同行,今天我们来聊聊固件安全。说实话,早些年我做 Bootloader 的时候,对安全这块儿真没太当回事。直到有一次,一个客户的产品在产线上被刷入了篡改过的固件,整批货差点报废。嗯,从那以后,我再也不敢轻视固件校验和签名了。

这一章,我会把固件完整性校验、数字签名、安全启动链这几个核心概念串起来讲。你想想看,一个不安全的 Bootloader,就像你家大门没上锁——谁都能进。

4.1 固件完整性校验:CRC 与 SHA

先说最简单的。完整性校验,说白了就是检查固件有没有被改过。我习惯把它分成两类:CRC 和 哈希。

4.1.1 CRC:快速但脆弱

CRC(循环冗余校验)在嵌入式里太常见了。它的优点是快,硬件支持好。但缺点也很明显——CRC 不是密码学安全的。什么意思?就是攻击者可以轻易构造出另一个固件,CRC 值却一模一样。

我的建议:CRC 只适合做传输校验,不适合做安全校验。我在一个 OTA 项目里就吃过这个亏,后来全部改成了 SHA256。

常见的 CRC 算法对比:

算法 位宽 用途 安全性
CRC-8 8 bit 小数据包校验 极低
CRC-16 16 bit Modbus、存储校验 低
CRC-32 32 bit 文件完整性、以太网 低
CRC-64 64 bit 大文件校验 较低

4.1.2 SHA:真正的完整性保证

SHA(安全哈希算法)才是正经的完整性校验。它的核心特性是单向性和抗碰撞性。你从固件算出哈希值,但没法从哈希值反推出固件。

我个人最常用的是 SHA256。为什么?因为 SHA1 已经被证明有碰撞风险了,而 SHA256 在 MCU 上跑起来也不算太慢。如果你用的是 Cortex-M4 以上内核,很多芯片都有硬件 SHA 加速器。

经验之谈:在 Bootloader 里做 SHA256 校验时,建议用 分块计算 的方式。别一次性把整个固件加载到 RAM 里——很多 MCU 的 RAM 就那么几十 KB,根本装不下几百 KB 的固件。

分块计算的伪代码示例:

// SHA256 分块计算示例
SHA256_CTX ctx;
sha256_init(&ctx);

uint32_t offset = 0;
uint8_t buffer[1024]; // 每次读 1KB

while (offset < firmware_size) {
    uint32_t chunk = min(1024, firmware_size - offset);
    read_flash(firmware_addr + offset, buffer, chunk);
    sha256_update(&ctx, buffer, chunk);
    offset += chunk;
}

uint8_t hash[32];
sha256_final(&ctx, hash);

// 比较 hash 和预存值
if (memcmp(hash, expected_hash, 32) == 0) {
    // 固件完整
} else {
    // 固件被篡改
}

4.2 数字签名原理:RSA 与 ECDSA

完整性校验能防意外篡改,但防不了恶意篡改。为什么?因为哈希值本身也可能被改掉。这时候就需要数字签名了。

数字签名的核心思想是非对称加密:私钥签名,公钥验签。私钥只有你(开发者)有,公钥可以公开放在芯片里。

4.2.1 RSA:经典但沉重

RSA 是最早的公钥密码体系之一。它的安全性基于大整数分解的难度。在 MCU 上,RSA 验签的典型参数是 2048 位密钥。

说实话,RSA 在 MCU 上跑起来挺吃力的。我记得在一个 STM32F4 项目里,RSA 2048 验签一次要 200 多毫秒。用户开机要等这么久,体验很差。

算法 密钥长度 签名长度 验签速度(Cortex-M4 @168MHz)
RSA 2048 bit 256 字节 ~200 ms
RSA 4096 bit 512 字节 ~800 ms
ECDSA 256 bit (P-256) 64 字节 ~30 ms
ECDSA 384 bit (P-384) 96 字节 ~60 ms

4.2.2 ECDSA:轻量级选手

ECDSA(椭圆曲线数字签名算法)是我现在的首选。同样的安全强度下,ECDSA 的密钥更短、签名更小、计算更快。

举个例子:ECDSA P-256 的安全性和 RSA 3072 相当,但签名只有 64 字节,验签速度是 RSA 的 5-10 倍。

注意:ECDSA 的随机数生成非常关键。如果随机数重复,私钥会直接泄露。我曾经见过一个产品因为随机数生成器没初始化好,导致所有签名都能被伪造——那场面,相当尴尬。

4.3 安全启动链:信任的传递

安全启动链,说白了就是一级验证一级。从芯片上电开始,每一级代码都要验证下一级代码的签名,直到应用固件被验证通过。

典型的四层结构:

  1. ROM Boot:芯片出厂固化的只读代码,不可更改。验证 Flash 中的 Bootloader 签名。
  2. Bootloader Stage 1:第一级 Bootloader,通常很小。验证 Stage 2 的签名。
  3. Bootloader Stage 2:功能完整的 Bootloader,支持 OTA、恢复等。验证应用固件的签名。
  4. Application:用户应用固件。运行前必须通过签名验证。

下面这张图展示了安全启动链的完整流程:

安全启动链流程图 ROM Boot 验证 Bootloader Stage 1 签名 Bootloader Stage 1 验证 Bootloader Stage 2 签名 Bootloader Stage 2 验证 Application 签名

3.3.1 信任根

信任根(Root of Trust)是整个安全链的基石。它通常是芯片 ROM 里的一段代码,由芯片厂商固化,不可更改。信任根里存着公钥的哈希值,用来验证第一级 Bootloader 的签名。

关键点:信任根必须是 不可变的。如果攻击者能改信任根,整个安全链就崩塌了。所以,选芯片时一定要确认 ROM 是否支持写保护或一次性编程(OTP)。

3.3.2 密钥管理

密钥管理是安全启动里最容易出问题的地方。我见过太多项目把私钥直接放在代码仓库里——那跟把密码贴在门上有什么区别?

我的建议:

  • 私钥:放在离线机器上,用硬件安全模块(HSM)或加密 U 盘存储。永远不要联网。
  • 公钥:编译进 Bootloader,但建议存它的哈希值,防止被替换。
  • 密钥轮换:产品生命周期内,最好支持密钥更新。万一私钥泄露,还能补救。

4.4 实战:在 Bootloader 中集成签名验证

好了,理论讲完了,咱们来点实际的。下面是我在一个 STM32H7 项目里用的签名验证流程。

4.4.1 固件打包流程(PC 端)

  1. 编译生成原始固件 firmware.bin
  2. 计算 SHA256 哈希:sha256sum firmware.bin
  3. 用私钥对哈希签名:openssl dgst -sha256 -sign private.pem -out signature.bin firmware.bin
  4. 将签名附加到固件尾部:cat firmware.bin signature.bin > firmware_signed.bin

4.4.2 Bootloader 验签流程(MCU 端)

// Bootloader 验签核心逻辑
int verify_firmware(uint32_t addr, uint32_t size) {
    // 1. 读取固件末尾的签名
    uint8_t signature[64]; // ECDSA P-256 签名长度
    read_flash(addr + size - 64, signature, 64);

    // 2. 计算固件哈希(不含签名部分)
    uint8_t hash[32];
    compute_sha256(addr, size - 64, hash);

    // 3. 用公钥验签
    if (ecdsa_verify(public_key, hash, signature) == 0) {
        return 0; // 验证通过
    } else {
        return -1; // 验证失败
    }
}

避坑指南:我曾经在验签时忘了排除签名本身,结果哈希一直算不对。折腾了两天才发现——固件哈希把签名自己也算进去了。记住:签名不能参与哈希计算。

4.5 安全启动的常见陷阱

最后,我总结几个实际项目中容易踩的坑:

  • 调试接口没关:SWD/JTAG 没锁住,攻击者可以直接读 Flash、改代码。记得在产品量产前烧录选项字节,禁用调试接口。
  • 回滚攻击:旧版本固件可能有漏洞。Bootloader 要检查版本号,禁止刷入比当前版本更旧的固件。
  • 公钥硬编码被替换:如果公钥存在可写的 Flash 区域,攻击者可以替换成自己的公钥。解决方案:把公钥哈希存在 OTP 区域,每次验签前先验证公钥本身。
  • 随机数质量差:ECDSA 签名需要高质量随机数。MCU 内置的 RNG 有时候不够随机,建议加一个熵池或外部 TRNG。

重要提醒:安全启动不是万能的。它只能保证固件在启动时是完整的、未被篡改的。如果攻击者能在运行时注入代码,安全启动也拦不住。所以,安全启动 + 运行时保护 才是完整方案。

嗯,这一章的内容就到这里。固件签名和安全启动链,说白了就是建立一条信任链——从芯片上电的第一条指令开始,每一级都验证下一级。只要信任根是安全的,整条链就是安全的。下次你设计 Bootloader 的时候,记得把这些机制加进去。别像我当年那样,等出了事才后悔。