4. 固件签名与验证:代码签名流程、密钥管理、安全启动链
固件签名,说白了就是给你的固件打上一个「防伪标签」。我见过太多团队,花了大半年做功能开发,最后在签名验证上栽跟头——要么密钥泄露,要么启动链断了一环,整块板子变砖。嗯,这一节我们就把这件事彻底讲透。
4.1 代码签名流程:从编译到烧录
签名不是最后随手加一步,它应该嵌入你的 CI/CD 流水线。我个人习惯把签名流程拆成四个阶段:
- 编译:生成原始固件二进制(.bin / .hex)
- 哈希:对固件计算摘要(SHA-256 / SHA-384)
- 签名:用私钥加密摘要,生成签名块
- 打包:将固件 + 签名 + 元数据(版本、设备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 到应用
安全启动链,说白了就是「信任的传递」。每一级代码在运行前,都要验证下一级代码的签名。一旦某级验证失败,系统必须停止启动。
我画了一张流程图,帮你理清这个链条:
为什么要有这么多级?我解释一下:ROM Bootloader 是芯片出厂固化的,攻击者改不了。它只做一件事——验证 Flash 中的 Bootloader 签名。Flash Bootloader 再验证固件签名。这样即使 Flash 内容被篡改,ROM 代码也能拦住。
关键点:每一级验证的「公钥」必须提前烧录在上一级的安全存储中。ROM 的公钥写在芯片 OTP 里,Flash Bootloader 的公钥写在固件头部(由 ROM 验证)。
4.4 OTA 场景下的签名验证
OTA 升级时,签名验证的流程稍有不同。因为固件是通过网络传输的,你还要考虑「传输中篡改」和「回滚攻击」。
我建议的 OTA 签名验证流程:
- 云端签名:在 OTA 服务器上用中间密钥签名固件包
- 设备下载:通过 HTTPS + 签名双重保障
- 设备验证:Bootloader 验证签名,同时检查版本号是否大于当前版本
- 回滚防护:如果版本号低于当前版本,拒绝安装
注意:回滚攻击是 OTA 最常见的攻击方式之一。攻击者会诱骗设备安装旧版本固件,利用已知漏洞。所以版本号检查必须和签名验证绑定,不能分开处理。
我曾经在审计一个 IoT 项目时发现,他们的 OTA 只验证了签名,没检查版本号。结果攻击者伪造了一个旧版本固件(签名是合法的,因为旧版本私钥没轮换),成功降级了设备。嗯,从那以后,我把「版本号 + 签名」联合验证写进了所有项目的安全基线。
4.5 密钥轮换与吊销
密钥不可能永远不换。我建议的轮换策略:
| 密钥类型 | 轮换周期 | 轮换方式 |
|---|---|---|
| 根密钥 | 不轮换(物理保护) | 仅用于签发中间密钥 |
| 中间密钥 | 每 3-6 个月 | 用根密钥签发新中间密钥 |
| 设备公钥 | 不轮换(OTP 不可改写) | 出厂前烧录 |
如果中间密钥泄露,怎么办?你需要一个吊销机制。我个人习惯的做法是:
- 在固件头部维护一个「吊销列表」(Revocation List)
- 列表里记录已泄露的密钥 ID
- Bootloader 验证签名时,先检查密钥 ID 是否在吊销列表中
- 如果在,直接拒绝加载
小技巧:吊销列表要放在固件头部,并且由根密钥签名。这样即使攻击者篡改列表,也能被检测到。
好了,固件签名与验证的核心内容就这些。记住一句话:信任不能传递,只能验证。每一级代码都要自己验证下一级的签名,不能依赖上一级的「口头保证」。