3、固件签名流程实战:使用OpenSSL对固件进行签名、签名验证流程、多级引导中的签名链(Chain of Trust)

好,咱们进入实战环节。前面两章讲了理论基础和威胁模型,这一章咱们直接动手——用 OpenSSL 把固件签上名,再验证回来,最后把这一套串成多级引导的信任链。

说实话,签名这件事本身不复杂。复杂的是你怎么把它嵌到整个启动流程里,让每一级都信任上一级。我见过不少团队,签名工具写得挺溜,结果引导链断了,板子变砖。嗯,咱们今天就把这个闭环走通。

3.1 用 OpenSSL 生成签名密钥对

签名之前,你得先有一对钥匙。私钥你自己藏好,公钥扔到固件里或者烧进芯片的一次性存储里。

我个人习惯用 RSA-4096,虽然 ECC 更快,但 RSA 在老旧 BootROM 里兼容性更好。你想想看,很多 SoC 的 BootROM 是十年前写的,ECC 支持可能是个坑。

# 生成 RSA 私钥,AES-256 加密保护
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 \
    -aes-256-cbc -out firmware_signing_key.pem

# 导出公钥
openssl rsa -pubout -in firmware_signing_key.pem \
    -out firmware_signing_pubkey.pem

# 查看公钥信息
openssl rsa -pubin -in firmware_signing_pubkey.pem -text -noout
⚠️ 我曾经踩过的坑:私钥密码不要写在脚本里。有一次我把密码硬编码在 CI 脚本中,结果脚本被上传到公共仓库…… 虽然马上回滚了,但那几天我都没睡好。用硬件安全模块(HSM)或者密钥管理服务(KMS)才是正道。

3.2 固件签名流程实战

签名不是把整个固件加密,而是对固件的哈希值做签名。这样既快又安全——你想想看,几 MB 的固件用私钥加密得多慢?

标准流程分三步:

  1. 计算固件哈希——通常用 SHA-256 或 SHA-384
  2. 用私钥签名哈希——生成签名数据
  3. 打包——把固件、签名、公钥(或公钥哈希)打包成一个镜像
# 步骤1:计算固件哈希
openssl dgst -sha256 -binary firmware.bin > firmware.hash

# 步骤2:用私钥签名哈希
openssl pkeyutl -sign -inkey firmware_signing_key.pem \
    -in firmware.hash -out firmware.sig

# 步骤3:打包(简单拼接,实际项目会用自定义镜像格式)
cat firmware.bin firmware.sig firmware_signing_pubkey.pem > firmware_signed.bin
💡 我的建议:实际产品中,签名结构通常包含一个头部。头部里写清楚签名算法、哈希算法、公钥索引、固件版本号。这样 BootROM 解析时不用猜,直接读头部就知道怎么验。

3.3 签名验证流程

验证是签名的逆过程。BootROM 拿到固件后,先拆包,再用公钥验签名。这里有个关键点:公钥从哪来?

如果公钥是明文放在固件里的,那攻击者可以替换公钥,用自己的私钥签恶意固件。所以公钥必须来自一个可信源——要么烧死在芯片 OTP 里,要么由上一级引导程序传递。

# 验证流程(模拟 BootROM 侧操作)
# 1. 从镜像中提取公钥、签名、原始固件
# 2. 计算原始固件的哈希
openssl dgst -sha256 -binary extracted_firmware.bin > computed.hash

# 3. 用公钥验证签名
openssl pkeyutl -verify -pubin -inkey extracted_pubkey.pem \
    -sigfile extracted_signature.sig -in computed.hash

# 如果输出 "Signature Verified Successfully",说明固件未被篡改

验证失败时怎么办?

  • 如果签名验证失败,BootROM 必须拒绝启动——不能 fallback 到未签名固件
  • 我建议设计一个恢复模式:如果主镜像验签失败,尝试从恢复分区加载备份镜像
  • 千万不要在验证失败后继续执行,哪怕只是打印错误——攻击者可以利用这个窗口

3.4 多级引导中的签名链(Chain of Trust)

单级签名好理解,但实际嵌入式设备通常是多级引导:BootROM → SPL → U-Boot → Kernel → 文件系统。每一级都要验证下一级,这就形成了信任链。

说白了,信任链的核心思想是:每一级只信任它自己的公钥,然后用这个公钥验证下一级的签名。 只要第一级(BootROM)是可信的,整个链就安全。

下面这张图展示了典型的四级信任链结构:

多级引导签名链(Chain of Trust) 多级引导签名链(Chain of Trust) 第1级:BootROM(硬件信任根) 公钥烧死在 OTP,不可更改 验证签名 第2级:SPL / 预引导加载器 携带 BootROM 签名的公钥哈希 验证签名 第3级:U-Boot / 引导加载器 携带 SPL 签名的公钥 验证签名 第4级:Kernel + 文件系统 携带 U-Boot 签名的公钥 🔑 信任链关键点 每一级只信任上一级传递下来的公钥,并用它验证下一级的签名。 如果任何一级验证失败,整个启动过程必须终止。

3.5 信任链的工程实现细节

理论讲完了,咱们看看实际代码里怎么实现。以 U-Boot 验证 Kernel 为例:

// U-Boot 中验证 Kernel 镜像的伪代码
int verify_kernel_image(void *kernel_addr, size_t kernel_size) {
    // 1. 从固定偏移读取签名和公钥
    signature_t *sig = (signature_t *)(kernel_addr + kernel_size);
    public_key_t *pubkey = (public_key_t *)(kernel_addr + kernel_size + sizeof(signature_t));
    
    // 2. 检查公钥是否匹配 U-Boot 内置的信任公钥哈希
    uint8_t pubkey_hash[SHA256_DIGEST_LENGTH];
    sha256(pubkey, sizeof(public_key_t), pubkey_hash);
    
    if (memcmp(pubkey_hash, trusted_pubkey_hash, SHA256_DIGEST_LENGTH) != 0) {
        printf("ERROR: Public key not trusted!\n");
        return -1;  // 拒绝启动
    }
    
    // 3. 验证签名
    uint8_t image_hash[SHA256_DIGEST_LENGTH];
    sha256(kernel_addr, kernel_size, image_hash);
    
    int ret = rsa_verify(pubkey, sig, image_hash);
    if (ret != 0) {
        printf("ERROR: Signature verification failed!\n");
        return -2;  // 拒绝启动
    }
    
    printf("Kernel image verified successfully.\n");
    return 0;
}
⚠️ 这里有个容易忽略的细节:公钥哈希的存储位置。BootROM 里的公钥哈希必须烧在 OTP 里,而且 OTP 要锁定写入。我见过一个项目,公钥哈希存在 SPI Flash 里,结果攻击者直接替换了 Flash 内容…… 那签名链就形同虚设了。

3.6 签名链的常见攻击面与防御

信任链不是万能的。攻击者会盯着链上的薄弱环节。我整理了一个表格,列出最常见的攻击手法和对应的防御措施:

攻击面 攻击手法 防御措施
回滚攻击 攻击者用旧版本(有已知漏洞)的已签名固件替换新固件 在签名结构中包含版本号,BootROM 检查版本号是否递增
公钥替换 攻击者替换固件包中的公钥,用自己的私钥签名 公钥哈希烧死在 OTP,BootROM 只信任 OTP 中的哈希
中间人攻击 在固件传输过程中篡改数据 签名验证天然防篡改,但要注意传输通道也要加密
侧信道攻击 通过功耗、电磁辐射分析私钥 使用恒定时间比较函数,避免基于数据的分支
BootROM 漏洞 利用 BootROM 本身的缓冲区溢出等漏洞 选择经过安全审计的 BootROM,定期更新芯片
💡 我曾经处理过一个回滚攻击案例:客户的产品在 OTA 升级时没有检查版本号,攻击者把固件回滚到三年前的版本,那个版本有一个已知的调试接口漏洞。修复方案就是在签名头部加入版本号字段,并在 BootROM 中增加版本号单调递增检查。从那以后,我所有项目的签名结构里都强制包含版本号。

3.7 实战:搭建一个完整的签名链测试环境

纸上谈兵没意思。咱们用 QEMU 模拟一个简单的双级引导环境,跑通签名链:

  1. 生成两级密钥对:BootROM 密钥对 + U-Boot 密钥对
  2. 签名 U-Boot 镜像:用 BootROM 私钥签名 U-Boot
  3. 签名 Kernel 镜像:用 U-Boot 私钥签名 Kernel
  4. 模拟验证:写一个测试程序模拟 BootROM 验证 U-Boot,U-Boot 验证 Kernel
#!/bin/bash
# 签名链测试脚本(双级)

# 1. 生成密钥对
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
    -out bootrom_key.pem
openssl rsa -pubout -in bootrom_key.pem -out bootrom_pubkey.pem

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
    -out uboot_key.pem
openssl rsa -pubout -in uboot_key.pem -out uboot_pubkey.pem

# 2. 签名 U-Boot(用 BootROM 私钥)
openssl dgst -sha256 -binary uboot.bin > uboot.hash
openssl pkeyutl -sign -inkey bootrom_key.pem \
    -in uboot.hash -out uboot.sig

# 3. 签名 Kernel(用 U-Boot 私钥)
openssl dgst -sha256 -binary kernel.bin > kernel.hash
openssl pkeyutl -sign -inkey uboot_key.pem \
    -in kernel.hash -out kernel.sig

# 4. 打包(实际项目会使用 FIT 或自定义格式)
# 这里只是演示结构
echo "BootROM 验证 U-Boot:"
openssl pkeyutl -verify -pubin -inkey bootrom_pubkey.pem \
    -sigfile uboot.sig -in uboot.hash && echo "OK" || echo "FAIL"

echo "U-Boot 验证 Kernel:"
openssl pkeyutl -verify -pubin -inkey uboot_pubkey.pem \
    -sigfile kernel.sig -in kernel.hash && echo "OK" || echo "FAIL"

测试结果解读:

  • 如果两个验证都返回 "OK",说明信任链建立成功
  • 如果篡改 uboot.bin 或 kernel.bin,验证会失败——这就是签名链的保护效果
  • 你可以试试替换公钥,看看验证是否会失败——这是对公钥替换攻击的模拟

好了,这一章的内容就到这。签名链的核心就一句话:信任从硬件开始,一级一级传递下去,每一级只信上一级给的东西。 你把这个逻辑理清楚,代码实现就是水到渠成的事。