4、签名与验签流程:镜像签名工具链、验签流程实现、公钥管理策略

签名与验签,是整个安全启动的基石。说白了,就是给固件镜像发一张「身份证」,Bootloader 在加载前先查一下这张证是不是真的。我见过不少项目,加密算法选得挺强,结果公钥管理一塌糊涂,最后还是被攻破。这一章,咱们就把签名工具链、验签流程和公钥管理这三个环节彻底讲透。

4.1 镜像签名工具链

签名不是在嵌入式设备上做的,而是在你的 PC 或 CI 服务器上。工具链的核心任务就是:用私钥对镜像摘要加密,生成签名。

4.1.1 典型签名流程

  1. 计算摘要:对原始镜像(如 .bin)做哈希运算,得到固定长度的摘要(如 SHA-256)。
  2. 签名生成:用私钥对摘要进行非对称加密(如 RSA-2048 或 ECDSA),输出签名数据。
  3. 打包:将原始镜像 + 签名 + 元数据(版本号、镜像类型等)合并为一个签名镜像。

核心原则:私钥必须离线保存,绝不能出现在 CI 脚本或代码仓库里。我曾经见过有人把私钥硬编码在 Makefile 里……嗯,那项目后来整个安全体系都重做了。

4.1.2 常用签名工具对比

工具 算法支持 适用场景 个人评价
OpenSSL RSA、ECDSA、Ed25519 通用、CI 集成 功能最全,但命令行参数复杂
Python Cryptography RSA、ECDSA 自动化脚本 我习惯用它做原型验证
mbedTLS 自编译工具 RSA、ECDSA 资源受限环境 适合与 Bootloader 共用代码库
HSM 专用工具 厂商自定义 高安全等级量产 硬件保护私钥,成本较高

4.1.3 签名脚本示例(Python + Cryptography)

from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.backends import default_backend

def sign_image(image_path, private_key_path, output_path):
    # 1. 加载私钥
    with open(private_key_path, "rb") as key_file:
        private_key = serialization.load_pem_private_key(
            key_file.read(),
            password=None,  # 生产环境建议加密
            backend=default_backend()
        )

    # 2. 读取镜像并计算摘要
    with open(image_path, "rb") as img_file:
        image_data = img_file.read()
    digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
    digest.update(image_data)
    image_hash = digest.finalize()

    # 3. 签名
    signature = private_key.sign(
        image_hash,
        padding.PKCS1v15(),
        hashes.SHA256()
    )

    # 4. 打包:镜像 + 签名长度 + 签名 + 元数据
    with open(output_path, "wb") as out_file:
        out_file.write(image_data)
        out_file.write(len(signature).to_bytes(2, 'big'))
        out_file.write(signature)
        # 可附加版本号、哈希算法ID等

    print(f"签名完成,输出文件:{output_path}")

避坑指南:签名时一定要包含版本号或防回滚标记。我曾经遇到过只签镜像内容,结果攻击者把旧版本镜像的签名直接搬过来用——这就是经典的「回滚攻击」。

4.2 验签流程实现

验签发生在 Bootloader 里。设备上电后,Bootloader 在加载固件前,必须完成验签。这一步如果没过,系统直接拒绝启动。

4.2.1 验签核心步骤

  1. 提取签名:从签名镜像中分离出原始镜像和签名数据。
  2. 计算摘要:对原始镜像做同样的哈希运算。
  3. 公钥解密:用预置的公钥解密签名,得到解密后的摘要。
  4. 比对:对比步骤2和步骤3的摘要,一致则通过。

注意:验签必须在 Bootloader 的早期阶段完成,且验签代码本身不能被篡改。如果 Bootloader 的第一阶段(ROM code)没有验签能力,那整个安全链就断了。

4.2.2 Bootloader 验签代码片段(C 语言风格)

#include "crypto_wrapper.h"  // 封装了 mbedTLS 或内部加密库

int verify_image(const uint8_t *image, uint32_t image_len,
                 const uint8_t *signature, uint32_t sig_len,
                 const uint8_t *public_key, uint32_t key_len) {

    uint8_t computed_hash[32];
    uint8_t decrypted_hash[32];

    // 1. 计算镜像哈希
    sha256_compute(image, image_len, computed_hash);

    // 2. 用公钥解密签名
    if (rsa_public_decrypt(signature, sig_len,
                           public_key, key_len,
                           decrypted_hash) != 0) {
        return -1;  // 解密失败
    }

    // 3. 比对哈希
    if (memcmp(computed_hash, decrypted_hash, 32) != 0) {
        return -2;  // 验签失败
    }

    return 0;  // 验签通过
}

你想想看,这段代码看起来简单,但实际落地时坑很多。比如哈希比对要用恒定时间比较,防止时序攻击。我早期一个项目就忽略了这点,结果被侧信道攻击抓到了漏洞。

4.2.3 验签失败处理策略

  • 直接停机:进入死循环或触发硬件复位,不给攻击者任何反馈。
  • 进入恢复模式:如果设备支持,可以进入一个最小化的恢复 Bootloader,等待合法镜像通过安全通道刷入。
  • 记录错误日志:在安全存储区记录验签失败次数,用于检测暴力攻击。

我的建议:量产设备上,验签失败后不要打印任何调试信息。攻击者最喜欢从「Error: signature mismatch」这种信息里推断你的密钥结构。

4.3 公钥管理策略

公钥虽然可以公开,但它的完整性和可信根必须被保护。公钥一旦被篡改,攻击者就能用自己的私钥签名恶意镜像。

4.3.1 公钥存储位置

存储方式 安全性 灵活性 典型场景
ROM 硬编码 最高(不可更改) 最低 一次性量产,永不更新
OTP(一次性可编程) 高(熔断后不可改) 低 量产时烧录,支持少量批次
安全存储区(如 TrustZone) 中高 中 支持 OTA 公钥更新
外部 Flash(加密存储) 中 高 需要密钥派生或解密后才能使用

4.3.2 公钥轮换与多级信任链

单把公钥用到底,风险太大。一旦私钥泄露,所有设备都得召回。我建议采用多级证书链:

  • 根公钥:烧录在 ROM 或 OTP 中,永远不变。它只用来验证次级公钥的签名。
  • 次级公钥:存储在可更新区域,由根私钥签名。可以定期轮换。
  • 镜像签名公钥:由次级私钥签名,用于日常镜像验签。

实际经验:我在一个车规级项目里,根公钥烧在 OTP 里,次级公钥每季度轮换一次。即使次级私钥泄露,也只影响当季的镜像,不会波及整个产品生命周期。

4.3.3 公钥分发与更新流程

  1. 出厂烧录:根公钥在芯片出厂时写入 OTP。
  2. 首次启动:Bootloader 用根公钥验证次级公钥的签名,加载次级公钥到安全 RAM。
  3. OTA 更新公钥:新公钥必须由上一级私钥签名,通过安全通道下发。Bootloader 验签通过后,擦除旧公钥,写入新公钥。

关键约束:公钥更新操作必须是原子性的。如果更新过程中掉电,设备可能变成「砖头」。我习惯的做法是:先写入新公钥到备份区域,验签通过后再切换主区域指针。

4.4 签名与验签整体流程(SVG 框架图)

签名与验签整体流程 签名与验签整体流程 签名端(PC / CI) 原始镜像 私钥(离线) SHA-256 摘要 RSA 签名 签名镜像(镜像+签名) OTA / 烧录 验签端(Bootloader) 提取镜像+签名 公钥(OTP/ROM) SHA-256 摘要 RSA 验签 比对摘要 → 通过 / 拒绝 说明: 签名端使用私钥,验签端使用公钥。公钥存储在安全区域(OTP/ROM),私钥离线保管。 验签失败时,Bootloader 应直接停机或进入安全恢复模式,不输出任何调试信息。 关键设计要点 1. 哈希算法必须与签名端一致(SHA-256 是当前最低要求)。 2. 公钥必须防篡改:OTP 熔断或 ROM 硬编码,不可被软件覆盖。 3. 签名必须包含版本号或防回滚计数器,防止旧版本攻击。 4. 验签代码必须位于 Bootloader 不可篡改区域(如 ROM Boot)。

4.5 常见问题与避坑

  • 签名算法选择:RSA-2048 目前够用,但新项目我建议直接上 ECDSA(如 P-256),密钥更短,验签更快。
  • 哈希碰撞:别用 MD5 或 SHA-1。我在 2018 年还见过用 MD5 签名的 IoT 设备……那基本等于没锁门。
  • 公钥硬编码风险:如果公钥写在代码里,攻击者可以通过反编译提取。一定要放在 OTP 或安全存储区。
  • 验签性能:RSA-2048 验签在低端 MCU 上可能耗时几百毫秒。如果启动时间敏感,考虑 ECDSA 或硬件加速。

总结一句话:签名是给镜像上锁,验签是开锁,公钥管理是决定锁芯能不能被换掉。三个环节缺一不可,任何一个出问题,安全启动就形同虚设。