4、密码学基础(上):对称加密(AES)、非对称加密(RSA/ECC)、哈希函数(SHA256)
做 BootLoader 安全升级,说白了就是解决三个问题:怎么保证升级包不被偷看?怎么保证它确实是官方发的?怎么保证它没被篡改?这三个问题,分别对应着加密、签名和完整性校验。而它们的根基,就是密码学。
这一章我们先啃最核心的三个工具:AES(对称加密)、RSA/ECC(非对称加密)和 SHA256(哈希函数)。别被这些缩写吓到,我会用实际项目里的例子把它们讲透。
核心观点:没有密码学,BootLoader 升级就是裸奔。攻击者可以伪造固件、窃取密钥、植入后门。我见过太多因为「先上线再补加密」导致翻车的项目了。
4.1 对称加密:AES 是怎么保护固件内容的?
对称加密,就是加密和解密用同一把钥匙。你想想看,就像你用一把锁锁住箱子,寄给朋友,朋友用同样的钥匙打开。在 BootLoader 场景里,固件在服务器上用密钥加密,设备收到后用同样的密钥解密。
AES(Advanced Encryption Standard)是目前最主流的对称加密算法。我个人的习惯是,只要硬件支持,优先选 AES-256。为什么?因为 AES-128 虽然目前还安全,但量子计算一旦成熟,128 位密钥的强度就不够了。而 AES-256 还能扛一阵子。
AES 的工作模式
AES 本身只处理 16 字节的数据块。要加密一整段固件,就得用「模式」。这里我重点讲三种最常见的:
| 模式 | 特点 | BootLoader 适用性 | 我踩过的坑 |
|---|---|---|---|
| ECB | 每个块独立加密,简单 | ❌ 不推荐 | 相同明文块产生相同密文块,容易被分析 |
| CBC | 需要 IV,每个块依赖前一个 | ✅ 常用 | IV 必须随机且每次不同,否则有风险 |
| CTR | 流模式,可并行加解密 | ✅ 推荐 | 计数器不能重复,否则 XOR 密钥流会泄露 |
⚠️ 千万不要用 ECB 模式加密固件!我曾经接手过一个项目,前任工程师用 ECB 加密升级包,结果固件里有大量重复的 0xFF 填充字节,密文一眼就能看出规律。攻击者不需要破解密钥,光靠模式分析就能定位关键代码段的位置。
我个人最推荐 CTR 模式。原因有三:第一,它可以并行计算,在支持多核或硬件加速的芯片上速度飞快;第二,它不需要填充,固件长度任意;第三,加解密逻辑完全一样,代码量少一半。
AES 在 BootLoader 中的典型用法
/* 伪代码:AES-CTR 解密固件块 */
#define AES_BLOCK_SIZE 16
int aes_decrypt_firmware(uint8_t *ciphertext, uint32_t len,
uint8_t *key, uint8_t *iv, uint8_t *plaintext)
{
AES_CTR_ctx ctx;
uint32_t offset = 0;
aes_ctr_init(&ctx, key, iv);
while (offset < len) {
uint32_t chunk = MIN(len - offset, 1024); // 每次处理 1KB
aes_ctr_decrypt(&ctx, ciphertext + offset,
plaintext + offset, chunk);
offset += chunk;
}
return 0;
}
💡 小技巧:在 BootLoader 里,IV 通常由两部分组成:一个固定的种子(烧录在芯片 OTP 里)加上固件头部的随机数。这样即使同一份固件,每次升级的 IV 也不同,防止重放攻击。
4.2 非对称加密:RSA 和 ECC 怎么保证固件来源可信?
对称加密有个致命问题:密钥怎么安全地分发给设备?如果密钥在产线里被泄露,或者被逆向工程提取,整个加密体系就崩了。
非对称加密解决了这个问题。它用一对密钥:公钥公开,私钥保密。服务器用私钥签名固件,设备用公钥验证签名。公钥就算被攻击者拿到,他也无法伪造签名。
RSA vs ECC:怎么选?
| 对比项 | RSA-2048 | ECC-256(secp256r1) |
|---|---|---|
| 密钥长度 | 2048 位 | 256 位 |
| 签名长度 | 256 字节 | 64 字节 |
| 签名速度 | 较慢(私钥操作) | 快 |
| 验签速度 | 较快(公钥操作) | 较快 |
| 安全强度 | 等效 112 位对称密钥 | 等效 128 位对称密钥 |
| 硬件资源消耗 | 高(大数运算) | 低(椭圆曲线运算) |
嗯,这里要注意:ECC 在资源受限的 MCU 上优势非常明显。我记得有一次给一个 Cortex-M0 的芯片做安全升级,RSA-2048 验签一次要 2 秒多,换成 ECC-256 后降到 200 毫秒以内。用户按「升级」按钮后的等待感完全不一样。
数字签名流程(以 ECDSA 为例)
- 服务器端:计算固件的 SHA256 哈希值 → 用私钥对哈希值签名 → 将签名附加到固件包中。
- 设备端:接收固件包 → 计算本地 SHA256 哈希值 → 用公钥验证签名 → 匹配则继续,不匹配则拒绝升级。
关键点:签名验证的是「哈希值」,不是整个固件。因为非对称加密处理大块数据非常慢。先哈希再签名,是工程上的标准做法。
公钥的存储安全
公钥虽然可以公开,但必须保证它没有被篡改。如果攻击者替换了设备里的公钥,他就能用自己的私钥签名恶意固件。
我建议的做法是:
- 将公钥哈希烧录到芯片的 一次性可编程(OTP)区域。
- BootLoader 启动时,先验证公钥的哈希是否与 OTP 中的值一致。
- 如果一致,才用该公钥验证固件签名。
⚠️ 我曾经见过一个产品,公钥直接明文存在外部 Flash 里。攻击者用逻辑分析仪抓取 SPI 通信,轻松替换了公钥,然后签了一个挖矿程序进去。设备变成了肉鸡。这个教训告诉我们:公钥可以公开,但不能被篡改。
4.3 哈希函数:SHA256 如何保证固件完整性?
哈希函数,说白了就是一个「数字指纹」。不管你的固件是 1KB 还是 1MB,SHA256 都会输出一个固定 256 位(32 字节)的摘要。只要固件里改了一个比特,摘要就会变得面目全非——这就是所谓的雪崩效应。
SHA256 在 BootLoader 中的三个用途
- 完整性校验:升级前计算固件的 SHA256,与固件头部的声明值对比。不一致则拒绝升级。
- 签名的基础:如上一节所说,非对称签名是对哈希值签名,不是对原始固件。
- 密钥派生:可以用 SHA256 从主密钥派生出会话密钥,避免主密钥直接暴露。
代码示例:计算固件块的 SHA256
/* 伪代码:分块计算 SHA256,适合内存受限的 BootLoader */
#define SHA256_DIGEST_SIZE 32
int compute_firmware_hash(uint8_t *firmware, uint32_t size,
uint8_t hash_out[SHA256_DIGEST_SIZE])
{
SHA256_CTX ctx;
uint32_t offset = 0;
sha256_init(&ctx);
while (offset < size) {
uint32_t chunk = MIN(size - offset, 512); // 每次 512 字节
sha256_update(&ctx, firmware + offset, chunk);
offset += chunk;
}
sha256_final(&ctx, hash_out);
return 0;
}
💡 实际项目经验:在 BootLoader 里,我习惯把 SHA256 计算和 AES 解密流水线化。一边解密一边算哈希,等解密完成,哈希也出来了。这样既省内存又省时间。不过要注意顺序:先算原始密文的哈希,再解密。如果先解密再算哈希,那验证的是明文,逻辑上就不对了。
4.4 三者如何协同工作?
在真正的 BootLoader 安全升级中,这三个算法不是各自为战,而是打配合的。我画了一张流程图,帮你理清关系:
这个流程里,顺序很重要。先校验哈希,确保固件在传输过程中没有被损坏;再验证签名,确认固件来自合法的发布方;最后用 AES 解密,得到可执行的明文固件。每一步都依赖上一步的结果,环环相扣。
总结一下:
- AES 负责「保密」—— 固件内容不被窃听。
- RSA/ECC 负责「认证」—— 固件来源可信。
- SHA256 负责「完整性」—— 固件未被篡改。
三者缺一不可。少了任何一个,你的 BootLoader 升级就是纸糊的。