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 流水线或开发机上完成。流程如下:

  1. 计算固件哈希:对固件二进制文件做 SHA-256,得到 32 字节摘要。
  2. 签名哈希:用私钥对哈希值做签名运算,生成签名数据(RSA 是 256 字节,ECC 是 64 字节)。
  3. 打包固件:将原始固件 + 签名数据 + 公钥(或公钥索引)打包成一个固件包。

代码示例(伪代码,用 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 执行。流程是签名的逆过程:

  1. 提取固件和签名:从 Flash 中读取固件包,分离出原始固件和签名数据。
  2. 计算固件哈希:对原始固件做 SHA-256,得到 32 字节摘要。
  3. 验签:用公钥对「摘要 + 签名」做验签运算。如果验签通过,说明固件未被篡改且来自合法发布者。

代码示例(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 端验签,中间是固件包传输。

固件签名与验签完整流程 PC端 / 构建服务器(签名) 固件二进制文件 SHA-256 哈希计算 32字节哈希值 私钥签名 → 签名数据 打包:固件 + 签名 + 公钥 固件包 (Flash / 网络) MCU端 / BootROM(验签) 解包:分离固件、签名、公钥 SHA-256 哈希计算 32字节哈希值 公钥验签:哈希 + 签名 验签通过 → 启动 / 验签失败 → 停止

4.6 避坑指南

做固件签名验证,有几个坑我踩过,分享给你:

  • 哈希计算范围:签名时算的是整个固件的哈希,还是去掉签名部分?我建议算整个固件(包括头部),然后把签名附加在尾部。这样验签时先读头部知道固件长度,再算哈希,逻辑清晰。
  • 公钥存储位置:别把公钥放在固件里。公钥应该烧在 OTP 或写在 BootROM 代码中。有一次我看到一个方案把公钥放在固件头部——那等于自己拆了自己的锁。
  • 验签失败处理:验签失败后,MCU 应该进入错误状态,不能回退到未签名固件。我见过一个设计,验签失败后尝试从备份区启动——结果备份区也是被篡改的固件,等于没验。
  • 侧信道攻击:如果攻击者能测量验签时间或功耗,可能推断出私钥信息。建议使用常数时间比较函数(如 mbedtls_safer_memcmp)来比较哈希值。

总结一下:固件签名验证是安全启动的基石。哈希保证完整性,签名保证真实性,公钥保证信任根。这三者缺一不可。

嗯,这一章的内容就到这里。记住:签名不是加密,是防伪。你的 BootROM 就是那个「验章员」,它只认正确的签名,其他的一概不认。