第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的过程拆成四个步骤。每一步都有坑,我一个个说。
- 获取公钥:BL1里硬编码了一个公钥哈希值(不是公钥本身)。它先从Flash里读出完整的公钥,然后计算这个公钥的哈希,跟硬编码的哈希比对。匹配了,才用这个公钥去验签。
- 读取BL2镜像和签名:BL2镜像通常包含三部分:镜像头、镜像数据、签名值。BL1需要把它们都读到内存里。
- 计算镜像哈希:对BL2的镜像数据(不含签名)做SHA-256哈希计算。
- 验签:用步骤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的完整流程。你跟着箭头走一遍,应该就全明白了。
这张图里,我特意把公钥验证和镜像验签用不同颜色区分了。橙色是公钥处理,绿色是镜像处理。为什么这么分?因为公钥验证失败和镜像验签失败,处理方式完全不同——公钥失败说明整个信任链的根被破坏了,必须死机;镜像验签失败,可能只是镜像损坏,可以尝试重新加载。
警告:千万不要在公钥哈希验证失败后,还继续往下走。我见过一个案例,工程师为了调试方便,在公钥验证失败后加了个“跳过”选项,结果产品发布时忘了关,被安全团队抓了个正着。这种低级错误,一旦出事就是灾难性的。
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。只要这一步不出错,后面的安全启动就有保障。
可可精品资料库