3. 安全启动流程:ROM 代码、BootROM、信任链建立、安全固件加载
安全启动,说白了就是系统上电后的第一道防线。我做了这么多年嵌入式安全,见过太多因为启动阶段被攻破而全盘皆输的项目。你想想看,如果连最开始的启动代码都不安全,后面再强的加密算法也白搭。
这一章我们重点聊聊 TrustZone 体系下的安全启动流程。我会从最底层的 ROM 代码讲起,一步步带你走完整个信任链的建立过程。
3.1 ROM 代码:不可更改的信任根
芯片一上电,CPU 首先执行的是 ROM 里的代码。这部分代码是芯片出厂时固化的,物理上无法修改。为什么这么做?因为这是整个信任链的起点,必须绝对可靠。
ROM 代码的任务其实很简单:
- 初始化最基本的硬件(时钟、电源、内存控制器)
- 验证下一级 BootROM 的完整性
- 如果验证通过,跳转到 BootROM 执行
核心要点:ROM 代码本身不需要验证,因为它不可更改。这就是所谓的「信任根」(Root of Trust)。
我记得有一次帮客户调试启动问题,发现 ROM 代码在执行时总是卡在某个校验点。查了半天,原来是芯片的 OTP(一次性可编程)区域被意外写入了错误的值。嗯,这里要提醒大家:OTP 的烧录一定要在产线上做严格校验,否则芯片直接变砖。
3.2 BootROM:第一级启动加载器
BootROM 是 ROM 代码验证通过后加载的第一段可执行程序。它通常存储在芯片内部的 SRAM 或特定的 Flash 区域。
BootROM 的主要职责包括:
- 硬件初始化:配置 DDR 控制器、时钟树、中断控制器等
- 安全校验:验证下一级固件(通常是 BL2 或类似组件)的数字签名
- 加载执行:将验证通过的固件加载到安全内存中并跳转
个人经验:我在设计 BootROM 时,习惯把校验算法放在最前面执行。这样即使后续代码有漏洞,攻击者也很难绕过签名验证。说白了,就是「先验证,后信任」。
3.3 信任链的建立过程
信任链的建立,本质上是一个「逐级验证、逐级授权」的过程。每一级代码只信任上一级,并且只负责验证下一级。
典型的信任链如下:
| 层级 | 组件 | 存储位置 | 验证方式 |
|---|---|---|---|
| 第0级 | ROM 代码 | 芯片内部 ROM | 物理不可更改 |
| 第1级 | BootROM | 内部 SRAM/Flash | ROM 代码验证其哈希 |
| 第2级 | 安全固件(BL2) | 外部 Flash | BootROM 验证其签名 |
| 第3级 | 安全操作系统(如 OP-TEE) | 外部 Flash | BL2 验证其签名 |
| 第4级 | 普通世界固件(如 U-Boot) | 外部 Flash | 安全固件验证其签名 |
为什么会这样设计?因为每一级只做一件事,复杂度可控。我曾经见过一个项目,把所有的验证逻辑都塞进 BootROM,结果 ROM 空间不够,还导致启动时间长达 10 秒。后来拆成多级,每级只验证下一级,问题就解决了。
3.4 安全固件加载的细节
安全固件加载,这里指的是将经过签名的固件从非安全存储(比如外部 SPI Flash)加载到安全内存中。这个过程有几个关键点:
- 签名验证:使用公钥验证固件的数字签名。公钥通常存储在 OTP 或 eFuse 中。
- 哈希校验:验证固件的哈希值是否与签名中的一致。
- 安全加载:通过 TrustZone 的 TZASC(TrustZone Address Space Controller)确保加载过程不会被普通世界干扰。
避坑指南:我曾经遇到过一个案例,固件签名验证通过了,但加载到内存后执行时却崩溃。查了三天才发现,是固件在加载过程中被 DMA 控制器偷偷篡改了。解决方案很简单:在加载完成后,再做一次内存完整性校验。
下面是一个简化的安全固件加载流程伪代码:
// 安全固件加载流程(简化版)
void secure_firmware_load(uint32_t *fw_addr, uint32_t fw_size) {
// 1. 读取固件头部
fw_header_t *header = (fw_header_t *)fw_addr;
// 2. 验证签名
if (!verify_signature(header->signature, fw_addr + sizeof(fw_header_t), fw_size)) {
panic("Signature verification failed!");
}
// 3. 计算哈希并比对
uint8_t hash[SHA256_SIZE];
sha256_compute(fw_addr + sizeof(fw_header_t), fw_size, hash);
if (memcmp(hash, header->expected_hash, SHA256_SIZE) != 0) {
panic("Hash mismatch!");
}
// 4. 加载到安全内存
uint32_t *secure_mem = (uint32_t *)TZ_DRAM_BASE;
memcpy(secure_mem, fw_addr, fw_size);
// 5. 再次校验内存完整性
sha256_compute(secure_mem, fw_size, hash);
if (memcmp(hash, header->expected_hash, SHA256_SIZE) != 0) {
panic("Memory integrity check failed!");
}
// 6. 跳转到安全固件入口
jump_to_entry(secure_mem + header->entry_offset);
}
3.5 实际项目中的注意事项
最后,我结合自己的项目经验,给大家几点建议:
- 公钥存储要安全:公钥一旦泄露,攻击者可以伪造固件。建议使用 eFuse 或 OTP,并且烧录后锁定。
- 启动时间要控制:每级验证都会增加启动时间。我建议在开发阶段用完整校验,量产时可以根据安全等级适当简化。
- 回滚保护:防止攻击者加载旧版本的固件(可能有已知漏洞)。可以在固件头部加入版本号,并在 BootROM 中检查。
- 调试接口要关闭:量产芯片一定要关闭 JTAG/SWD 等调试接口,否则攻击者可以直接读取内存。
总结一下:安全启动的核心就是「信任链」。从 ROM 代码开始,每一级只信任上一级,并且只验证下一级。只要根是安全的,整个链条就是安全的。嗯,这个道理说起来简单,但真正实现好,需要你对硬件、软件、密码学都有深入的理解。
下一章我们会聊聊 TrustZone 的内存隔离机制,也就是如何通过 TZASC 和 TZMA 来保护安全世界的内存区域。到时候我会分享一个我在调试内存访问权限时遇到的经典 bug,保证让你印象深刻。