3. 固件签名机制:代码签名流程、签名密钥管理、防回滚保护
说到固件升级的安全防护,签名机制是绕不开的核心防线。我见过太多产品因为签名没做好,被黑客直接塞进恶意固件,整批设备沦陷。说白了,签名就是给固件贴上一张「防伪标签」,让设备能认出来——这到底是不是我亲爹发的包。
这一节我们拆成三块来讲:签名流程怎么跑、密钥怎么管、以及怎么防止设备被降级到有漏洞的老版本。
3.1 代码签名流程:从编译到验证
先看一张整体流程图,把签名和验证的完整链路串起来。
流程其实不复杂,但每一步都有坑。我一个个说。
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 小结
固件签名机制,说白了就是三件事:签名流程要完整、密钥管理要严格、防回滚要到位。签名流程保证固件来源可信,密钥管理保证签名本身不被伪造,防回滚保证设备始终运行在安全版本上。这三者缺一不可。