4. 固件签名与验证:代码签名流程、密钥管理、安全启动链

固件签名,说白了就是给你的固件打上一个「防伪标签」。我见过太多团队,花了大半年做功能开发,最后在签名验证上栽跟头——要么密钥泄露,要么启动链断了一环,整块板子变砖。嗯,这一节我们就把这件事彻底讲透。

4.1 代码签名流程:从编译到烧录

签名不是最后随手加一步,它应该嵌入你的 CI/CD 流水线。我个人习惯把签名流程拆成四个阶段:

  1. 编译:生成原始固件二进制(.bin / .hex)
  2. 哈希:对固件计算摘要(SHA-256 / SHA-384)
  3. 签名:用私钥加密摘要,生成签名块
  4. 打包:将固件 + 签名 + 元数据(版本、设备ID)合并为发布包

核心原则:签名必须在受控环境执行,私钥绝不能出现在开发者的笔记本上。

我在项目中遇到过,有人把签名脚本直接写在 Makefile 里,私钥硬编码在环境变量中。结果一次 Git 泄露,整个产品线的固件都能被伪造。从那以后,我强制团队使用独立的签名服务器或 HSM(硬件安全模块)。

下面是一个典型的签名命令行流程(以 OpenSSL + Python 为例):

# 1. 计算固件哈希
openssl dgst -sha256 -binary firmware.bin > firmware.hash

# 2. 用私钥签名哈希
openssl pkeyutl -sign \
    -inkey private.pem \
    -keyform PEM \
    -in firmware.hash \
    -out firmware.sig

# 3. 打包(Python 示例)
import struct
with open('firmware.bin', 'rb') as f:
    fw_data = f.read()
with open('firmware.sig', 'rb') as f:
    sig_data = f.read()

# 元数据:版本号 4字节 + 签名长度 4字节
meta = struct.pack('<II', 0x0100, len(sig_data))
package = meta + sig_data + fw_data
with open('firmware_pkg.bin', 'wb') as f:
    f.write(package)

小技巧:签名块最好放在固件头部,而不是尾部。这样 Bootloader 可以先读取签名,再决定是否加载固件,不用先读完整个文件。

4.2 密钥管理:最容易被忽视的环节

你想想看,签名算法再强,密钥丢了等于白搭。密钥管理是整个安全启动链的「命门」。我把它分为三个层级:

层级 存储位置 用途 生命周期
根密钥 HSM / 离线保险柜 签发中间密钥 产品全生命周期不变
中间密钥 签名服务器(加密存储) 日常固件签名 每季度轮换
设备密钥 芯片 OTP / eFuse 验证签名 出厂即烧录,不可更改

我曾经见过一个团队,把生产用的私钥放在共享网盘上,还设了个「123456」的密码。嗯,后果可想而知——市场上出现了大量伪造固件,售后成本直接爆表。

避坑指南:

  • 绝对不要用同一套密钥签开发版和生产版固件
  • 密钥轮换时,旧版本固件必须用旧密钥验证,不能一刀切
  • 设备端的公钥要写在 OTP 里,不能放在 Flash 中(容易被替换)

4.3 安全启动链:从 ROM 到应用

安全启动链,说白了就是「信任的传递」。每一级代码在运行前,都要验证下一级代码的签名。一旦某级验证失败,系统必须停止启动。

我画了一张流程图,帮你理清这个链条:

安全启动链流程图 第1级:ROM Bootloader 硬件固化,不可修改 第2级:Flash Bootloader 验证签名 → 通过则跳转 第3级:固件镜像 验证签名 → 通过则加载 第4级:应用层 运行已验证的代码 验证失败 进入恢复模式 信任根(RoT) ROM 代码是信任的起点 后续每一级都验证上一级 任何一级失败 → 停止启动 密钥存储 公钥:OTP / eFuse 私钥:HSM / 签名服务器

为什么要有这么多级?我解释一下:ROM Bootloader 是芯片出厂固化的,攻击者改不了。它只做一件事——验证 Flash 中的 Bootloader 签名。Flash Bootloader 再验证固件签名。这样即使 Flash 内容被篡改,ROM 代码也能拦住。

关键点:每一级验证的「公钥」必须提前烧录在上一级的安全存储中。ROM 的公钥写在芯片 OTP 里,Flash Bootloader 的公钥写在固件头部(由 ROM 验证)。

4.4 OTA 场景下的签名验证

OTA 升级时,签名验证的流程稍有不同。因为固件是通过网络传输的,你还要考虑「传输中篡改」和「回滚攻击」。

我建议的 OTA 签名验证流程:

  1. 云端签名:在 OTA 服务器上用中间密钥签名固件包
  2. 设备下载:通过 HTTPS + 签名双重保障
  3. 设备验证:Bootloader 验证签名,同时检查版本号是否大于当前版本
  4. 回滚防护:如果版本号低于当前版本,拒绝安装

注意:回滚攻击是 OTA 最常见的攻击方式之一。攻击者会诱骗设备安装旧版本固件,利用已知漏洞。所以版本号检查必须和签名验证绑定,不能分开处理。

我曾经在审计一个 IoT 项目时发现,他们的 OTA 只验证了签名,没检查版本号。结果攻击者伪造了一个旧版本固件(签名是合法的,因为旧版本私钥没轮换),成功降级了设备。嗯,从那以后,我把「版本号 + 签名」联合验证写进了所有项目的安全基线。

4.5 密钥轮换与吊销

密钥不可能永远不换。我建议的轮换策略:

密钥类型 轮换周期 轮换方式
根密钥 不轮换(物理保护) 仅用于签发中间密钥
中间密钥 每 3-6 个月 用根密钥签发新中间密钥
设备公钥 不轮换(OTP 不可改写) 出厂前烧录

如果中间密钥泄露,怎么办?你需要一个吊销机制。我个人习惯的做法是:

  • 在固件头部维护一个「吊销列表」(Revocation List)
  • 列表里记录已泄露的密钥 ID
  • Bootloader 验证签名时,先检查密钥 ID 是否在吊销列表中
  • 如果在,直接拒绝加载

小技巧:吊销列表要放在固件头部,并且由根密钥签名。这样即使攻击者篡改列表,也能被检测到。

好了,固件签名与验证的核心内容就这些。记住一句话:信任不能传递,只能验证。每一级代码都要自己验证下一级的签名,不能依赖上一级的「口头保证」。