3. 固件安全启动:安全启动链原理、信任根建立、签名验证机制、防回滚保护
各位好,我是老张。今天咱们聊聊TWS耳机的安全启动。说实话,这玩意儿看着高大上,但说白了就是一句话:确保你耳机里跑的每一行代码,都是厂家授权的,没被人动过手脚。
我在做TWS项目时,遇到过最头疼的事就是——用户刷了个第三方固件,结果耳机变砖了。更可怕的是,如果攻击者通过恶意固件控制了麦克风,那用户的隐私就全完了。所以,安全启动不是锦上添花,而是底线。
3.1 安全启动链原理
安全启动链,你可以想象成一条锁链。每一环都验证下一环的合法性。只要有一环断了,整个系统就不启动。
典型的TWS安全启动链长这样:
- ROM Bootloader(只读存储器中的引导程序)—— 芯片出厂就固化好了,改不了
- Flash Bootloader(闪存引导程序)—— 存放在Flash里,可更新
- 固件镜像 —— 也就是我们常说的Firmware
- 应用程序 —— 跑在RTOS上的业务代码
每一级启动前,都要先验证下一级的签名。验证通过,才把控制权交过去。这就是所谓的链式信任。
核心要点:安全启动链的强度,取决于最弱的一环。而最弱的一环,往往是信任根。
3.2 信任根建立
信任根,英文叫Root of Trust,简称RoT。它是整个安全体系的基石。
为什么叫「根」?因为它是第一个被信任的实体,后续所有的信任都建立在它之上。如果根被污染了,那整个链就全废了。
在TWS芯片中,信任根通常放在哪里?
- ROM —— 只读存储器,出厂后不可更改。这是最理想的信任根载体。
- OTP —— 一次性可编程存储器。可以烧录一次,之后锁死。
- eFuse —— 电子熔丝。通过熔断来存储密钥或配置信息。
我个人习惯把公钥哈希放在OTP里。为什么是哈希而不是公钥本身?因为哈希只有32字节,而公钥可能256字节甚至更长。OTP空间很宝贵,能省则省。
我的经验:曾经有个项目,我把公钥直接放在Flash里。结果发现攻击者可以替换Flash内容,然后用自己的公钥签名固件。嗯,那一次我学到了——信任根必须放在不可篡改的存储介质中。
3.3 签名验证机制
签名验证,说白了就是「盖章」。厂家用私钥给固件盖章,芯片用公钥验章。章对了,固件就是真的。
常用的签名算法有哪些?
| 算法 | 密钥长度 | 签名长度 | 安全性 | 适用场景 |
|---|---|---|---|---|
| RSA-2048 | 2048位 | 256字节 | 高 | 资源充足的设备 |
| ECDSA-P256 | 256位 | 64字节 | 高 | TWS等资源受限设备 |
| Ed25519 | 256位 | 64字节 | 极高 | 现代轻量级设备 |
我个人推荐TWS用ECDSA-P256。为什么?你想想看,TWS芯片的RAM通常只有几百KB,跑RSA-2048的签名验证要好几秒,用户等得起吗?ECDSA-P256只需要几十毫秒,而且安全性不输RSA。
签名验证的流程大致如下:
// 伪代码:签名验证流程
bool verify_firmware(uint8_t* firmware, uint32_t size, uint8_t* signature) {
// 1. 从OTP读取信任根公钥哈希
uint8_t root_hash[32];
read_otp(OTP_ROOT_HASH_ADDR, root_hash, 32);
// 2. 从固件头部提取公钥,计算哈希
uint8_t pubkey_hash[32];
sha256(firmware->pubkey, firmware->pubkey_len, pubkey_hash);
// 3. 比较哈希是否一致
if (memcmp(root_hash, pubkey_hash, 32) != 0) {
return false; // 公钥不匹配,拒绝启动
}
// 4. 用公钥验证固件签名
return ecdsa_verify(firmware->pubkey, firmware, size, signature);
}
注意:这里有个坑。我曾经见过一个方案,只验证了固件头部,没验证整个镜像。攻击者可以修改固件主体,保留头部不变。结果签名验证通过了,但跑的是恶意代码。所以,签名一定要覆盖整个固件镜像,包括代码段、数据段、配置信息等。
3.4 防回滚保护
防回滚,英文叫Anti-Rollback。它的作用是防止攻击者把固件降级到有漏洞的旧版本。
你可能会问:「降级有什么问题?」
问题大了去了。假设新版本修复了一个安全漏洞,攻击者把固件回滚到旧版本,漏洞又回来了。这就是所谓的降级攻击。
防回滚的常见做法:
- 版本号检查 —— 固件头部包含版本号,芯片只允许升级到更高版本。
- 单调计数器 —— 硬件维护一个只增不减的计数器,每次升级都加1。
- eFuse熔断 —— 每次升级熔断一根eFuse,表示版本已更新。
我个人最常用的是版本号+单调计数器的组合。为什么?因为eFuse数量有限,熔断一根少一根。而单调计数器可以用Flash模拟,成本低得多。
防回滚的代码实现:
// 伪代码:防回滚检查
bool check_anti_rollback(uint32_t new_version) {
// 读取当前版本号(存储在Flash的保留区域)
uint32_t current_version = read_flash(FLASH_VERSION_ADDR);
// 新版本必须大于当前版本
if (new_version <= current_version) {
return false; // 拒绝降级
}
// 更新版本号
write_flash(FLASH_VERSION_ADDR, new_version);
return true;
}
避坑指南:我曾经遇到过一个案例,版本号检查通过了,但攻击者把Flash的版本号区域擦除了。结果版本号变成了0xFFFFFFFF,比任何新版本都大,导致再也无法升级。所以,版本号存储区域一定要有写保护,或者使用冗余存储+校验。
3.5 综合实践建议
好了,讲了这么多,我总结几条实战经验:
- 信任根一定要固化 —— 放在ROM或OTP里,别放Flash。
- 签名算法选ECDSA —— 安全性和性能的平衡点。
- 签名覆盖整个镜像 —— 别只验头部。
- 防回滚用单调计数器 —— 简单可靠,成本低。
- 留一条安全升级通道 —— 万一固件刷坏了,还能通过DFU恢复。
最后说一句:安全启动不是万能的,但没有安全启动是万万不能的。你想想看,如果连启动代码都不安全,那后续的所有安全措施都是空中楼阁。
嗯,今天就聊到这儿。下一章咱们讲讲固件加密和密钥管理,那又是另一番天地了。