3、信任链的建立:从RoT到Bootloader再到OS的完整信任传递过程
好,咱们接着聊。上一章我们把可信根(RoT)是什么讲清楚了——它就是那个不可篡改的信任起点。但光有一个根没用,你得让这个根能“生长”出信任来,对吧?
说白了,安全启动的核心就是一场接力赛。RoT是第一棒,Bootloader是第二棒,OS是第三棒。每一棒都要验证下一棒的身份和完整性。只要有一棒出了问题,整个系统就不可信了。
我当年做第一个安全启动项目时,犯过一个低级错误——只验证了Bootloader的签名,没验证它要加载的配置分区。结果呢?攻击者篡改了配置,系统虽然启动了,但跑在了攻击者设定的参数上。嗯,从那以后,我养成了一个习惯:信任链上的每一个环节,哪怕是一个字节,都不能放过。
3.1 信任链的总体架构
先看一张图,把整个流程串起来。这张图我画了很多遍,每次给团队新人讲安全启动,我都会先让他们看这个。
你看,整个链条是单向的、不可逆的。RoT验证Bootloader,Bootloader验证OS,OS再验证应用模块。每一级都只信任上一级,没有跨级信任。为什么?因为跨级信任会破坏链式结构,给攻击者留下跳板。
3.2 第一棒:RoT如何验证Bootloader
这是整个信任链最关键的一步。RoT通常是芯片内部的Boot ROM,它出厂就固化在硅片上,物理上无法修改。我习惯把它叫做“硅基信任”。
RoT验证Bootloader的过程,我拆成四个步骤来讲:
- 加载Bootloader镜像:从Flash中读取Bootloader到SRAM
- 验证签名:用eFuse中的公钥哈希验证Bootloader的数字签名
- 验证完整性:计算镜像的哈希值,与签名中的哈希比对
- 移交控制权:验证通过后,跳转到Bootloader入口
核心原则:RoT只做验证,不做执行。它验证通过后就把控制权交出去,自己不再参与后续流程。这样可以减少RoT的代码量,降低出bug的概率。
这里有个细节很多人会忽略——公钥的存储。我见过一个项目,把公钥直接放在Flash里,RoT从Flash读取公钥去验证Bootloader。你想想看,攻击者把Flash里的公钥换成自己的,那整个信任链就崩塌了。正确的做法是把公钥哈希烧进eFuse,RoT用这个哈希去验证公钥的合法性。
3.3 第二棒:Bootloader验证OS
Bootloader拿到控制权后,它的任务就是加载并验证操作系统。这里分两种情况:单阶段Bootloader和多阶段Bootloader。
| 类型 | 特点 | 验证方式 | 典型场景 |
|---|---|---|---|
| 单阶段 | 一个Bootloader完成所有工作 | 直接验证OS签名 | 简单嵌入式系统 |
| 多阶段 | SPL→U-Boot→OS | 逐级验证 | 复杂SoC系统 |
多阶段Bootloader的信任传递是这样的:
- RoT验证SPL(第一阶段Bootloader)
- SPL验证U-Boot(第二阶段Bootloader)
- U-Boot验证OS内核
为什么要搞这么复杂?因为RoT能访问的资源有限——通常只有SRAM,没有DDR。SPL负责初始化DDR,然后加载更大的U-Boot。U-Boot再加载OS。每一级都在扩展能力,同时也在传递信任。
我的经验:多阶段Bootloader的验证链容易出问题的地方是SPL到U-Boot的过渡。SPL验证U-Boot时,要确保验证的是完整的U-Boot镜像,包括它的配置和DTB。我曾经遇到一个案例,SPL只验证了U-Boot的代码段,没验证DTB,结果攻击者篡改DTB后系统照样启动了。
3.4 第三棒:OS验证应用模块
OS内核启动后,信任链并没有结束。内核还要验证它加载的模块、驱动、文件系统。这一步很多人会忽略,觉得内核都起来了,后面应该安全了吧?其实不然。
我参与过一个车载信息娱乐系统的项目。攻击者无法篡改内核,但通过修改一个内核模块的.so文件,成功注入了恶意代码。为什么?因为内核加载模块时没有验证签名。嗯,从那以后,我坚持所有内核模块都必须签名验证。
OS层面的验证通常包括:
- 内核模块签名验证:modprobe时检查模块签名
- 文件系统完整性:dm-verity或IMA机制
- 关键配置验证:/etc下的敏感文件
- 动态加载库验证:dlopen时的签名检查
注意:OS层面的验证不能依赖用户态程序,因为用户态可能已经被篡改。验证逻辑必须在内核态完成,或者使用硬件辅助验证(如TEE)。
3.5 信任链的常见断裂点
做了这么多年安全启动,我总结了几处最容易出问题的地方:
| 断裂点 | 问题描述 | 后果 | 修复方案 |
|---|---|---|---|
| 跳过RoT验证 | 通过JTAG/SWD绕过Boot ROM | 直接加载恶意Bootloader | 禁用调试接口,或设置访问权限 |
| 公钥被替换 | Flash中的公钥被篡改 | 攻击者用自己的密钥签名 | 公钥哈希锁在eFuse中 |
| 验证不完整 | 只验证了部分镜像 | 未验证部分被篡改 | 验证整个镜像,包括元数据 |
| 降级攻击 | 加载旧版本的有漏洞镜像 | 利用已知漏洞 | 版本号检查,禁止回滚 |
| 验证失败后降级 | 签名错误但系统继续启动 | 信任链形同虚设 | 验证失败必须停止启动 |
我曾经在一个客户现场排查问题,发现他们的设备明明有安全启动,但还是被攻破了。查了半天,原因是Bootloader验证OS失败后,没有停止启动,而是打印了一条警告就继续了。你想想看,这跟没验证有什么区别?
3.6 信任链的代码实现示例
光说不练假把式。我写一个简化的信任链验证流程,用伪代码展示核心逻辑:
// RoT阶段:验证Bootloader
void boot_rom_main() {
// 1. 从eFuse读取公钥哈希
uint8_t expected_hash[32];
efuse_read(EFUSE_PUBKEY_HASH, expected_hash, 32);
// 2. 从Flash读取Bootloader镜像和签名
uint8_t* bl_image = flash_read(BOOTLOADER_ADDR);
uint8_t* bl_signature = flash_read(BOOTLOADER_SIG_ADDR);
uint8_t* bl_pubkey = flash_read(BOOTLOADER_PUBKEY_ADDR);
// 3. 验证公钥合法性
uint8_t pubkey_hash[32];
sha256(bl_pubkey, pubkey_hash);
if (memcmp(pubkey_hash, expected_hash, 32) != 0) {
halt_system("Public key mismatch!");
}
// 4. 验证Bootloader签名
if (!rsa_verify(bl_image, bl_signature, bl_pubkey)) {
halt_system("Bootloader signature invalid!");
}
// 5. 验证通过,跳转到Bootloader
jump_to(bl_image);
}
// Bootloader阶段:验证OS内核
void bootloader_main() {
// 1. 从Flash读取内核镜像和签名
uint8_t* kernel = flash_read(KERNEL_ADDR);
uint8_t* kernel_sig = flash_read(KERNEL_SIG_ADDR);
// 2. 使用内置公钥验证内核签名
if (!rsa_verify(kernel, kernel_sig, g_bootloader_pubkey)) {
halt_system("Kernel signature invalid!");
}
// 3. 验证内核完整性(防止部分篡改)
uint8_t kernel_hash[32];
sha256(kernel, kernel_hash);
if (!verify_hash_with_signature(kernel_hash, kernel_sig)) {
halt_system("Kernel integrity check failed!");
}
// 4. 验证通过,启动内核
boot_kernel(kernel);
}
这段代码看起来简单,但实际项目中要考虑的细节很多。比如:
- 签名算法用什么?RSA还是ECDSA?我建议用ECDSA,密钥更短,验证更快
- 哈希算法用什么?SHA256是底线,别用MD5或SHA1
- 公钥怎么存?eFuse空间有限,通常只存哈希,不存完整公钥
- 验证失败怎么处理?必须halt,不能有任何降级路径
一句话总结:信任链的建立,本质上就是“验证→移交→再验证→再移交”的循环。每一环都不可跳过,每一环都不可降级。只有这样,才能从RoT这个信任原点,构建出整个系统的可信运行环境。