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 图,把整个签名流程串起来了。从固件编译完成,到最终打包成可烧录的镜像,每一步都标清楚了。

固件签名完整流程图 固件签名完整流程图 固件编译 提取元数据 计算摘要 签名生成 HSM / 私钥 打包元数据 生成烧录镜像 关键说明 • 摘要输入包含版本号和平台ID • 签名算法推荐 ECDSA P256 • 元数据格式推荐 CBOR • 最终镜像 = 固件 + 元数据 签名模式 • 离线:HSM 物理隔离,人工触发 • 在线:签名服务器 API,自动流水线 • 私钥永不离 HSM 或安全飞地

4.5 避坑指南:我踩过的三个坑

最后分享几个实战中容易翻车的地方,都是我用真金白银换来的教训。

  1. 摘要输入顺序不一致:开发环境和产线工具用的摘要输入顺序不同,导致签名验证失败。解决方案:在代码里硬编码输入顺序,并在 CI 里加自动化测试,每次构建都验证签名一致性。
  2. 签名算法参数不匹配:ECDSA 的曲线参数(如 P256 vs P384)必须和 BootROM 完全一致。我见过一个项目,开发机用 OpenSSL 默认的 P256 签名,但 BootROM 只支持 P384,结果启动时验签失败。嗯,最后只能重新签名。
  3. 元数据中的时间戳未考虑时区:签名服务器用 UTC,产线工具用本地时间,导致时间戳校验失败。统一用 Unix 时间戳(自 1970-01-01 00:00:00 UTC 的秒数),彻底避免时区问题。

一个小技巧:在元数据里加一个 4 字节的「魔数」(比如 0x5349474E 即 "SIGN"),BootROM 扫描固件时先找魔数,找到后再解析元数据。这样即使元数据放在固件尾部,BootROM 也能快速定位,不用硬编码偏移量。