4、固件签名与验证:数字签名原理(RSA/ECC)、哈希算法(SHA-256)、固件签名流程、验签流程
各位工程师,欢迎来到第四章。
上一章我们聊了安全启动的信任链,从 BootROM 一级级往上校验。但有个核心问题没解决:BootROM 凭什么相信下一级固件是合法的?
答案就是——数字签名。
说白了,签名就是给固件盖一个「防伪章」。这个章只有私钥能盖,但任何人都能用公钥来验。我在项目里见过太多人把签名和加密搞混——签名不是为了保密,而是为了防篡改和防伪造。你想想看,固件在 Flash 里躺着,万一被黑客改了一字节,启动时能不能发现?签名就是干这个的。
核心结论:没有签名的固件,就像没有锁的门。安全启动的第一步,就是让 BootROM 学会「认章不认人」。
4.1 哈希算法:固件的「指纹」
签名之前,得先给固件算个「摘要」。这个摘要就是哈希值。
哈希算法有很多种,但嵌入式安全领域最常用的是 SHA-256。为什么是它?
- 不可逆:从哈希值推不出原文,这是安全基础。
- 抗碰撞:几乎不可能找到两个不同固件有相同哈希值。
- 固定长度:不管固件多大,SHA-256 输出永远是 256 位(32 字节)。
我个人习惯在项目初期就把哈希算法定死。有一次,团队里有人想用 MD5 省点 Flash 空间,被我拦住了——MD5 的碰撞攻击在 2004 年就被公开了,用在安全启动上等于裸奔。
经验之谈:SHA-256 在主流 MCU 上都有硬件加速器,算 1MB 固件也就几十毫秒。别为了省那点时间用弱哈希,不值得。
4.2 非对称签名算法:RSA vs ECC
哈希只能保证完整性,不能保证真实性。要证明「这个固件确实是官方发布的」,就得靠非对称签名。
目前主流方案就两个:RSA 和 ECC(椭圆曲线)。
| 对比项 | RSA(2048位) | ECC(256位) |
|---|---|---|
| 安全等级 | 112 位 | 128 位 |
| 签名长度 | 256 字节 | 64 字节 |
| 验签速度 | 较慢(模幂运算) | 较快(点乘运算) |
| 公钥大小 | 256 字节 | 32 字节 |
| 硬件支持 | 非常广泛 | 逐渐普及 |
怎么选?我一般这么看:
- 选 RSA:如果你的 MCU 只有 RSA 硬件加速器,或者团队对 RSA 更熟悉。2048 位是底线,1024 位已经不安全了。
- 选 ECC:如果 Flash 空间紧张、验签速度要求高、或者 MCU 支持 ECC 硬件加速。256 位 ECC 的安全强度跟 3072 位 RSA 差不多,但签名短得多。
注意:ECC 的曲线参数选择很关键。别自己造曲线,直接用 NIST P-256 或 secp256r1。我在一个开源项目里见过有人用自定义曲线,结果验签逻辑有侧信道漏洞——嗯,后来整个项目重做了。
4.3 固件签名流程(PC 端/构建服务器)
签名是在固件发布前做的,通常在 CI/CD 流水线或开发机上完成。流程如下:
- 计算固件哈希:对固件二进制文件做 SHA-256,得到 32 字节摘要。
- 签名哈希:用私钥对哈希值做签名运算,生成签名数据(RSA 是 256 字节,ECC 是 64 字节)。
- 打包固件:将原始固件 + 签名数据 + 公钥(或公钥索引)打包成一个固件包。
代码示例(伪代码,用 OpenSSL 命令行演示):
# 1. 计算固件哈希
openssl dgst -sha256 -binary firmware.bin > firmware.hash
# 2. 用私钥签名哈希(RSA 示例)
openssl pkeyutl -sign \
-inkey private_key.pem \
-in firmware.hash \
-out firmware.sig
# 3. 打包(简单拼接)
cat firmware.bin firmware.sig > firmware_signed.bin
建议:签名用的私钥一定要放在离线机器或 HSM(硬件安全模块)里。我见过有人把私钥放在 Git 仓库里——那跟把家门钥匙贴在门上没区别。
4.4 固件验签流程(MCU 端/BootROM)
验签是在 MCU 启动时做的,由 BootROM 或第一级 bootloader 执行。流程是签名的逆过程:
- 提取固件和签名:从 Flash 中读取固件包,分离出原始固件和签名数据。
- 计算固件哈希:对原始固件做 SHA-256,得到 32 字节摘要。
- 验签:用公钥对「摘要 + 签名」做验签运算。如果验签通过,说明固件未被篡改且来自合法发布者。
代码示例(C 语言,使用 mbedTLS 库):
#include <mbedtls/sha256.h>
#include <mbedtls/pk.h>
int verify_firmware(const uint8_t *fw, size_t fw_len,
const uint8_t *sig, size_t sig_len,
const uint8_t *pub_key, size_t pub_key_len)
{
mbedtls_pk_context pk;
uint8_t hash[32];
int ret;
// 1. 初始化公钥上下文
mbedtls_pk_init(&pk);
ret = mbedtls_pk_parse_public_key(&pk, pub_key, pub_key_len);
if (ret != 0) return -1;
// 2. 计算固件哈希
mbedtls_sha256(fw, fw_len, hash, 0);
// 3. 验签
ret = mbedtls_pk_verify(&pk, MBEDTLS_MD_SHA256,
hash, 0, sig, sig_len);
mbedtls_pk_free(&pk);
return ret; // 0 表示验签通过
}
关键点:公钥必须安全存储。通常的做法是把公钥熔丝在 OTP(一次性可编程)区域,或者写在 BootROM 里。如果公钥能被篡改,那签名就形同虚设。
4.5 整体流程 SVG 图
下面这张图把签名和验签的完整流程串起来了。左边是 PC 端签名,右边是 MCU 端验签,中间是固件包传输。
4.6 避坑指南
做固件签名验证,有几个坑我踩过,分享给你:
- 哈希计算范围:签名时算的是整个固件的哈希,还是去掉签名部分?我建议算整个固件(包括头部),然后把签名附加在尾部。这样验签时先读头部知道固件长度,再算哈希,逻辑清晰。
- 公钥存储位置:别把公钥放在固件里。公钥应该烧在 OTP 或写在 BootROM 代码中。有一次我看到一个方案把公钥放在固件头部——那等于自己拆了自己的锁。
- 验签失败处理:验签失败后,MCU 应该进入错误状态,不能回退到未签名固件。我见过一个设计,验签失败后尝试从备份区启动——结果备份区也是被篡改的固件,等于没验。
- 侧信道攻击:如果攻击者能测量验签时间或功耗,可能推断出私钥信息。建议使用常数时间比较函数(如
mbedtls_safer_memcmp)来比较哈希值。
总结一下:固件签名验证是安全启动的基石。哈希保证完整性,签名保证真实性,公钥保证信任根。这三者缺一不可。
嗯,这一章的内容就到这里。记住:签名不是加密,是防伪。你的 BootROM 就是那个「验章员」,它只认正确的签名,其他的一概不认。