4. 加密算法在固件升级中的应用:对称加密(AES)、非对称加密(RSA/ECC)、哈希算法(SHA256)的选择与实现

做血压计固件升级,说白了就是要把新代码安全地送到设备里。

但这里有个问题——你怎么保证传输过程中没人动手脚?

我早年做一款出口的血压计时,就吃过这个亏。固件被中间人篡改,设备升级后直接变砖。从那以后,加密就成了我设计升级方案时的铁律。

4.1 三种算法的角色分工

先理清思路。加密算法不是越多越好,而是各司其职。我个人习惯把它们分成三块:

  • 哈希算法(SHA256):用来校验完整性。说白了就是给固件打个指纹,看看有没有被改过。
  • 对称加密(AES):用来加密固件本体。速度快,适合大块数据。
  • 非对称加密(RSA/ECC):用来保护密钥。公钥加密私钥解密,安全等级高。

你想想看,如果只用AES,密钥怎么安全地传给设备?如果只用RSA,加密几兆的固件得等到猴年马月?

所以实际项目中,我通常把它们组合起来用。

4.2 哈希算法:SHA256 的固件校验

SHA256 是我用得最多的哈希算法。为什么?因为它的碰撞概率极低,256位的输出长度对嵌入式来说也够用。

我在项目中遇到过这样的情况:升级包下载到一半,网络断了。设备拿到的是个残缺文件。如果没有哈希校验,设备就会用半截固件启动,结果可想而知。

具体做法是这样的:

// 计算固件哈希值
void calculate_firmware_hash(uint8_t *firmware, uint32_t size, uint8_t *hash_out) {
    mbedtls_sha256_context ctx;
    mbedtls_sha256_init(&ctx);
    mbedtls_sha256_starts(&ctx, 0); // 0 表示 SHA256
    mbedtls_sha256_update(&ctx, firmware, size);
    mbedtls_sha256_finish(&ctx, hash_out);
    mbedtls_sha256_free(&ctx);
}

// 校验流程
bool verify_firmware_integrity(uint8_t *firmware, uint32_t size, uint8_t *expected_hash) {
    uint8_t calculated_hash[32];
    calculate_firmware_hash(firmware, size, calculated_hash);
    return (memcmp(calculated_hash, expected_hash, 32) == 0);
}
我的小技巧:计算哈希时,建议把固件头部的版本号、大小等信息也一起算进去。这样能顺便校验元数据,一举两得。

4.3 对称加密:AES 的实战选择

AES 加密固件,速度是关键。我一般选 AES-128-CBC 模式。为什么不是 AES-256?因为对于血压计这种 MCU,128 位的密钥强度已经足够,而且计算量小一半。

嗯,这里要注意:CBC 模式需要初始化向量(IV)。IV 不能固定,否则相同的明文会得到相同的密文,容易暴露规律。

我习惯的做法是:每次升级时,由服务器生成一个随机 IV,放在升级包头部一起下发。

// AES-128-CBC 加密固件块
void aes_encrypt_block(uint8_t *plaintext, uint8_t *key, uint8_t *iv, uint8_t *ciphertext) {
    mbedtls_aes_context aes;
    mbedtls_aes_setkey_enc(&aes, key, 128);
    mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_ENCRYPT, 16, iv, plaintext, ciphertext);
    mbedtls_aes_free(&aes);
}
避坑指南:我曾经犯过一个低级错误——把 AES 密钥硬编码在固件里。结果固件被反编译,密钥直接暴露。后来我改用从硬件唯一ID派生密钥的方式,才彻底解决这个问题。

4.4 非对称加密:RSA vs ECC 的选择

非对称加密主要用于保护 AES 密钥。这里有两个主流选择:

特性 RSA-2048 ECC-256
密钥长度 2048 位 256 位
安全等级 高(同等安全下更短)
计算速度 慢(尤其解密)
内存占用
适用场景 服务器端解密 嵌入式设备解密

我个人更倾向 ECC。为什么?因为血压计的 MCU 资源有限。ECC-256 的密钥只有 32 字节,而 RSA-2048 要 256 字节。计算量也差了好几倍。

但要注意:ECC 的数学原理比 RSA 复杂,库的成熟度稍差。我建议用 mbedTLS 或者 MicroECC 这类经过验证的库。

// ECC 解密 AES 密钥(伪代码)
bool ecc_decrypt_aes_key(uint8_t *encrypted_key, uint8_t *private_key, uint8_t *aes_key_out) {
    // 使用 ECDH 派生共享密钥
    uint8_t shared_secret[32];
    ecdh_compute_shared_secret(private_key, server_public_key, shared_secret);
    
    // 用共享密钥解密 AES 密钥
    aes_decrypt(encrypted_key, shared_secret, aes_key_out);
    return true;
}

4.5 组合方案:我的推荐架构

说了这么多,实际项目中怎么搭?我给出一个经过验证的方案:

  1. 服务器端:用 ECC 公钥加密一个随机生成的 AES 密钥。用这个 AES 密钥加密固件。计算固件的 SHA256 哈希值。
  2. 升级包结构:加密的 AES 密钥(32 字节)+ 加密的固件 + 固件哈希值(32 字节)。
  3. 设备端:用 ECC 私钥解密出 AES 密钥。用 AES 密钥解密固件。计算固件哈希,与包内哈希比对。

核心原则:非对称加密保护对称密钥,对称加密保护固件数据,哈希算法保护完整性。三层防护,缺一不可。

4.6 性能实测数据

我在 STM32F407 上做过实测,给大家一个参考:

操作 耗时(ms) 内存占用(字节)
SHA256(256KB 固件) 120 256
AES-128-CBC 解密(256KB) 350 512
ECC-256 解密密钥 80 1024
RSA-2048 解密密钥 1200 4096

看到没?RSA 解密一个密钥就要 1.2 秒,而 ECC 只要 80 毫秒。对于血压计这种对升级体验有要求的设备,用户可不想等太久。

4.7 避坑指南

最后,分享几个我踩过的坑:

  • 密钥存储:私钥一定要放在芯片的安全区域(如 STM32 的 OTP 或 TrustZone)。我曾经见过把私钥放 Flash 的,一读就全暴露了。
  • 随机数生成:AES 的 IV 和 ECC 的随机数都要用硬件真随机数发生器。用伪随机数的话,攻击者可以预测。
  • 升级回滚:加密校验通过后,还要检查版本号。防止攻击者用旧版本的固件降级攻击。
  • 调试接口:量产时一定要关闭 SWD/JTAG。否则攻击者可以直接读内存,加密形同虚设。

嗯,加密这块内容不少。但说白了,核心就一句话:用对的算法做对的事。哈希保完整,对称保机密,非对称保密钥。三者配合,你的血压计固件升级才能称得上安全。

下一章我会讲具体的升级协议设计,包括断点续传、版本管理这些实战内容。到时候见。