3. ATF启动流程总览

安全启动链,说白了就是一条「谁信谁」的链条。芯片上电后,谁第一个跑?谁又来验证下一个?这个链条的起点,就是我们常说的信任根(Root of Trust,RoT)。

我个人习惯把信任根比作「第一把锁」。这把锁是硬件固化的,没人能改。它要是信了不该信的东西,整条链就全崩了。所以,信任根的设计是整个安全架构的基石。

3.1 信任根(RoT)到底是什么?

信任根不是一个软件概念,它是一个物理存在。通常固化在芯片的 BootROM 里,或者通过 eFuse 一次性编程写入。

它的核心职责就两条:

  • 不可篡改:上电后第一个执行的代码,必须是硬件固化的。
  • 不可绕过:任何外部接口都无法跳过它。

我在项目中遇到过一种情况:某款芯片的 BootROM 存在一个 JTAG 调试接口,测试模式下可以跳过 BootROM 直接加载外部镜像。这等于把信任根给「短路」了。后来我们强制在量产时熔断 JTAG 相关的 eFuse,才堵住这个漏洞。

信任根的本质: 硬件保证的「第一口饭」必须是干净的。后续所有代码的合法性,都由它来背书。

3.2 BootROM 加载 BL1

芯片上电后,CPU 会从一个固定的物理地址取指。这个地址指向的就是 BootROM。BootROM 代码很小,通常只有几十 KB,但它干的事很关键。

它的工作流程大致如下:

  1. 硬件初始化:设置时钟、配置内存控制器、初始化存储接口(比如 SPI Flash、eMMC)。
  2. 定位 BL1:从预定义的存储位置(比如 Flash 的固定偏移地址)读取 BL1 镜像到 SRAM 中。
  3. 验证 BL1:用硬件加速引擎(比如 SHA256 + RSA)校验 BL1 的签名。
  4. 跳转执行:验证通过后,CPU 跳转到 BL1 的入口地址。

你想想看,BootROM 本身没有签名验证,因为它就是信任根。它要是坏了,整颗芯片就废了。所以 BootROM 的代码质量要求极高,通常要经过多轮形式化验证。

避坑指南: 我曾经在一个项目里发现,BootROM 加载 BL1 时没有检查镜像长度。攻击者可以构造一个超长的 BL1 镜像,覆盖 SRAM 中的关键数据结构,从而实现代码执行。后来我们在 BootROM 里加上了长度校验和边界检查。

3.3 BL1 验证 BL2 的镜像签名

BL1 被加载到 SRAM 后,它的任务就是找到并验证 BL2。BL2 通常比 BL1 大得多,所以一般存储在外部 Flash 中。

验证流程是这样的:

  • 读取镜像头:BL1 从 Flash 中读取 BL2 的镜像头,里面包含了签名、公钥哈希、镜像大小等信息。
  • 计算哈希:对 BL2 的代码段和数据段计算 SHA256 哈希值。
  • 验签:用预置的公钥(或者从 eFuse 中读取的公钥哈希来验证公钥)对签名进行 RSA 验签。
  • 决定命运:验签通过,跳转执行 BL2;验签失败,进入错误处理流程(比如死循环或重启)。

这里有个细节很多人会忽略:公钥本身怎么保证可信?

嗯,这里要注意。BL1 内部通常固化了一个公钥哈希值(Hash of Public Key)。BL1 先从镜像头中取出公钥,计算它的哈希,跟内部固化的哈希比对。一致了,才用这个公钥去验签。这样就防止了攻击者替换公钥。

重要提醒: 公钥哈希的存储位置必须是 eFuse 或 BootROM,不能放在可写的存储介质里。否则攻击者可以同时替换公钥和签名,直接绕过验证。

3.4 安全启动链的完整流程

我把整个链条画了一张图,方便你理解:

安全启动链流程图 安全启动链:从信任根到 BL2 BootROM (信任根) 加载 BL1 到 SRAM 验证 BL1 签名 验证失败:死循环 验证成功:跳转 BL1 关键点 • 公钥哈希固化在 eFuse • 镜像头包含签名+公钥 • 验签失败不执行任何代码

3.5 签名验证的代码示例

下面是一个简化的 BL1 验证 BL2 签名的伪代码。实际 ATF 中的实现会更复杂,但核心逻辑是一样的。

// BL1 验证 BL2 镜像签名(简化版)
int verify_bl2_image(uint8_t *image_base, uint32_t image_size) {
    image_header_t *header = (image_header_t *)image_base;
    
    // 1. 从镜像头中提取公钥
    uint8_t *public_key = image_base + sizeof(image_header_t);
    
    // 2. 计算公钥的哈希,与 eFuse 中固化的哈希比对
    uint8_t pubkey_hash[SHA256_DIGEST_SIZE];
    sha256_compute(public_key, KEY_SIZE, pubkey_hash);
    
    if (memcmp(pubkey_hash, efuse_pubkey_hash, SHA256_DIGEST_SIZE) != 0) {
        return -1;  // 公钥不匹配,拒绝执行
    }
    
    // 3. 计算镜像的哈希
    uint8_t image_hash[SHA256_DIGEST_SIZE];
    uint8_t *image_data = image_base + header->data_offset;
    uint32_t data_size = header->data_size;
    sha256_compute(image_data, data_size, image_hash);
    
    // 4. 用公钥验签
    uint8_t *signature = image_base + header->signature_offset;
    if (rsa_verify(public_key, image_hash, signature) != 0) {
        return -2;  // 签名验证失败
    }
    
    return 0;  // 验证通过
}

经验之谈: 我建议在验签失败时,不要只是简单地返回错误码。最好直接进入一个不可恢复的死循环,或者触发硬件复位。这样可以防止攻击者通过分析错误码来推断验证逻辑。

3.6 常见攻击手法与防御

攻击手法 原理 防御措施
回滚攻击 加载旧版本的有漏洞镜像 在镜像头中加入版本号,BL1 检查版本号不低于 eFuse 中记录的最低版本
公钥替换 同时替换镜像中的公钥和签名 公钥哈希固化在 eFuse,BL1 先验证公钥再验签
缓冲区溢出 构造超长镜像头覆盖关键数据 严格检查镜像大小和偏移量,禁止越界访问
侧信道攻击 通过功耗或电磁泄露分析密钥 使用常数时间比较函数,避免基于数据的分支

我在做安全审计时,最常见的就是回滚攻击。很多团队只验证签名,不检查版本号。攻击者拿到一个旧版本的有漏洞镜像,照样能通过签名验证。所以,版本号检查一定要做,而且版本号要存储在一次性编程的区域,比如 eFuse。

血的教训: 曾经有一个产品,BL1 验证 BL2 时只检查了签名,没检查镜像大小。攻击者构造了一个超小的镜像,签名验证通过后,BL1 跳转到了未初始化的内存区域,那里残留着攻击者注入的代码。后来我们在验证逻辑里加上了「镜像大小必须等于预期值」的检查。

3.7 小结

安全启动链的核心,就是一条「信任传递」的链条。信任根(BootROM)是起点,它验证 BL1,BL1 验证 BL2,BL2 再验证后续的镜像。每一环都依赖上一环的验证结果。

说白了,只要信任根没被攻破,整条链就是安全的。而信任根的安全,靠的是硬件固化、不可篡改、不可绕过这三个特性。

嗯,这一章的内容就到这里。记住:安全启动链不是银弹,它只是安全架构的第一道防线。后续的章节里,我们会看到 ATF 如何在运行时提供更多的安全机制。