4. 数字签名原理:签名生成流程与验证流程,公钥基础设施(PKI)概念

好,咱们进入正题。数字签名这东西,说白了就是给固件盖个「防伪章」。你想想看,OTA 升级时设备从网络上下载一个固件包,怎么知道这包是官方发的,不是黑客塞进来的?靠的就是数字签名。

我刚开始接触嵌入式安全时,总觉得数字签名很玄乎。后来亲手在 STM32 上跑了一遍签名验证流程,才明白——其实就是「私钥加密,公钥解密」的逆向思维。嗯,咱们一步步拆开看。

4.1 签名生成流程:私钥是「印章」

签名生成发生在固件发布方,也就是你的编译服务器或 CI 流水线上。流程大致如下:

  1. 计算固件哈希:对固件二进制做一次哈希运算(比如 SHA-256),得到一个固定长度的摘要。
  2. 用私钥加密哈希:把哈希值用签名者的私钥进行非对称加密(比如 RSA-2048 或 ECDSA)。加密后的结果就是「数字签名」。
  3. 打包固件 + 签名:把原始固件和签名拼接在一起,形成可发布的升级包。

核心要点:签名不是加密整个固件,而是加密固件的哈希。这样做既快又安全——哈希很短(32 字节),加密开销小;而且哈希能唯一代表固件内容。

我在项目中遇到过一个问题:有人把签名直接追加在固件末尾,结果 bootloader 解析时没对齐 4 字节边界,导致 CRC 校验失败。后来我习惯在签名前加一个固定魔数(比如 0x5349474E),方便 bootloader 定位签名位置。

4.2 签名验证流程:公钥是「验章机」

验证发生在设备端,也就是 bootloader 或安全固件加载器里。流程如下:

  1. 拆包:从升级包中分离出原始固件和签名。
  2. 计算固件哈希:对原始固件做同样的哈希运算(SHA-256),得到摘要 A。
  3. 用公钥解密签名:用预先烧录在设备里的公钥解密签名,得到摘要 B。
  4. 比较摘要 A 和 B:如果一致,签名验证通过;否则拒绝升级。

避坑指南:我曾经在某个项目里,公钥烧录时字节序搞反了(大端小端问题),结果每次签名验证都失败。排查了两天才发现——嗯,嵌入式世界里字节序是个隐形杀手。

你可能会问:为什么不用对称加密?因为对称加密的密钥必须保密,而设备端成千上万台,密钥一旦泄露就全完了。非对称加密的好处是:公钥公开也没事,私钥只在发布方手里。

4.3 公钥基础设施(PKI)概念

PKI 听起来高大上,其实就是一个「信任链」体系。咱们用一张图来理解:

PKI 信任链结构图 PKI 信任链结构 根 CA(自签名) 中间 CA 设备证书 固件签名 信任锚点 预置在设备中 证书链 逐级签发验证 设备身份 包含公钥和签名 最终验证 固件哈希比对

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)来管理密钥。