4. 数字签名原理:签名生成流程与验证流程,公钥基础设施(PKI)概念
好,咱们进入正题。数字签名这东西,说白了就是给固件盖个「防伪章」。你想想看,OTA 升级时设备从网络上下载一个固件包,怎么知道这包是官方发的,不是黑客塞进来的?靠的就是数字签名。
我刚开始接触嵌入式安全时,总觉得数字签名很玄乎。后来亲手在 STM32 上跑了一遍签名验证流程,才明白——其实就是「私钥加密,公钥解密」的逆向思维。嗯,咱们一步步拆开看。
4.1 签名生成流程:私钥是「印章」
签名生成发生在固件发布方,也就是你的编译服务器或 CI 流水线上。流程大致如下:
- 计算固件哈希:对固件二进制做一次哈希运算(比如 SHA-256),得到一个固定长度的摘要。
- 用私钥加密哈希:把哈希值用签名者的私钥进行非对称加密(比如 RSA-2048 或 ECDSA)。加密后的结果就是「数字签名」。
- 打包固件 + 签名:把原始固件和签名拼接在一起,形成可发布的升级包。
核心要点:签名不是加密整个固件,而是加密固件的哈希。这样做既快又安全——哈希很短(32 字节),加密开销小;而且哈希能唯一代表固件内容。
我在项目中遇到过一个问题:有人把签名直接追加在固件末尾,结果 bootloader 解析时没对齐 4 字节边界,导致 CRC 校验失败。后来我习惯在签名前加一个固定魔数(比如 0x5349474E),方便 bootloader 定位签名位置。
4.2 签名验证流程:公钥是「验章机」
验证发生在设备端,也就是 bootloader 或安全固件加载器里。流程如下:
- 拆包:从升级包中分离出原始固件和签名。
- 计算固件哈希:对原始固件做同样的哈希运算(SHA-256),得到摘要 A。
- 用公钥解密签名:用预先烧录在设备里的公钥解密签名,得到摘要 B。
- 比较摘要 A 和 B:如果一致,签名验证通过;否则拒绝升级。
避坑指南:我曾经在某个项目里,公钥烧录时字节序搞反了(大端小端问题),结果每次签名验证都失败。排查了两天才发现——嗯,嵌入式世界里字节序是个隐形杀手。
你可能会问:为什么不用对称加密?因为对称加密的密钥必须保密,而设备端成千上万台,密钥一旦泄露就全完了。非对称加密的好处是:公钥公开也没事,私钥只在发布方手里。
4.3 公钥基础设施(PKI)概念
PKI 听起来高大上,其实就是一个「信任链」体系。咱们用一张图来理解:
PKI 的核心组件包括:
| 组件 | 作用 | 在 OTA 中的角色 |
|---|---|---|
| 根 CA | 信任链的起点,自签名证书 | 预置在设备 ROM 中,不可更改 |
| 中间 CA | 由根 CA 签发,用于签发设备证书 | 可轮换,降低根 CA 暴露风险 |
| 设备证书 | 包含设备公钥和 CA 签名 | 用于验证固件签名 |
| 证书吊销列表(CRL) | 记录已失效的证书 | OTA 前检查证书是否被吊销 |
重要提醒:千万不要把根 CA 私钥放在 CI 服务器上!我见过一个团队把根 CA 私钥硬编码在 Jenkins 脚本里,结果仓库被黑,所有设备都得召回。根 CA 私钥应该离线存储,只在签名机里使用。
4.4 实际项目中的 PKI 落地
在嵌入式 OTA 场景里,PKI 通常简化成两层:
- 根证书:烧录在芯片的一次性可编程(OTP)区域,或者安全元件(SE)里。这是信任的锚点。
- 签名证书:由根证书签发,用于实际签名固件。可以定期轮换。
我习惯在 bootloader 里做证书链验证:先验证签名证书是否由根证书签发,再用签名证书的公钥验证固件签名。这样即使签名证书泄露,只要根证书没丢,重新签发一个就行。
举个例子,一个典型的固件签名验证代码片段(伪代码):
// 伪代码:bootloader 中的签名验证
bool verify_firmware(const uint8_t* firmware, uint32_t size,
const uint8_t* signature, uint32_t sig_len) {
// 1. 计算固件哈希
uint8_t hash[32];
sha256(firmware, size, hash);
// 2. 用预置根证书验证签名证书
if (!verify_cert_chain(signature_cert, root_cert)) {
return false; // 证书链无效
}
// 3. 用签名证书的公钥验证签名
return rsa_verify(hash, signature, sig_len,
signature_cert.public_key);
}
这段代码虽然简单,但涵盖了 PKI 验证的核心逻辑。实际项目中还要考虑防回滚、版本号校验等,那是后话了。
4.5 常见误区与避坑
- 误区一:签名 = 加密。签名是哈希加密,不是整个固件加密。加密是为了保密,签名是为了防篡改。
- 误区二:公钥可以随便存。公钥虽然公开,但必须保证完整性——如果攻击者替换了设备里的公钥,就能用自己的私钥签名恶意固件。所以公钥要放在只读区域。
- 误区三:证书链越长越安全。链太长会占用 bootloader 的宝贵 Flash 空间,而且验证时间变长。嵌入式场景一般两层就够了。
个人经验:我曾经在量产前发现,某个芯片的 OTP 区域只能烧录一次,而根证书的哈希值写错了。结果只能换芯片——嗯,教训就是:烧录前一定要做三次校验,最好用硬件安全模块(HSM)来管理密钥。