4、固件签名与校验:哈希算法选择(SHA256 vs SHA512)、非对称签名(RSA/ECDSA)、签名流程与验签流程
固件签名这事儿,说白了就是给固件贴个「防伪标签」。你想想看,Bootloader 从 Flash 里把固件读出来,如果没做校验,那黑客改一个字节,设备就可能变砖,甚至被植入后门。我早年做 IoT 网关时,就吃过这个亏——产品出货后,有人通过串口直接刷了篡改过的固件,导致设备成了矿机肉鸡。从那以后,我对签名校验再也不敢马虎。
4.1 哈希算法:SHA256 还是 SHA512?
签名之前,先要对固件算一个「摘要」,也就是哈希值。哈希算法选哪个,直接影响安全性和性能。
核心原则:哈希不是加密,它是单向的。你没法从哈希值反推出原始数据。所以哈希算法的强度,取决于它抗碰撞的能力——也就是找到两个不同输入、但哈希值相同的难度。
4.1.1 SHA256 与 SHA512 的对比
| 对比项 | SHA256 | SHA512 |
|---|---|---|
| 输出长度 | 256 bit(32 字节) | 512 bit(64 字节) |
| 安全强度 | 128 bit(抗碰撞) | 256 bit(抗碰撞) |
| 计算速度(32位MCU) | 较快(约 80-120 MB/s) | 较慢(约 40-60 MB/s) |
| 计算速度(64位CPU) | 中等 | 更快(利用 64 位寄存器) |
| 内存占用 | 低(约 256 字节上下文) | 高(约 512 字节上下文) |
| 适用场景 | 嵌入式 MCU、资源受限设备 | 高性能处理器、安全等级要求极高 |
我个人习惯,在 Cortex-M4 或更低端的 MCU 上,优先选 SHA256。为什么?因为 32 位 MCU 处理 64 位数据块时,需要额外的移位和拼接操作,反而比 SHA256 慢。我在一个 STM32F4 项目上实测过,SHA512 比 SHA256 慢了将近 40%,而安全强度对嵌入式场景来说,SHA256 的 128 位抗碰撞能力已经绰绰有余。
我的建议:如果固件大小超过 1MB,且 MCU 有硬件哈希加速器(比如 NXP i.MX RT 系列),那 SHA256 和 SHA512 的速度差距可以忽略。这时候选 SHA512 更安心,毕竟多一层保护没坏处。
4.1.2 哈希在签名中的作用
签名算法(RSA/ECDSA)处理不了大块数据,它们只能对固定长度的哈希值签名。所以流程是:
- 对固件算哈希(比如 SHA256)
- 对哈希值做签名
- 把签名和固件一起打包
验签时,Bootloader 先算固件哈希,再用公钥验证签名。如果签名匹配,说明固件没被改过,而且确实来自持有私钥的合法发布者。
4.2 非对称签名:RSA vs ECDSA
非对称签名,就是一对密钥:私钥签名,公钥验签。私钥你藏好,公钥烧在 Bootloader 里。这样即使固件被截获,别人没有私钥也伪造不了签名。
4.2.1 RSA 签名
RSA 是老牌选手,原理基于大整数分解。签名时用私钥的指数 d 和模数 n 对哈希值做模幂运算,验签时用公钥的指数 e 和模数 n 做同样的运算,比对结果。
- 优点:算法成熟,几乎所有加密库都支持。公钥可以很小(比如 e=65537)。
- 缺点:签名和验签都很慢,尤其是签名。密钥越长,速度越慢。2048 位 RSA 在 200MHz MCU 上签名一次可能要几百毫秒。
- 签名长度:等于密钥长度。2048 位 RSA 签名就是 256 字节。
避坑指南:我曾经在一个项目里用了 4096 位 RSA,结果 Bootloader 验签花了将近 2 秒,用户升级时以为死机了。后来我改成 2048 位,验签时间降到 300ms 以内,用户体验好很多。记住:嵌入式场景,2048 位 RSA 是安全与性能的甜点区。
4.2.2 ECDSA 签名
ECDSA 基于椭圆曲线离散对数问题。同样的安全强度下,ECDSA 的密钥和签名都比 RSA 短得多。
- 优点:签名短(64 字节 for P-256),验签快(比 RSA 快 3-5 倍)。
- 缺点:实现复杂,容易踩坑(比如随机数质量、曲线参数校验)。
- 常见曲线:NIST P-256(secp256r1)、secp256k1(比特币用的那个)。
我建议,新项目优先考虑 ECDSA。为什么?因为签名短,固件包就小,升级传输时间也短。而且验签快,Bootloader 启动延迟低。我在一个电池供电的传感器项目里,用 ECDSA P-256,验签只用了 80ms,比 RSA 2048 快了 4 倍。
4.2.3 RSA vs ECDSA 对比
| 对比项 | RSA-2048 | ECDSA P-256 |
|---|---|---|
| 安全强度 | 112 bit | 128 bit |
| 公钥长度 | 256 字节 | 64 字节(未压缩) |
| 签名长度 | 256 字节 | 64 字节 |
| 验签速度(200MHz MCU) | 约 200-300 ms | 约 50-80 ms |
| 签名速度 | 慢(约 500-800 ms) | 中等(约 100-200 ms) |
| 实现复杂度 | 低 | 高(需处理曲线运算) |
我的经验:如果团队里没有密码学专家,选 RSA 更稳妥。ECDSA 的随机数生成如果出问题(比如重复使用 k 值),私钥会直接泄露。我见过一个团队因为随机数种子没初始化好,导致所有签名都能被伪造。嗯,这种坑踩一次就够了。
4.3 签名流程与验签流程
下面我把完整的流程拆开讲。你照着这个做,基本不会出大问题。
4.3.1 签名流程(在 PC 或构建服务器上执行)
- 准备固件:编译生成固件二进制文件(比如 firmware.bin)。
- 计算哈希:用 SHA256 对 firmware.bin 算哈希,得到 32 字节的 digest。
- 签名:用私钥对 digest 做签名,得到签名值(RSA 是 256 字节,ECDSA 是 64 字节)。
- 打包:把签名值附加到固件后面,或者放在固件头部。我习惯放在头部,这样 Bootloader 解析方便。
# 示例:使用 OpenSSL 对固件签名(ECDSA P-256)
# 1. 生成私钥
openssl ecparam -genkey -name prime256v1 -out private.pem
# 2. 提取公钥
openssl ec -in private.pem -pubout -out public.pem
# 3. 计算固件哈希并签名
openssl dgst -sha256 -sign private.pem -out firmware.sig firmware.bin
# 4. 查看签名(64 字节)
xxd firmware.sig
4.3.2 验签流程(在 Bootloader 中执行)
- 读取固件:从 Flash 或外部存储读取固件数据。
- 提取签名:从固件头部或尾部取出签名值。
- 计算哈希:对固件数据(不含签名部分)算 SHA256,得到 digest。
- 验签:用公钥对 digest 和签名做验证。如果通过,固件合法;否则拒绝启动。
// 伪代码:Bootloader 验签流程
bool verify_firmware(const uint8_t* firmware, uint32_t size) {
// 假设签名在固件末尾 64 字节
const uint8_t* sig = firmware + size - 64;
uint32_t fw_size = size - 64;
// 1. 计算固件哈希
uint8_t hash[32];
sha256(firmware, fw_size, hash);
// 2. 用公钥验签
return ecdsa_verify(public_key, hash, sig);
}
避坑指南:我曾经犯过一个低级错误——验签时把签名数据也算进了哈希。结果每次验签都失败,查了两天才发现。记住:哈希只算固件本体,签名本身不算。
4.4 签名流程与验签流程的 SVG 框架图
下面这张图把签名和验签的完整链路画出来了。你可以看到,私钥只在签名侧出现,公钥在两侧都有,但 Bootloader 里只存公钥。
4.5 实际项目中的避坑点
最后,我把自己踩过的坑和总结的经验列出来,你写代码时对照着检查。
签名校验的黄金法则:永远不要在 Bootloader 里留后门。我见过有人为了调试方便,加了一个「跳过签名校验」的宏开关,结果出货时忘了关。嗯,那批产品后来全部召回重刷。
- 公钥存储:公钥要放在 Bootloader 的只读区域,最好加上 CRC 校验,防止 Flash 比特翻转导致公钥损坏。
- 哈希算法选择:32 位 MCU 用 SHA256,64 位 CPU 用 SHA512。有硬件加速器时,优先用硬件。
- 签名算法选择:新项目优先 ECDSA P-256,老项目或团队密码学经验不足时用 RSA-2048。
- 随机数质量:ECDSA 签名时,随机数必须真随机。重复使用 k 值会导致私钥泄露。我建议用硬件 TRNG 或高质量的 PRNG。
- 验签超时:Bootloader 验签时,要加看门狗喂狗操作。RSA 验签可能耗时几百毫秒,不加看门狗会复位。
- 固件版本回滚:签名校验通过后,还要检查固件版本号,防止回滚到有漏洞的旧版本。
我的习惯:每次发布固件前,我都会写一个自动化脚本,在 CI 流程里自动签名。脚本里会检查私钥文件权限,确保只有构建服务器能读取。这样既安全又省心。