25、安全启动(Secure Boot):签名验证与信任根

安全启动,说白了就是给嵌入式系统装上一道“防盗门”。

我刚开始接触这个领域时,觉得它就是个软件层面的花架子。直到有一次,我在一个工业控制项目里,亲眼看到一台设备被刷入了恶意固件,整个产线停了三天。嗯,从那以后,我再也不敢轻视安全启动。

25.1 什么是安全启动?

安全启动的核心逻辑其实很简单:只运行经过签名的、可信的代码。从芯片上电的第一条指令开始,每一级代码都要验证下一级代码的合法性。一旦发现签名不对,系统就拒绝启动。

你想想看,如果没有这个机制,攻击者只要拿到物理访问权限,就能把恶意固件刷进去。你的设备就成了别人的“肉鸡”。

安全启动的三大目标:

  • 完整性:确保代码没有被篡改
  • 真实性:确保代码来自可信的发布者
  • 链式信任:从硬件到应用层,每一级都可信

25.2 信任根(Root of Trust)

信任根是整个安全启动的基石。它通常是固化在芯片内部的一段只读代码,也叫 BootROM。这段代码在芯片制造时就被写死了,无法修改。

我个人习惯把信任根比作“保险箱的钥匙”。你所有的安全措施,最终都要依赖这把钥匙。如果钥匙本身有问题,那整个安全体系就崩塌了。

信任根必须满足的条件:

  • 不可篡改:通常存储在 ROM 或 OTP(一次性可编程)存储器中
  • 不可绕过:芯片复位后,第一条指令必须来自信任根
  • 最小化:代码量越少越好,减少攻击面

25.3 签名验证流程

安全启动的签名验证,本质上是一个非对称加密的过程。我画了一张流程图,帮你理清整个链条:

安全启动签名验证流程 信任根(BootROM) 加载第一级 Bootloader(SPL) 从 Flash 或 eMMC 读取 验证签名 启动失败 验证通过 加载下一级 Bootloader 或操作系统内核 重复验证直到应用层 公钥(OTP/efuse) 一次性编程,不可修改 提供公钥

这张图展示的是典型的链式验证过程。信任根先验证第一级 Bootloader,第一级 Bootloader 再验证第二级,以此类推。每一级都只信任上一级。

25.4 签名与验证的底层实现

在实际的 C 代码层面,签名验证通常涉及哈希计算和 RSA/ECDSA 验签。我拿一个简化版的验签流程给你看:

/* 简化版签名验证流程 */
#include <stdint.h>
#include <string.h>

/* 假设我们使用 SHA-256 和 RSA-2048 */
#define HASH_SIZE    32
#define SIGNATURE_SIZE  256

/* 公钥结构体(通常存储在 OTP 或 efuse 中) */
typedef struct {
    uint8_t modulus[256];   /* N */
    uint32_t exponent;      /* e,通常为 65537 */
} rsa_pub_key_t;

/* 从安全存储读取公钥 */
int secure_read_pub_key(rsa_pub_key_t *key) {
    /* 实际项目中,这里会从 OTP 或 efuse 读取 */
    /* 我曾经踩过一个坑:OTP 读取时序不对,导致公钥全为 0xFF */
    /* 结果所有固件都能通过验证... 那真是灾难 */
    memcpy(key->modulus, (void*)OTP_BASE, 256);
    key->exponent = 65537;
    return 0;
}

/* 验证固件签名 */
int verify_firmware(const uint8_t *firmware, uint32_t fw_size,
                    const uint8_t *signature, const rsa_pub_key_t *pub_key) {
    uint8_t hash[HASH_SIZE];
    
    /* 1. 计算固件的哈希值 */
    sha256_hash(firmware, fw_size, hash);
    
    /* 2. RSA 验签 */
    /* 这里调用硬件加速器或软件实现 */
    if (rsa_verify(hash, HASH_SIZE, signature, pub_key) != 0) {
        return -1;  /* 签名无效 */
    }
    
    return 0;  /* 验证通过 */
}

/* 安全启动主流程 */
int secure_boot(void) {
    rsa_pub_key_t pub_key;
    uint8_t *firmware_ptr;
    uint32_t fw_size;
    uint8_t *signature_ptr;
    
    /* 读取公钥 */
    if (secure_read_pub_key(&pub_key) != 0) {
        return -1;  /* 信任根损坏 */
    }
    
    /* 从 Flash 加载固件和签名 */
    load_firmware_from_flash(&firmware_ptr, &fw_size, &signature_ptr);
    
    /* 验证签名 */
    if (verify_firmware(firmware_ptr, fw_size, signature_ptr, &pub_key) != 0) {
        /* 验证失败,进入安全模式或死循环 */
        while(1);  /* 锁死系统 */
    }
    
    /* 跳转到固件入口 */
    jump_to_firmware(firmware_ptr);
    
    return 0;
}

⚠️ 重要提醒:

  • 公钥必须存储在一次性可编程存储器中,比如 efuse 或 OTP。存储在 Flash 里等于没存。
  • 验签过程必须在信任根内部完成,不能依赖外部代码。
  • 我曾经见过一个产品,公钥存储在 SPI Flash 里,结果攻击者直接替换了 Flash 芯片... 嗯,那款产品后来召回了。

25.5 链式信任的层级结构

一个典型的安全启动链,通常包含以下几个层级:

层级 代码 存储位置 验证者
第0级 BootROM(信任根) 芯片内部 ROM 硬件固化,不可修改
第1级 SPL(Secondary Program Loader) Flash 或 eMMC BootROM 验证
第2级 U-Boot 或类似 Bootloader Flash 或 eMMC SPL 验证
第3级 操作系统内核 Flash 或 eMMC Bootloader 验证
第4级 应用程序 文件系统 内核验证(可选)

每一级都只验证下一级的签名。如果某一级被攻破,它后面的所有层级都不再可信。所以,保护信任根是第一要务。

25.6 实际项目中的避坑指南

我在几个量产项目里摸爬滚打后,总结了一些血泪教训:

避坑指南:

  • 不要用软件实现 RSA 验签:太慢了。一个 2048 位的 RSA 验签,纯软件可能要几百毫秒。一定要用硬件加速器。
  • 注意回滚攻击:攻击者可能拿一个旧版本的、有漏洞的固件来刷。我建议在签名中加入版本号,并在 BootROM 中检查版本是否高于当前版本。
  • 调试接口要封死:JTAG/SWD 接口在生产后必须熔断。否则攻击者可以直接通过调试接口绕过安全启动。
  • 密钥管理要谨慎:签名用的私钥必须离线存储,不能放在构建服务器上。我曾经见过一个团队把私钥放在 Git 仓库里... 嗯,那画面太美我不敢看。

25.7 安全启动的局限性

安全启动不是万能的。它只能保证代码在启动时没有被篡改,但无法防御运行时的攻击。比如:

  • 缓冲区溢出攻击(安全启动管不了)
  • 侧信道攻击(比如通过功耗分析窃取密钥)
  • 物理攻击(比如用探针直接读取 OTP 内容)

所以,安全启动只是整个安全体系中的一环。你还需要配合内存保护单元(MPU)、可信执行环境(TEE)等措施,才能构建一个相对安全的系统。

总结一句话:安全启动的核心是信任根 + 链式验证。信任根必须不可篡改,验证过程必须逐级进行。别想着偷懒跳过某一级,攻击者会感谢你的。

公众号:蓝海资料掘金营,微信 deep3321