3. 固件签名机制:代码签名流程、签名密钥管理、防回滚保护

说到固件升级的安全防护,签名机制是绕不开的核心防线。我见过太多产品因为签名没做好,被黑客直接塞进恶意固件,整批设备沦陷。说白了,签名就是给固件贴上一张「防伪标签」,让设备能认出来——这到底是不是我亲爹发的包。

这一节我们拆成三块来讲:签名流程怎么跑、密钥怎么管、以及怎么防止设备被降级到有漏洞的老版本。

3.1 代码签名流程:从编译到验证

先看一张整体流程图,把签名和验证的完整链路串起来。

固件签名与验证流程 固件签名与验证流程 签名端(开发/发布环境) 原始固件 binary SHA-256 / SHA-512 哈希 私钥签名(RSA/ECDSA) 固件 + 签名 + 元数据 打包 发布到 OTA 服务器 验证端(设备端) 接收固件包 提取签名 + 公钥 计算固件哈希 公钥验签(RSA/ECDSA) 通过 → 升级 / 失败 → 拒绝 网络传输 核心原则:私钥永不离线,公钥预置在设备中

流程其实不复杂,但每一步都有坑。我一个个说。

3.1.1 签名端:私钥签名

开发环境编译出固件后,第一步是算哈希。我个人习惯用 SHA-256,平衡安全性和性能。算完哈希,用私钥对这个哈希做签名。常用的算法有 RSA-2048/4096 和 ECDSA P-256/P-384。

签名包结构示例:

+-------------------------------+
| 固件头 (Firmware Header)      |
|   - 魔数 (Magic Number)       |
|   - 版本号 (Version)          |
|   - 固件大小 (Size)           |
|   - 哈希算法标识 (Hash Alg)   |
|   - 签名算法标识 (Sig Alg)    |
|   - 时间戳 (Timestamp)        |
+-------------------------------+
| 固件数据 (Firmware Binary)    |
+-------------------------------+
| 签名值 (Signature)            |
|   - RSA-2048: 256 字节        |
|   - ECDSA P-256: 64 字节      |
+-------------------------------+
| 公钥证书 (可选)               |
+-------------------------------+

签名完成后,把固件、签名、元数据打包成一个文件。元数据里要包含版本号,这个后面防回滚要用到。

3.1.2 验证端:公钥验签

设备收到固件包后,先提取签名值和公钥。公钥是出厂时烧录在设备里的,一般放在安全存储区(比如 eFuse 或 TPM 芯片)。然后用公钥解密签名,得到哈希值 A。同时设备自己算一遍固件的哈希,得到哈希值 B。两个值一致,签名通过。

我的经验:验签一定要在 BootROM 或安全启动阶段做,不要在应用层做。应用层容易被绕过。我曾经在一个项目里发现,某厂商把验签放在 Linux 用户空间,结果黑客直接跳过了验签函数,刷入了恶意固件。

3.2 签名密钥管理:命根子要锁好

密钥管理是整个签名体系里最脆弱的一环。算法再强,密钥泄露了全白搭。我见过最离谱的事——某公司把私钥直接放在 Git 仓库里,还公开了。嗯,那批设备基本废了。

3.2.1 密钥分级:生产 vs 测试

一定要分两套密钥:测试密钥和生产密钥。测试密钥可以随便用,开发人员人手一份。生产密钥必须严格管控。

密钥类型 用途 存储位置 访问权限
测试私钥 开发调试、CI/CD 测试 开发机、CI 服务器 开发团队全员
生产私钥 正式发布固件 HSM(硬件安全模块) 仅 1-2 名管理员
测试公钥 开发板验证 设备固件中 公开
生产公钥 量产设备验证 设备 eFuse / OTP 公开(但不可改写)

3.2.2 私钥存储:HSM 是标配

生产私钥绝对不能以明文形式存在硬盘上。我建议用 HSM(硬件安全模块)来存储和签名。HSM 是一个专用硬件,私钥从生成到使用都在芯片内部完成,外部拿不到。签名时,把哈希值发给 HSM,HSM 返回签名结果。

警告:千万不要把生产私钥放在 CI 服务器的环境变量里。CI 服务器被攻破,私钥就没了。如果非要用 CI 自动签名,至少用云 HSM 服务(比如 AWS CloudHSM 或 Azure Key Vault)。

3.2.3 密钥轮换与吊销

密钥不能一辈子不换。我建议每年轮换一次生产密钥。轮换时,新固件用新私钥签名,同时设备里要预置新旧两个公钥。等所有设备都升级到新密钥后,再废弃旧公钥。

如果私钥泄露了,怎么办?这就是吊销机制。设备里要维护一个吊销列表(CRL),或者用密钥版本号机制。设备只接受版本号 >= 当前版本的密钥签名的固件。

3.3 防回滚保护:堵死降级攻击

防回滚,说白了就是不让设备刷回旧版本固件。为什么重要?因为旧版本可能有已知漏洞。黑客可以先拿到旧版本固件,分析漏洞,然后诱骗设备降级,再发动攻击。

3.3.1 版本号检查

最基础的防回滚就是版本号检查。固件头里包含版本号,设备在验签通过后,比较新固件的版本号和当前运行版本号。如果新版本 <= 当前版本,直接拒绝升级。

版本号检查伪代码:

// 设备端验签后的版本检查
uint32_t current_ver = read_current_firmware_version();
uint32_t new_ver = extract_version_from_firmware_header();

if (new_ver <= current_ver) {
    // 拒绝升级
    log_error("Rollback attack detected!");
    return UPGRADE_FAIL_REJECTED;
}

// 版本号通过,继续升级流程
proceed_with_upgrade();

这里有个细节:版本号必须是单调递增的。我建议用主版本号 + 次版本号 + 补丁号,或者直接用时间戳。但要注意,时间戳可能因为设备时钟不准而出问题。我个人更推荐用递增的整数版本号。

3.3.2 硬件防回滚:eFuse / OTP

软件版本号检查有个致命弱点——黑客可以篡改当前版本号。比如把设备里的版本号改小,然后刷旧固件。所以需要硬件层面的防回滚。

常见的做法是用 eFuse 或 OTP(一次性可编程存储器)。每次升级成功后,把新版本号写入 eFuse。eFuse 只能从 0 变 1,不能从 1 变 0。这样黑客想改小版本号,门都没有。

硬件机制 原理 优点 缺点
eFuse 电熔丝,熔断后不可恢复 成本低,集成在芯片内 位数量有限(通常 32-256 位)
OTP 一次性可编程存储器 容量比 eFuse 大 需要额外电压编程
TPM 独立安全芯片,可记录版本 功能丰富,支持远程证明 成本高,占用 PCB 面积

3.3.3 防回滚的常见坑

我踩过几个坑,分享出来大家别重蹈覆辙。

  • 版本号溢出:用 uint32_t 存版本号,如果版本号达到 0xFFFFFFFF,再升级就溢出了。我建议版本号从 1 开始,预留一些空间。或者用时间戳,但时间戳要处理好 2038 年问题。
  • 测试版固件回滚:开发阶段经常需要刷回旧版本调试。这时候硬件防回滚就成障碍了。我的做法是:测试密钥签名的固件不受版本号限制,生产密钥签名的固件才检查版本号。设备通过公钥类型区分。
  • 多分区回滚:有些设备有 A/B 分区,升级时写备用分区。如果新固件有问题,可以回滚到主分区。但回滚操作本身也要防回滚保护——不能从安全版本回滚到有漏洞的版本。

避坑指南:我曾经在一个 IoT 项目里,只做了软件版本号检查,没做硬件防回滚。结果黑客通过 UART 接口修改了内存中的版本号变量,成功刷入了有漏洞的旧固件。从那以后,我所有项目都强制要求 eFuse 记录版本号。

3.4 小结

固件签名机制,说白了就是三件事:签名流程要完整、密钥管理要严格、防回滚要到位。签名流程保证固件来源可信,密钥管理保证签名本身不被伪造,防回滚保证设备始终运行在安全版本上。这三者缺一不可。