固件加密与签名:AES-256-GCM 加密实践、ECDSA 签名、密钥管理策略

这一章我们聊聊固件安全里最核心的两个动作——加密和签名。很多人觉得这俩是一回事,其实不然。加密是为了保密,签名是为了防篡改和验身份。我见过不少产品,加密做得很好,但没签名,结果固件被替换成恶意版本,照样跑起来。嗯,咱们得把这两件事都做扎实。

为什么需要加密 + 签名?

你想想看,固件在 Flash 里躺着,或者在 OTA 传输途中,本质上都是暴露在攻击者眼皮底下的。如果只是明文存储,拿个逻辑分析仪就能把二进制读出来。更危险的是,攻击者可以修改固件,植入后门,再刷回去。

所以我们需要两把锁:

  • 加密:让固件内容只有合法设备能看懂。
  • 签名:让设备能确认固件来自可信源头,且没有被改过。

我个人习惯把加密和签名看作「保险柜 + 封条」的关系。加密是保险柜,签名是封条。封条破了,你知道东西被动过;保险柜锁着,别人拿不到内容。

AES-256-GCM:加密与认证一步到位

加密算法很多,为什么我推荐 AES-256-GCM?因为它同时提供了机密性和完整性认证。GCM 模式在加密的同时会生成一个认证标签(Authentication Tag),解密时如果标签不匹配,说明数据被篡改过。

我在项目中遇到过一件事:早期用 AES-CBC 加密固件,结果攻击者通过比特翻转攻击,修改了密文中的某些位,解密后固件虽然乱码了,但设备没有检测机制,直接跑飞了。换成 GCM 之后,认证标签会直接拒绝任何篡改过的数据,安全多了。

AES-256-GCM 加密流程

加密时我们需要三个输入:密钥、初始向量(IV)、明文。输出是密文和认证标签。IV 不需要保密,但每次加密必须不同,否则同样的密钥和 IV 会产生同样的密文,这就危险了。

// 伪代码示例:AES-256-GCM 加密
function encrypt_firmware(plaintext, key) {
    iv = generate_random_iv(12 bytes);  // 推荐 12 字节 IV
    (ciphertext, tag) = aes_256_gcm_encrypt(plaintext, key, iv);
    // 打包:IV + 密文 + 标签
    return iv || ciphertext || tag;
}

解密时,先提取 IV,再用密钥解密,最后验证认证标签。标签验证失败,直接拒绝加载固件。

我的建议:IV 一定要随机生成,不要用固定值。我曾经见过一个产品,IV 写死在代码里,结果所有设备的加密固件都一样,攻击者拿到一个设备的密钥就能解密所有设备。IV 随机化之后,即使密钥相同,每台设备的密文也不同。

ECDSA 签名:用椭圆曲线给固件打上烙印

加密解决了保密问题,但设备怎么知道固件是谁发的?这就需要数字签名。ECDSA(椭圆曲线数字签名算法)是目前嵌入式领域的主流选择,因为它签名短、计算快、安全性高。

签名过程是这样的:

  1. 签名者(比如服务器)用私钥对固件的哈希值签名,生成签名数据。
  2. 设备用公钥验证签名。验证通过,说明固件来自可信源。

这里有个关键点:签名是对哈希值签的,不是对整个固件。固件可能很大,哈希一下能大幅减少计算量。常用的哈希算法是 SHA-256。

ECDSA 签名与验证流程

// 签名端(服务器)
function sign_firmware(firmware_hash, private_key) {
    signature = ecdsa_sign(firmware_hash, private_key);
    return signature;  // 通常为 64 字节(P-256 曲线)
}

// 验证端(设备)
function verify_firmware(firmware_hash, signature, public_key) {
    result = ecdsa_verify(firmware_hash, signature, public_key);
    return result;  // true 或 false
}
注意:公钥存储在设备上,必须防篡改。我见过有人把公钥放在可读写的 Flash 区域,结果攻击者替换了公钥,用自己的私钥签名恶意固件,设备照样认。公钥应该烧死在 OTP(一次性可编程)区域或安全元件里。

密钥管理策略:最薄弱的环节

算法再强,密钥泄露就全完了。密钥管理是固件安全里最容易被忽视的部分。说白了,密钥就是你的命根子。

密钥生命周期管理

阶段 要求 常见问题
生成 使用硬件真随机数发生器(TRNG) 用软件伪随机数,可预测
存储 安全元件或硬件安全模块(HSM) 明文存储在 Flash 中
分发 安全通道,预置或证书交换 通过不安全的网络传输
使用 仅在安全环境中解密,用完即擦 密钥常驻内存,可被 dump
销毁 物理销毁或安全擦除 废弃设备中密钥未清除

我个人习惯把密钥分成几层:

  • 根密钥:烧死在芯片内部,不可读出。用于加密其他密钥。
  • 固件加密密钥:由根密钥派生,用于加密固件。
  • 签名私钥:保存在服务器 HSM 中,设备只存对应的公钥。

这样做的好处是,即使固件加密密钥泄露,根密钥还在,可以更换派生密钥。我曾经帮一个客户做安全审计,发现他们把签名私钥直接放在编译服务器上,任何能登录服务器的人都能签名固件。后来我们强制要求私钥必须存储在 HSM 中,签名操作通过 API 调用,私钥永不离开硬件。

整体流程:从加密到签名的端到端设计

下面这张图展示了固件从构建到部署的完整安全流程。你可以看到加密和签名是如何协同工作的。

固件加密与签名端到端流程 固件加密与签名端到端流程 固件构建 SHA-256 哈希 ECDSA 签名 AES-256-GCM 加密 打包:密文 + 签名 + 元数据 设备端先验证签名,再解密固件。签名验证失败则不执行解密。

流程其实不复杂:先对固件做哈希,哈希值拿去签名,固件本身拿去加密。最后把密文、签名、IV、认证标签打包在一起。设备端收到后,先验签名,再解密。签名验证失败,直接丢弃,不解密。

核心原则:先验证,后解密。永远不要先解密再验证,否则攻击者可能利用解密后的数据做文章。我在一个项目中见过反过来的实现,结果被时序攻击利用了。

避坑指南:我踩过的几个坑

  • 密钥硬编码:我曾经接手一个项目,AES 密钥直接写在源码里,编译进固件。攻击者反编译一下就拿到了。密钥应该从安全存储中读取,或者由硬件派生。
  • 签名验证不完整:有些实现只验证签名是否存在,不验证签名是否匹配。攻击者可以附上一个任意签名,设备照样通过。一定要用公钥完整验证。
  • 随机数重用:ECDSA 签名需要随机数 k,如果 k 重复,私钥会被算出来。索尼 PS3 就栽在这个坑上。一定要用硬件 TRNG 生成 k。
  • 忽略回滚攻击:攻击者可能拿旧版本的固件(有已知漏洞)来刷。签名验证通过,但版本是旧的。所以签名时要带上版本号或时间戳,设备端检查版本是否最新。