4、安全启动流程设计:Bootloader的分区设计
安全启动,说白了就是让芯片只运行你授权的代码。我刚开始做电机控制时,觉得这玩意儿是给军工用的,民品没必要。直到有一次,客户现场批量报故障,一查是有人把非法固件刷进去了……嗯,从那以后,我再也不敢轻视安全启动。
这一章,我们重点聊三个东西:分区怎么划、信任根怎么建、多级架构怎么搭。
4.1 分区设计:Bootloader、Application、Backup
Flash分区是安全启动的物理基础。我个人习惯把Flash至少分成三个区:Bootloader区、Application主区、Backup备份区。为什么是三个?你想想看,如果只有Bootloader和Application,升级过程中断电了怎么办?系统就变砖了。
下面是我在STM32G4上一个典型的分区布局:
| 分区名称 | 起始地址 | 大小 | 作用 |
|---|---|---|---|
| Bootloader | 0x08000000 | 32 KB | 安全启动、固件校验、升级入口 |
| Application | 0x08008000 | 128 KB | 电机控制主程序 |
| Backup | 0x08028000 | 128 KB | 升级备份、回滚用 |
| Parameter | 0x08048000 | 16 KB | 校准参数、运行日志 |
分区设计时,有几点要注意:
- Bootloader区要足够小——32KB通常够用,包含安全校验和升级协议就行。别往里面塞驱动库,那是Application的事。
- Backup区必须和Application区一样大——否则升级时没法做完整备份。我见过有人把Backup只留64KB,Application却用了128KB,结果升级一半空间不够,直接翻车。
- Parameter区单独划出来——电机控制的PID参数、编码器校准值,这些不能跟着固件一起擦除。我习惯用最后16KB存这些,每次升级前先读出来,升级完再写回去。
4.2 信任根建立:从硬件到软件
信任根(Root of Trust)是整个安全启动的基石。说白了,就是芯片上电后执行的第一段代码,它必须是绝对可信的。
信任根怎么建立?我把它分成三个层次:
- 硬件信任根——MCU内部ROM固件,出厂就烧好了,不可修改。比如STM32的System Memory,里面存了出厂Bootloader。这段代码是硬件保证的,没人能改。
- 一级信任根——我们自己写的Bootloader,放在Flash起始地址。它必须被硬件信任根校验通过后才能执行。
- 二级信任根——Application固件,被Bootloader校验通过后才能运行。
核心原则:每一级只信任上一级,上一级只校验下一级的签名和完整性。这就是所谓的「链式信任」。
我在项目中遇到过一个问题:某次升级后,Bootloader把自己给覆盖了,导致整个系统变砖。从那以后,我强制规定——Bootloader区在运行时是只读的,只有通过特殊指令才能擦写。这个保护机制,很多MCU都有,比如STM32的RDP(读保护)和PCROP(专有代码保护)。
4.3 多级安全启动架构
多级安全启动,就是把信任链拉长,每一级都做一次校验。为什么需要多级?因为单级校验有个致命问题——如果Bootloader本身被篡改了,那它校验Application的结果就毫无意义。
下面这张图,是我常用的三级安全启动架构:
这个架构的核心逻辑是:每一级只信任上一级,上一级只校验下一级。硬件信任根是物理不可篡改的,所以整个信任链是安全的。
4.4 启动链的信任根建立细节
信任根建立,不只是概念,得有具体实现。我以STM32G4为例,说说实际怎么做。
第一步:硬件信任根校验
STM32G4上电后,硬件自动从0x08000000读取前4个字节(堆栈指针)和接下来的4个字节(复位向量)。如果这两个值看起来合法,就直接跳转到复位向量执行。这一步,硬件不做签名校验——它只保证代码能跑起来。
第二步:Bootloader自校验
我们自己写的Bootloader,必须在跳转到Application之前,先校验自己的完整性。怎么做?
// Bootloader自校验伪代码
void bootloader_self_check(void) {
uint32_t hash_boot[8]; // SHA-256输出
uint32_t hash_expected[8];
// 从OTP区读取预存的Bootloader哈希值
otp_read(OTP_BOOT_HASH, hash_expected, 32);
// 计算当前Bootloader区的SHA-256
sha256_calc(FLASH_BOOT_ADDR, FLASH_BOOT_SIZE, hash_boot);
// 比较
if (memcmp(hash_boot, hash_expected, 32) != 0) {
// Bootloader被篡改,进入安全模式
enter_safe_mode();
}
}
第三步:Application校验
Bootloader校验通过后,接下来校验Application。这里我推荐用签名校验,不只是哈希。因为哈希只能防意外篡改,防不了恶意攻击。签名校验需要私钥签名、公钥验证,私钥只有开发团队有,攻击者拿不到。
// Application签名校验伪代码
bool verify_application_signature(void) {
uint8_t signature[256]; // RSA-2048签名
uint8_t public_key[256]; // 公钥
uint8_t hash_app[32]; // SHA-256
// 读取Application末尾的签名
read_flash(APP_SIGNATURE_ADDR, signature, 256);
// 读取公钥(从OTP区)
otp_read(OTP_PUBLIC_KEY, public_key, 256);
// 计算Application的哈希
sha256_calc(FLASH_APP_ADDR, FLASH_APP_SIZE, hash_app);
// RSA验证
if (rsa_verify(hash_app, signature, public_key) == 0) {
return true; // 签名有效
}
return false;
}
4.5 多级安全启动的实战要点
说了这么多理论,最后总结几个实战中容易踩的坑:
- 校验顺序不能乱——必须先校验Bootloader自己,再校验Application。我见过有人反过来,先校验Application,结果Bootloader被篡改了都不知道。
- 密钥管理要严格——私钥不能放在代码仓库里,更不能硬编码在源码中。我习惯用HSM(硬件安全模块)存储私钥,每次签名都走HSM,密钥永远不落地。
- 回滚机制要可靠——升级失败时,Bootloader必须能从Backup区恢复。我遇到过一种情况:Backup区里的固件也是坏的,结果回滚后还是跑不起来。所以,Backup区的固件在写入时也要做完整性校验。
- 启动时间要控制——多级校验会增加启动时间。SHA-256计算128KB的固件,大约需要50ms。如果再加上RSA签名验证,总共可能到200ms。对于电机控制来说,这个时间通常可以接受,但如果你做的是伺服驱动器,200ms的启动延迟可能就不行了。这时候可以考虑用硬件加速器,比如STM32的CRYP外设。
我的建议:安全启动不是一蹴而就的。如果你的产品对安全要求不高,可以先做一级校验(Bootloader校验Application)。等后面产品迭代了,再逐步加上多级校验和签名验证。别一开始就想搞个大而全的方案,容易把自己绕进去。