4. 固件加密与解密:对称加密算法在固件中的应用、密钥派生与存储、OTA升级中的加密传输

说到固件安全,加密这块儿是绕不过去的坎儿。我见过不少团队,功能跑得飞起,结果一抓包,固件明文传输,密钥硬编码在代码里。嗯,这种项目基本就是给攻击者送人头。今天咱们就聊聊对称加密在固件里的那些事儿,包括怎么用、怎么存密钥、OTA升级怎么保证不被篡改。

4.1 对称加密算法在固件中的应用

对称加密,说白了就是加密和解密用同一把钥匙。在嵌入式环境里,资源受限,我们常用的就那么几种:AES、ChaCha20、SM4(国标)。我个人习惯首选AES-128-CCM或GCM模式,因为自带认证,能同时保证机密性和完整性。

核心原则:固件加密不是为了隐藏算法,而是为了隐藏密钥。算法公开没关系,密钥泄露才是灾难。

我在项目中遇到过一件事:某款智能锁固件用AES-ECB模式加密通信数据。结果呢?同样的明文块产生同样的密文块,攻击者通过重放攻击直接开了锁。所以模式选择很重要,千万别用ECB。

算法 模式 适用场景 我的建议
AES-128 CCM / GCM 固件加密、OTA传输 首选,硬件加速普遍
ChaCha20-Poly1305 AEAD 低端MCU、无AES硬件 软件实现快,抗侧信道
SM4 GCM / CBC 国密合规场景 国内项目必备

你想想看,Matter设备里很多都是电池供电的,算力有限。AES-GCM模式虽然好,但如果MCU没有硬件加密引擎,纯软件跑起来功耗感人。这时候ChaCha20就香了,纯软件实现比AES快好几倍。

4.2 密钥派生与存储

密钥怎么来的?怎么存?这是个大问题。很多开发者图省事,直接把密钥写死在代码里。我曾经逆向过一个智能灯泡,密钥就在字符串表里,连偏移量都没变。这种操作,说实话,跟没加密一样。

正确的做法是用密钥派生函数(KDF)。比如从设备唯一ID、出厂种子、随机数一起算出一个会话密钥。Matter协议里用的是HKDF-SHA256,我个人觉得这个选择很合理。

// 密钥派生示例:基于设备序列号和随机数
uint8_t seed[16] = {0x...}; // 出厂预置种子
uint8_t serial[8];          // 设备唯一序列号
uint8_t nonce[12];          // 随机数
uint8_t derived_key[16];    // 派生出的会话密钥

hkdf_sha256(seed, sizeof(seed),
            serial, sizeof(serial),
            nonce, sizeof(nonce),
            derived_key, sizeof(derived_key));

小技巧:派生密钥时一定要加盐(salt)。不加盐的话,同样的种子和序列号会算出同样的密钥,攻击者可以预计算彩虹表。

密钥存储就更讲究了。别放Flash明文区,那是给调试器看的。我建议用这几招:

  • 硬件安全单元(HSM/Secure Element):密钥根本不出芯片,只往里送数据,拿结果。这是最安全的。
  • MCU内置OTP(一次性可编程)区域:写进去就改不了,读也受限。适合存根密钥。
  • Flash加密存储:如果没硬件,至少用设备唯一密钥加密后再存。但要注意,这个唯一密钥本身不能存Flash里。

避坑指南:我曾经见过一个项目,把密钥用XOR混淆后存Flash。攻击者只要读两次不同状态的Flash,一对比就能还原出原始密钥。这种“伪加密”比不加密更危险,因为它给你虚假的安全感。

4.3 OTA升级中的加密传输

OTA升级是攻击者的重点目标。你想啊,固件在空中飞,要是被中间人换了,设备就成别人的肉鸡了。Matter的OTA设计得还算严谨,但落地实现时坑不少。

一个完整的OTA加密传输流程应该是这样的:

  1. 设备发起请求,附带自己的证书和随机数。
  2. 服务器验证设备身份,生成会话密钥。
  3. 用会话密钥加密固件包,同时计算MAC(消息认证码)。
  4. 设备收到后,先验MAC,再解密,最后校验固件签名。

这里有个关键点:先验证完整性,再解密。为什么?因为如果先解密再验签,攻击者可以构造恶意密文触发解密时的缓冲区溢出。我在一次安全审计中就遇到过这种实现,差点出事。

// OTA固件包结构(简化版)
typedef struct {
    uint32_t magic;          // 魔数 0xOTA1
    uint32_t version;        // 固件版本
    uint8_t  iv[12];         // 初始向量
    uint8_t  tag[16];        // GCM认证标签
    uint8_t  ciphertext[];   // 加密后的固件数据
} ota_packet_t;

// 解密流程
int ota_decrypt(ota_packet_t *pkt, uint8_t *key, uint8_t *plaintext) {
    // 1. 先验证认证标签
    if (!aes_gcm_decrypt(pkt->ciphertext, len,
                         pkt->iv, key,
                         pkt->tag, plaintext)) {
        return -1; // 完整性校验失败,丢弃
    }
    // 2. 再校验固件数字签名
    if (!verify_signature(plaintext, len, device_pubkey)) {
        return -2; // 签名无效,拒绝升级
    }
    return 0;
}

关键点:OTA加密传输的“加密”只是第一道防线。真正的安全基石是固件签名。加密防窃听,签名防篡改。两者缺一不可。

我建议在OTA实现中注意这几个细节:

  • 每次OTA使用不同的随机IV,防止重放攻击。同一个密钥下IV重复,GCM就废了。
  • 固件版本号要防回滚。攻击者可能拿旧版本固件(有漏洞的)来降级攻击。Matter里用版本号单调递增来防这个。
  • 解密失败要记录日志,但不能泄露原因。比如“认证失败”和“解密失败”返回同样的错误码,防止攻击者通过错误信息猜密钥。
OTA升级加密传输流程 Matter设备 OTA服务器 ① 请求升级 + 证书 + 随机数 ② HKDF派生会话密钥 ③ AES-GCM加密固件 ④ 传输密文 + IV + 认证标签 ⑤ 验证标签 + 解密 ⑥ 校验固件签名 ⑦ 写入Flash并重启 ⚠ 关键检查点 先验MAC → 再解密 → 最后验签 顺序错了,安全就没了

最后说一句,加密这事儿,别自己发明轮子。用现成的库,比如mbedTLS、OpenSSL(如果空间够)、或者芯片厂商提供的加密库。我见过有人自己写AES实现,结果S盒查表有侧信道泄漏,功耗分析直接就把密钥扒出来了。嗯,专业的事交给专业的库,咱们把精力花在架构设计和密钥管理上,这才是正道。