4. 固件签名流程:固件摘要计算,签名生成(离线/在线),签名打包与元数据格式
好,我们进入第四章。前面几章我们把信任根和信任链的骨架搭起来了,现在要往里面填最关键的肉——固件签名。
签名这件事,说白了就是给固件发一张「身份证」,再加一个「防拆封条」。设备启动时,BootROM 先验这张身份证是不是官方发的,再检查封条有没有被撕开。整个过程分三步:算摘要 → 生成签名 → 打包成元数据。每一步都有坑,我一个个说。
4.1 固件摘要计算:选对哈希,别省性能
摘要计算是整个签名的地基。地基歪了,后面全白搭。
我个人习惯,摘要算法至少选 SHA-256。SHA-1 在 2017 年就被 Google 和 CWI 联合攻破了,现在还在用 SHA-1 的项目,基本等于裸奔。我在一个 IoT 网关项目里见过用 MD5 签固件的,问就是「BootROM 只支持 MD5,改不了」。嗯,那款芯片是 2008 年的老古董,最后我们只能加外部安全芯片做二次校验。
摘要计算要覆盖哪些内容?不只是固件二进制本身。我建议至少包含:
- 固件主体(.bin / .hex)
- 固件版本号(防版本回滚)
- 芯片唯一 ID 或平台 ID(防固件跨平台乱刷)
- 签名时间戳(可选,用于证书有效期校验)
计算时注意一个细节:摘要的输入顺序必须固定。我在产线调试时遇到过,开发环境用「版本号 + 固件」的顺序算摘要,产线工具用「固件 + 版本号」的顺序算,结果签名验证永远失败。排查了两天才发现,气得我直接在代码里加了输入顺序的断言。
摘要计算伪代码(推荐顺序):
// 输入:固件二进制、版本号、平台ID
// 输出:256位摘要
digest_input = concat(
version, // 4字节,小端序
platform_id, // 8字节
firmware_binary, // 变长
timestamp // 4字节,Unix时间戳
)
digest = SHA256(digest_input)
4.2 签名生成:离线 vs 在线,选哪个?
签名生成有两种模式:离线签名和在线签名。没有绝对的好坏,看你的安全等级和产线环境。
| 对比项 | 离线签名 | 在线签名 |
|---|---|---|
| 私钥存储位置 | HSM / 离线机(不联网) | 签名服务器(联网但受控) |
| 签名速度 | 慢(需人工或定时任务触发) | 快(API 调用,毫秒级) |
| 适用场景 | 高安全等级、小批量产 | 大批量产、CI/CD 流水线 |
| 私钥泄露风险 | 极低(物理隔离) | 中等(需严格网络隔离) |
离线签名,我最早在军工项目里接触。一台不联网的 Linux 机,插着 USB-HSM,每次签名要插拔密钥卡。安全是真安全,但效率也是真低。后来在消费电子项目里,产线一天要刷几万台设备,离线签名根本扛不住,只能上在线签名。
在线签名,核心是签名服务器。私钥存在服务器的 HSM 或安全飞地(如 Intel SGX)里,对外暴露一个签名 API。产线工具把摘要发过去,服务器返回签名值。这里有个关键点:签名服务器必须做请求认证和频率限制。我曾经见过一个项目,签名 API 没加认证,结果被内部员工写脚本批量签名恶意固件,差点出事。
我的建议:
如果年产量小于 10 万台,用离线签名,安全第一。超过 10 万台,上在线签名,但一定要配合 HSM 和双向 TLS 认证。
4.3 签名打包与元数据格式
签名生成了,怎么和固件绑在一起?直接拼接?太粗暴了。我们需要一个标准化的元数据格式,让 BootROM 能解析、能校验、能追溯。
常见的元数据格式有两种:CBOR(RFC 7049)和 JSON。嵌入式环境我推荐 CBOR,二进制格式,解析快,体积小。JSON 虽然可读性好,但解析开销大,不适合 BootROM 这种资源受限的环境。
一个典型的签名元数据结构长这样:
{
"magic": "SIGN", // 4字节魔数,用于快速识别
"version": 1, // 元数据格式版本
"hash_alg": "SHA256", // 摘要算法
"sign_alg": "ECDSA_P256", // 签名算法
"platform_id": 0xA1B2, // 目标平台ID
"fw_version": 42, // 固件版本
"timestamp": 1712345678, // 签名时间
"digest": "0xAB...", // 固件摘要(32字节)
"signature": "0xCD...", // 签名值(64字节,ECDSA P256)
"cert_chain": [ // 证书链(可选)
"0x...", // 中间证书
"0x..." // 根证书
]
}
打包时,元数据可以放在固件的头部或尾部。我习惯放尾部,因为这样固件主体在烧录时可以流式处理,不用先读头部再跳转。但放尾部有个问题:BootROM 需要先知道元数据的偏移和长度。解决方案是在固件固定偏移(比如 0x200 处)放一个元数据指针,指向尾部元数据的起始地址。
注意:元数据本身也要做完整性保护。签名是对「摘要」签的,而摘要是对「固件 + 元数据中的版本/平台ID」算的。如果元数据中的版本号被篡改,签名校验会失败。这就是防回滚的原理。
4.4 完整签名流程:一张图说清楚
下面这张 SVG 图,把整个签名流程串起来了。从固件编译完成,到最终打包成可烧录的镜像,每一步都标清楚了。
4.5 避坑指南:我踩过的三个坑
最后分享几个实战中容易翻车的地方,都是我用真金白银换来的教训。
- 摘要输入顺序不一致:开发环境和产线工具用的摘要输入顺序不同,导致签名验证失败。解决方案:在代码里硬编码输入顺序,并在 CI 里加自动化测试,每次构建都验证签名一致性。
- 签名算法参数不匹配:ECDSA 的曲线参数(如 P256 vs P384)必须和 BootROM 完全一致。我见过一个项目,开发机用 OpenSSL 默认的 P256 签名,但 BootROM 只支持 P384,结果启动时验签失败。嗯,最后只能重新签名。
- 元数据中的时间戳未考虑时区:签名服务器用 UTC,产线工具用本地时间,导致时间戳校验失败。统一用 Unix 时间戳(自 1970-01-01 00:00:00 UTC 的秒数),彻底避免时区问题。
一个小技巧:在元数据里加一个 4 字节的「魔数」(比如 0x5349474E 即 "SIGN"),BootROM 扫描固件时先找魔数,找到后再解析元数据。这样即使元数据放在固件尾部,BootROM 也能快速定位,不用硬编码偏移量。