4. 加密与签名机制:AES-128/256固件加密、ECDSA签名验证、安全启动链(Secure Boot)实现

说到固件保护,加密和签名是绕不开的两道坎。很多工程师觉得“我只要把固件藏好就行了”,但说实话,物理访问权限一旦被拿到,Flash里的二进制数据就是透明的。我见过不少产品,用逻辑分析仪直接就能把固件dump出来,连加密都没做。

这一章我们聚焦三个核心点:AES固件加密、ECDSA签名验证、以及安全启动链的完整实现。这三者组合起来,基本能挡住90%以上的固件篡改和逆向攻击。

核心原则:加密保证机密性,签名保证完整性,安全启动链保证信任根不被破坏。三者缺一不可。

4.1 AES-128/256 固件加密实战

AES是目前最主流的对称加密算法。128位和256位的区别,说白了就是密钥长度不同,安全强度也不同。我个人习惯:对性能敏感的MCU用AES-128,对安全等级要求高的用AES-256。

为什么?因为AES-256的密钥扩展轮数更多,加解密耗时大约是AES-128的1.4倍。在资源受限的嵌入式设备上,这个差距是能感知到的。

4.1.1 加密模式选择

AES有多种工作模式,我直接给结论:

模式 特点 推荐场景
ECB 相同明文块产生相同密文块,有模式泄露风险 不推荐,除非数据量极小且无规律
CBC 需要IV,密文块依赖前一块,安全性较好 固件加密常用,但注意IV不能重复
CTR 可并行加解密,支持随机访问 适合流式固件更新场景
GCM 同时提供加密和认证,抗篡改 推荐,但需要硬件加速支持

我在项目中遇到过一个问题:用CBC模式加密固件,但每次升级时IV都固定。结果攻击者通过对比两次升级的密文差异,直接推断出了部分明文内容。嗯,这里要注意——IV必须随机生成,且每次升级都要更换。

4.1.2 代码示例:AES-128 CBC固件加密

// 使用mbedTLS库实现AES-128 CBC加密
#include "mbedtls/aes.h"

#define AES_KEY_SIZE 16  // 128位
#define IV_SIZE       16

int encrypt_firmware(uint8_t *plaintext, size_t len, 
                     uint8_t *key, uint8_t *iv,
                     uint8_t *ciphertext) {
    mbedtls_aes_context aes_ctx;
    int ret;
    
    // 初始化上下文
    mbedtls_aes_init(&aes_ctx);
    
    // 设置加密密钥
    ret = mbedtls_aes_setkey_enc(&aes_ctx, key, AES_KEY_SIZE * 8);
    if (ret != 0) {
        // 密钥设置失败,常见原因是密钥长度不对
        return -1;
    }
    
    // 执行CBC加密
    ret = mbedtls_aes_crypt_cbc(&aes_ctx, MBEDTLS_AES_ENCRYPT,
                                len, iv, plaintext, ciphertext);
    if (ret != 0) {
        // 加密失败,检查输入长度是否为16字节对齐
        return -2;
    }
    
    mbedtls_aes_free(&aes_ctx);
    return 0;
}
避坑指南:我曾经因为忘记做PKCS7填充,导致加密后的固件长度不是16的倍数,解密时直接崩溃。记住:AES是块加密,输入长度必须是块大小(16字节)的整数倍。

4.2 ECDSA签名验证

加密只能防止固件被直接读取,但防不住攻击者篡改。这时候就需要签名了。ECDSA(椭圆曲线数字签名算法)相比RSA,密钥更短、签名更快、安全性更高。256位的ECDSA,安全强度相当于3072位的RSA。

你想想看,在MCU上做一次RSA-2048签名验证,可能要几百毫秒;而ECDSA P-256,几十毫秒就搞定了。这对启动时间敏感的设备来说,差距是巨大的。

4.2.1 签名与验证流程

  1. 生成密钥对:私钥保存在安全的生产环境中,公钥烧录到设备OTP或eFuse中。
  2. 计算固件哈希:对固件做SHA-256哈希,得到摘要。
  3. 签名:用私钥对摘要进行ECDSA签名,生成签名值(r, s)。
  4. 验证:设备端用公钥解密签名,与本地计算的哈希比对。
重要:公钥必须存储在一次性可编程(OTP)区域或eFuse中,且烧录后锁定。如果公钥可以被篡改,整个信任链就崩塌了。我见过一个产品,公钥放在Flash里,攻击者直接替换公钥和固件,安全启动形同虚设。

4.2.2 代码示例:ECDSA签名验证

// 使用mbedTLS验证ECDSA签名
#include "mbedtls/ecdsa.h"
#include "mbedtls/sha256.h"

int verify_firmware_signature(uint8_t *firmware, size_t fw_len,
                              uint8_t *signature, size_t sig_len,
                              mbedtls_ecp_keypair *pub_key) {
    uint8_t hash[32];
    int ret;
    
    // 1. 计算固件SHA-256哈希
    mbedtls_sha256(firmware, fw_len, hash, 0);
    
    // 2. 验证签名
    ret = mbedtls_ecdsa_read_signature(pub_key, hash, sizeof(hash),
                                       signature, sig_len);
    if (ret != 0) {
        // 签名验证失败,固件可能被篡改
        return -1;
    }
    
    return 0;  // 验证通过
}

4.3 安全启动链(Secure Boot)实现

安全启动链,说白了就是从芯片上电到应用运行,每一步都要验证上一级的完整性和真实性。这是一个链式信任模型:

信任链:ROM Bootloader → 一级Bootloader → 二级Bootloader → 固件镜像 → 应用

每一级在加载下一级之前,都要验证其签名。如果任何一级验证失败,系统必须进入安全状态(比如死循环或进入恢复模式)。

4.3.1 安全启动流程图

下面这张图展示了典型的安全启动链流程:

安全启动链(Secure Boot Chain)流程图 芯片上电 / Reset ROM Bootloader(不可修改) 验证一级Bootloader签名 进入安全模式 / 死循环 加载并执行一级Bootloader 验证二级Bootloader/固件签名 信任根 ROM Bootloader是 不可修改的信任根 (硬件固化)

4.3.2 实现要点

  • 信任根(Root of Trust):必须是硬件不可修改的,比如ROM代码或eFuse中的公钥哈希。
  • 链式验证:每一级只验证下一级的签名,不跨级验证。这样可以减少ROM代码的复杂度。
  • 回滚保护:固件版本号必须递增,防止攻击者降级到有漏洞的旧版本。
  • 安全状态:验证失败时,不能只是“返回错误码”,必须进入不可恢复的安全状态。
个人经验:我曾经在一个项目中,安全启动链只做了两级验证(ROM→Bootloader→固件),结果攻击者通过替换中间的一级Bootloader,绕过了固件签名验证。后来我改成三级验证,并在每一级都加入了版本号检查,才彻底堵住这个漏洞。

4.4 综合实现:AES加密 + ECDSA签名 + 安全启动

把这三者组合起来,才是完整的固件保护方案:

  1. 生产阶段:用AES密钥加密固件,然后用ECDSA私钥对加密后的固件签名。
  2. 分发阶段:加密固件和签名一起打包,通过OTA或烧录器下发。
  3. 启动阶段:安全启动链先验证签名,通过后再用AES密钥解密固件,最后加载执行。
关键点:AES密钥和ECDSA公钥必须分开存储。AES密钥可以存储在安全元件(SE)或TPM中,ECDSA公钥存储在OTP区域。我曾经见过一个设计,把AES密钥和ECDSA公钥放在同一个Flash扇区,结果一次擦除操作把两个都丢了,设备直接变砖。