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 安全启动链:信任的传递
安全启动链,说白了就是一级验证一级。从芯片上电开始,每一级代码都要验证下一级代码的签名,直到应用固件被验证通过。
典型的四层结构:
- ROM Boot:芯片出厂固化的只读代码,不可更改。验证 Flash 中的 Bootloader 签名。
- Bootloader Stage 1:第一级 Bootloader,通常很小。验证 Stage 2 的签名。
- Bootloader Stage 2:功能完整的 Bootloader,支持 OTA、恢复等。验证应用固件的签名。
- 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 端)
- 编译生成原始固件
firmware.bin - 计算 SHA256 哈希:
sha256sum firmware.bin - 用私钥对哈希签名:
openssl dgst -sha256 -sign private.pem -out signature.bin firmware.bin - 将签名附加到固件尾部:
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 的时候,记得把这些机制加进去。别像我当年那样,等出了事才后悔。