第4章:BL1阶段安全启动:BL1的职责、BL1如何验证BL2的签名

好,我们进入正题。BL1,说白了就是整个ATF安全启动链条的“第一块多米诺骨牌”。它要是倒了,后面全白搭。我在做第一个安全启动项目时,就吃过这个亏——当时觉得BL1简单,没仔细抠它的验证逻辑,结果后面BL2加载时老是校验失败,查了三天才发现是BL1里一个哈希比对的小bug。嗯,从那以后,我对BL1的敬畏心就上来了。

4.1 BL1到底在忙什么?

BL1是芯片上电后执行的第一个可信代码。它通常存放在片上ROM里,不可修改。它的职责,我总结为三件事:

  • 初始化硬件:把CPU、内存控制器、定时器这些基础外设搞起来。注意,这里只做最小初始化,够用就行。
  • 加载BL2镜像:从存储介质(比如QSPI Flash、eMMC)里把BL2的二进制数据读到SRAM里。
  • 验证BL2签名:这是核心。用公钥验证BL2的数字签名,确保它没被篡改过。

你想想看,如果BL1本身不可信,那后面所有安全机制都是空中楼阁。所以BL1必须放在ROM里,物理上防篡改。

4.2 签名验证的完整流程

我个人习惯把BL1验证BL2的过程拆成四个步骤。每一步都有坑,我一个个说。

  1. 获取公钥:BL1里硬编码了一个公钥哈希值(不是公钥本身)。它先从Flash里读出完整的公钥,然后计算这个公钥的哈希,跟硬编码的哈希比对。匹配了,才用这个公钥去验签。
  2. 读取BL2镜像和签名:BL2镜像通常包含三部分:镜像头、镜像数据、签名值。BL1需要把它们都读到内存里。
  3. 计算镜像哈希:对BL2的镜像数据(不含签名)做SHA-256哈希计算。
  4. 验签:用步骤1得到的公钥,对步骤3的哈希值和步骤2的签名值做RSA验签。通过,则BL2可信;不通过,直接死循环或复位。

这里有个关键点:公钥的完整性保护。公钥本身不能明文存,否则攻击者可以替换公钥。所以BL1里只存公钥的哈希,相当于给公钥加了个“指纹锁”。

核心逻辑:信任链的根是BL1里的公钥哈希。这个哈希值在芯片制造时烧录进ROM,无法更改。只要这个根是安全的,整个链条就安全。

4.3 代码层面的实现要点

我们来看看ATF里BL1验证BL2的代码骨架。我简化了一下,但核心逻辑都在。

/* BL1验证BL2的伪代码 */
int bl1_verify_bl2(void) {
    /* 1. 从Flash读取公钥 */
    uint8_t pub_key[PUB_KEY_SIZE];
    read_from_flash(PUB_KEY_OFFSET, pub_key, PUB_KEY_SIZE);

    /* 2. 计算公钥哈希,与ROM中硬编码的哈希比对 */
    uint8_t pub_key_hash[HASH_SIZE];
    sha256_compute(pub_key, PUB_KEY_SIZE, pub_key_hash);
    if (memcmp(pub_key_hash, rom_pub_key_hash, HASH_SIZE) != 0) {
        return -1;  /* 公钥被篡改 */
    }

    /* 3. 读取BL2镜像头和签名 */
    bl2_header_t header;
    read_from_flash(BL2_HEADER_OFFSET, &header, sizeof(header));

    uint8_t signature[SIGNATURE_SIZE];
    read_from_flash(BL2_SIGNATURE_OFFSET, signature, SIGNATURE_SIZE);

    /* 4. 读取BL2镜像数据 */
    uint8_t *bl2_image = malloc(header.image_size);
    read_from_flash(BL2_IMAGE_OFFSET, bl2_image, header.image_size);

    /* 5. 计算镜像哈希 */
    uint8_t image_hash[HASH_SIZE];
    sha256_compute(bl2_image, header.image_size, image_hash);

    /* 6. RSA验签 */
    if (rsa_verify(pub_key, image_hash, HASH_SIZE, signature) != 0) {
        return -2;  /* 签名无效 */
    }

    return 0;  /* 验证通过 */
}

这段代码里,我特别想强调两点:

  • 公钥哈希比对失败:直接返回-1,不要做任何后续操作。我曾经见过一个实现,公钥哈希比对失败后还继续往下走,结果攻击者用自己生成的公钥和签名就绕过了验证。
  • 内存分配:BL2镜像可能很大,用malloc要小心。在BL1阶段,堆空间通常很小,我建议用静态缓冲区或者直接从Flash地址映射。

4.4 签名方案的选型

ATF支持多种签名算法,我列个表,方便你对比。

算法 密钥长度 签名大小 验证速度 安全性
RSA-2048 2048 bits 256 bytes 较慢 高
RSA-4096 4096 bits 512 bytes 很慢 极高
ECDSA-P256 256 bits 64 bytes 快 高
ECDSA-P384 384 bits 96 bytes 较快 极高

我个人建议:如果芯片有硬件加密引擎,优先用ECDSA-P256。验证速度快,签名小,对BL1这种资源受限的环境很友好。如果芯片没有硬件加速,RSA-2048也够用,就是慢点。

避坑指南:我曾经在一个项目里选了RSA-4096,结果BL1验证BL2花了将近2秒,导致整个启动时间超标。后来换成ECDSA-P256,验证时间降到200毫秒以内。所以,选型时一定要考虑启动时间预算。

4.5 流程图:BL1验证BL2的完整链路

下面这张图,我画了BL1从启动到验证BL2的完整流程。你跟着箭头走一遍,应该就全明白了。

BL1验证BL2流程图 芯片上电复位 BL1初始化硬件 从Flash读取公钥 验证公钥哈希 读取BL2镜像和签名 RSA/ECDSA验签 跳转到BL2执行

这张图里,我特意把公钥验证和镜像验签用不同颜色区分了。橙色是公钥处理,绿色是镜像处理。为什么这么分?因为公钥验证失败和镜像验签失败,处理方式完全不同——公钥失败说明整个信任链的根被破坏了,必须死机;镜像验签失败,可能只是镜像损坏,可以尝试重新加载。

警告:千万不要在公钥哈希验证失败后,还继续往下走。我见过一个案例,工程师为了调试方便,在公钥验证失败后加了个“跳过”选项,结果产品发布时忘了关,被安全团队抓了个正着。这种低级错误,一旦出事就是灾难性的。

4.6 实际项目中的常见坑

最后,我分享几个我在项目中踩过的坑,希望能帮你省点时间。

  • 坑一:Flash读取时序不对。BL1刚上电时,Flash控制器可能还没完全初始化。我遇到过读取前几个字节正确,后面全乱的情况。解决办法:在读取前加一个小的延时,或者先读一个已知的Magic Number做校验。
  • 坑二:签名大小超出预期。ECDSA签名是64字节,RSA-2048是256字节。如果你在代码里硬编码了签名缓冲区大小,换算法时一定要改。我见过有人从ECDSA切到RSA,签名缓冲区没改,结果签名被截断,验签永远失败。
  • 坑三:哈希算法不匹配。签名时用的哈希算法和验签时用的必须一致。比如签名时用了SHA-256,验签时用了SHA-384,结果肯定不对。这个看似简单,但在多团队协作时很容易搞混。

好了,BL1验证BL2的核心内容就这些。说白了,就是“公钥哈希锁死根,签名验证明清白”。你把这个逻辑吃透了,后面BL2验证BL31、BL31验证BL33,都是同样的套路。

一句话总结:BL1是信任链的起点,它用ROM里的公钥哈希锁死公钥,再用公钥验签BL2。只要这一步不出错,后面的安全启动就有保障。

可可精品资料库