4. BootROM 设计:不可篡改的第一阶段启动代码

BootROM,说白了就是芯片出厂时固化在 ROM 里的一小段程序。它不可修改,不可擦除,是整个安全启动链条的「信任根」。我经常跟团队里的新人说:BootROM 要是出了问题,后面所有的安全措施都是白搭。

为什么?因为它是系统上电后执行的第一段代码。它的职责、它的限制,直接决定了整个设备的安全边界。今天我们就来聊聊这个「第一关」该怎么设计。

4.1 BootROM 的核心职责

BootROM 的任务其实不多,但每一条都至关重要。我把它归纳为四个核心职责:

职责 说明 关键要求
硬件初始化 配置时钟、内存控制器、基本外设 最小化、可预测、无依赖
安全根建立 验证下一阶段代码(通常是 SPL 或 BL1)的签名 公钥必须固化在 ROM 或 OTP 中
启动介质检测 按优先级扫描 SPI Flash、eMMC、UART 等启动源 顺序固定,不可被篡改
故障处理 校验失败时进入安全状态(如死循环、报警) 不允许回退到不安全模式

嗯,这里要注意:BootROM 的代码量通常被限制在 4KB 到 32KB 之间。为什么这么小?因为 ROM 面积很贵,而且 BootROM 越复杂,出 bug 的概率就越高。我见过一个项目,BootROM 里塞了完整的 USB 协议栈,结果一次 USB 枚举失败导致整批设备变砖——教训深刻。

4.2 不可篡改的设计原则

「不可篡改」不是一句口号,它需要从物理和逻辑两个层面来保证。我个人习惯把设计原则总结成下面这张图:

BootROM 不可篡改设计原则 BootROM 信任根 物理防护 ROM 只读 / 熔丝锁定 逻辑防护 签名链 / 哈希锁定 最小化原则 代码精简 / 无冗余功能 可验证性 JTAG 熔断 / 调试接口锁定 四个维度缺一不可:物理防篡改 + 逻辑防绕过 + 代码最小化 + 接口可验证 任何一环被突破,整个信任链都会崩塌

你想想看,如果攻击者能物理读取 ROM 内容,或者通过 JTAG 绕过 BootROM 的执行,那签名校验做得再好也没用。所以,物理防护是第一道防线。

4.3 BootROM 的典型执行流程

我参与过的一个车规级 MCU 项目,BootROM 的执行流程大致如下。这个流程几乎成了行业标准模板:

  1. 上电复位:CPU 从复位向量地址(通常是 0x00000000)取指,开始执行 BootROM。
  2. 最小硬件初始化:配置 CPU 时钟、关闭看门狗、初始化堆栈指针。这一步要快,我一般控制在 200 个 CPU 周期以内。
  3. 启动介质检测:按固定优先级扫描启动源。优先级顺序通常固化在 ROM 里,不可更改。
  4. 加载下一阶段代码:从检测到的启动介质中读取 SPL 或 BL1 到 SRAM。
  5. 签名校验:用 ROM 中固化的公钥验证加载代码的签名。校验通过才跳转执行。
  6. 校验失败处理:进入安全错误状态,比如死循环、输出错误码、或者触发硬件报警。

关键点:BootROM 必须保证在签名校验通过之前,绝不跳转到外部代码执行。我曾经见过一个设计,BootROM 先跳转到外部 Flash 执行一段「自检代码」,然后再回来做签名校验——这等于把大门钥匙先交给了别人。

4.4 公钥的存储与保护

BootROM 做签名校验,总得有个公钥吧?这个公钥放哪?怎么保证它不被替换?

常见的做法有两种:

方案 存储位置 优点 缺点
ROM 固化 公钥直接编码在 ROM 掩模中 不可更改,物理安全 无法更新,一旦泄露永久失效
OTP 存储 公钥写入一次性可编程存储器 可烧录不同密钥,灵活 需要额外的 OTP 区域,成本略高
混合方案 ROM 中存根密钥,OTP 存覆盖密钥 兼顾安全与灵活性 设计复杂,需要 fallback 逻辑

我个人更推荐 OTP 方案。为什么?因为 ROM 固化密钥一旦在流片后被破解,整批芯片都得报废。而 OTP 可以在产线烧录时写入,即使某批次密钥泄露,也只影响该批次。

注意:OTP 区域必须设置硬件写保护,确保 BootROM 执行期间无法被意外写入。我遇到过一起事故:某个芯片的 OTP 控制寄存器默认是未锁定的,结果一段有 bug 的测试代码在 BootROM 执行时把 OTP 区域覆盖了——整批芯片的公钥全变成了 0x00。

4.5 BootROM 的限制与避坑

BootROM 不是万能的。它的限制主要来自三个方面:

  • 空间限制:ROM 面积很贵,BootROM 通常只有 4KB~32KB。这意味着你不能在里面塞复杂的算法。比如完整的 RSA-4096 验签可能就要占用 8KB 代码空间,你还要留空间给启动介质驱动、硬件初始化等。我一般建议用 ECDSA 或 Ed25519,签名短、验签快。
  • 性能限制:BootROM 通常在低频模式下运行(因为 PLL 还没配置好),验签速度可能很慢。一个 2048 位的 RSA 验签在 100MHz 的 Cortex-M 上可能要跑几百毫秒——这对某些启动时间要求苛刻的场景(比如汽车气囊控制器)是不可接受的。
  • 调试限制:BootROM 一旦流片,里面的 bug 就永远修不了。所以设计时一定要做充分的仿真验证。我习惯在 BootROM 里留一个「调试后门」——通过特定的物理引脚序列进入调试模式,但这个后门在量产时必须通过熔丝永久关闭。

我的经验:BootROM 的代码最好用纯汇编编写,或者至少是汇编 + 极小部分 C。为什么?因为 C 编译器的启动代码本身就需要依赖一些初始化条件,而 BootROM 执行时这些条件可能还不满足。用汇编可以精确控制每一条指令的执行顺序和时序。

4.6 一个实际的 BootROM 验签代码片段

下面是一个简化版的 BootROM 验签流程伪代码。实际项目中会复杂得多,但核心逻辑就是这样:

// BootROM 主流程(伪代码)
void bootrom_main(void) {
    // 1. 最小硬件初始化
    init_cpu_clock();       // 配置 CPU 时钟为内部 RC 振荡器
    init_stack_pointer();   // 设置堆栈指针
    disable_watchdog();     // 关闭看门狗,防止复位

    // 2. 检测启动介质
    boot_source_t src = detect_boot_source();
    if (src == BOOT_SOURCE_NONE) {
        enter_error_loop(ERROR_NO_BOOT_SOURCE);
    }

    // 3. 加载下一阶段代码到 SRAM
    uint8_t *load_addr = (uint8_t *)SRAM_BASE;
    uint32_t load_size = load_image(src, load_addr, MAX_IMAGE_SIZE);
    if (load_size == 0) {
        enter_error_loop(ERROR_LOAD_FAILED);
    }

    // 4. 从 OTP 读取公钥
    uint8_t pubkey[PUBKEY_SIZE];
    otp_read(OTP_PUBKEY_OFFSET, pubkey, PUBKEY_SIZE);

    // 5. 验签
    // 假设镜像头部包含签名和镜像大小信息
    image_header_t *hdr = (image_header_t *)load_addr;
    if (!verify_signature(pubkey, load_addr + sizeof(image_header_t),
                          hdr->image_size, hdr->signature)) {
        enter_error_loop(ERROR_SIGNATURE_FAILED);
    }

    // 6. 跳转到下一阶段代码
    jump_to(load_addr + sizeof(image_header_t));
}

这段代码看起来简单,但每个函数背后都有大量细节。比如 otp_read() 必须确保 OTP 控制器处于安全状态,verify_signature() 必须使用常数时间比较防止时序攻击——这些都是实战中容易踩的坑。

4.7 小结

BootROM 是整个安全启动的基石。它不可篡改,但也不是万能的。设计时要在 空间、性能、安全、灵活性 之间找到平衡。记住:BootROM 的职责是建立信任根,而不是解决所有安全问题。把信任根守住了,后面的安全链条才能立起来。