25、安全启动(Secure Boot):签名验证与信任根
安全启动,说白了就是给嵌入式系统装上一道“防盗门”。
我刚开始接触这个领域时,觉得它就是个软件层面的花架子。直到有一次,我在一个工业控制项目里,亲眼看到一台设备被刷入了恶意固件,整个产线停了三天。嗯,从那以后,我再也不敢轻视安全启动。
25.1 什么是安全启动?
安全启动的核心逻辑其实很简单:只运行经过签名的、可信的代码。从芯片上电的第一条指令开始,每一级代码都要验证下一级代码的合法性。一旦发现签名不对,系统就拒绝启动。
你想想看,如果没有这个机制,攻击者只要拿到物理访问权限,就能把恶意固件刷进去。你的设备就成了别人的“肉鸡”。
安全启动的三大目标:
- 完整性:确保代码没有被篡改
- 真实性:确保代码来自可信的发布者
- 链式信任:从硬件到应用层,每一级都可信
25.2 信任根(Root of Trust)
信任根是整个安全启动的基石。它通常是固化在芯片内部的一段只读代码,也叫 BootROM。这段代码在芯片制造时就被写死了,无法修改。
我个人习惯把信任根比作“保险箱的钥匙”。你所有的安全措施,最终都要依赖这把钥匙。如果钥匙本身有问题,那整个安全体系就崩塌了。
信任根必须满足的条件:
- 不可篡改:通常存储在 ROM 或 OTP(一次性可编程)存储器中
- 不可绕过:芯片复位后,第一条指令必须来自信任根
- 最小化:代码量越少越好,减少攻击面
25.3 签名验证流程
安全启动的签名验证,本质上是一个非对称加密的过程。我画了一张流程图,帮你理清整个链条:
这张图展示的是典型的链式验证过程。信任根先验证第一级 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)等措施,才能构建一个相对安全的系统。
总结一句话:安全启动的核心是信任根 + 链式验证。信任根必须不可篡改,验证过程必须逐级进行。别想着偷懒跳过某一级,攻击者会感谢你的。