3. 固件安全启动:安全启动链原理、信任根建立、签名验证机制、防回滚保护

各位好,我是老张。今天咱们聊聊TWS耳机的安全启动。说实话,这玩意儿看着高大上,但说白了就是一句话:确保你耳机里跑的每一行代码,都是厂家授权的,没被人动过手脚

我在做TWS项目时,遇到过最头疼的事就是——用户刷了个第三方固件,结果耳机变砖了。更可怕的是,如果攻击者通过恶意固件控制了麦克风,那用户的隐私就全完了。所以,安全启动不是锦上添花,而是底线。

3.1 安全启动链原理

安全启动链,你可以想象成一条锁链。每一环都验证下一环的合法性。只要有一环断了,整个系统就不启动。

典型的TWS安全启动链长这样:

  1. ROM Bootloader(只读存储器中的引导程序)—— 芯片出厂就固化好了,改不了
  2. Flash Bootloader(闪存引导程序)—— 存放在Flash里,可更新
  3. 固件镜像 —— 也就是我们常说的Firmware
  4. 应用程序 —— 跑在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 综合实践建议

好了,讲了这么多,我总结几条实战经验:

  1. 信任根一定要固化 —— 放在ROM或OTP里,别放Flash。
  2. 签名算法选ECDSA —— 安全性和性能的平衡点。
  3. 签名覆盖整个镜像 —— 别只验头部。
  4. 防回滚用单调计数器 —— 简单可靠,成本低。
  5. 留一条安全升级通道 —— 万一固件刷坏了,还能通过DFU恢复。

最后说一句:安全启动不是万能的,但没有安全启动是万万不能的。你想想看,如果连启动代码都不安全,那后续的所有安全措施都是空中楼阁。

嗯,今天就聊到这儿。下一章咱们讲讲固件加密和密钥管理,那又是另一番天地了。