3、密码学基础:哈希算法(SHA256)、非对称加密(RSA/ECC)、数字签名原理
说实话,很多做嵌入式开发的兄弟,一听到「密码学」三个字就头大。我当年也一样,总觉得这是搞上层应用的人该操心的。直到有一次,我负责的一个OTA升级方案被爆出签名校验漏洞——攻击者直接替换了固件包,设备照单全收。嗯,从那以后,我再也不敢说「密码学跟我没关系」了。
这一章,咱们就聊透安全启动和OTA升级里最核心的三个密码学工具:哈希算法、非对称加密、数字签名。不扯玄乎的数学公式,只讲你写Bootloader时真正用得上的东西。
核心认知:安全启动的本质,就是「验证信任链」。哈希保证数据没被改过,非对称加密保证密钥只有你持有,数字签名把两者结合起来——你签名的固件,只有你的公钥能验。
3.1 哈希算法:数据的「指纹」
哈希算法,说白了就是把任意长度的数据,压缩成一个固定长度的摘要。这个摘要就像人的指纹——数据不同,指纹就不同;数据一样,指纹一定一样。
在安全启动里,我们最常用的是 SHA256。它输出256位(32字节)的哈希值。为什么选它?因为碰撞概率极低——你想找到两个不同的固件镜像,却有相同的SHA256哈希值,以目前的算力来说,几乎不可能。
我的经验:曾经有个项目,同事为了省Flash空间,用了CRC32做固件完整性校验。结果OTA升级时,一个比特翻转的坏块恰好和原固件CRC32相同——设备「愉快地」刷入了损坏的固件,直接变砖。从那以后,我所有项目的固件校验都只用SHA256,绝不妥协。
SHA256 在 Bootloader 中的典型用法
// 伪代码:Bootloader 校验固件完整性
uint8_t expected_hash[32]; // 从固件头中读取的预期哈希值
uint8_t computed_hash[32]; // 实时计算的哈希值
// 1. 读取固件数据,逐块计算 SHA256
sha256_init(&ctx);
while (has_more_data()) {
sha256_update(&ctx, read_block(), BLOCK_SIZE);
}
sha256_final(&ctx, computed_hash);
// 2. 比对哈希值
if (memcmp(expected_hash, computed_hash, 32) != 0) {
// 哈希不匹配 —— 固件已被篡改或损坏
enter_error_mode();
return;
}
// 哈希匹配 —— 固件完整,继续启动
你想想看,这个流程其实很简单。但有个坑:哈希只保证完整性,不保证真实性。什么意思?攻击者可以同时替换固件和固件头里的哈希值,你的Bootloader照样能通过校验。所以,光有哈希不够,还得有数字签名。
3.2 非对称加密:公钥和私钥的「夫妻档」
非对称加密,核心就是一对密钥:私钥(你死死藏好的)和公钥(可以公开给全世界的)。私钥签名的数据,只有对应的公钥能验证;公钥加密的数据,只有对应的私钥能解密。
在嵌入式安全领域,主流方案就两个:RSA 和 ECC。我直接给你对比一下:
| 特性 | RSA | ECC(椭圆曲线) |
|---|---|---|
| 密钥长度(同等安全强度) | 2048 / 4096 位 | 256 / 384 位 |
| 签名速度 | 较慢(私钥操作) | 较快 |
| 验签速度 | 较快(公钥操作) | 较慢 |
| 签名体积 | 256 / 512 字节 | 64 / 96 字节 |
| 资源消耗(Flash/RAM) | 较高 | 较低 |
| 典型应用场景 | PC/服务器、大容量MCU | IoT设备、资源受限MCU |
避坑指南:我曾经在一个STM32F103项目上硬上RSA-4096验签,结果Bootloader启动时间从200ms飙到了3秒——用户直接投诉「开机太慢」。后来换成ECC-256,验签时间降到300ms,问题解决。选型时一定要算好你的MCU算力和启动时间预算。
3.3 数字签名:哈希 + 非对称加密的「黄金组合」
数字签名,就是把哈希和非对称加密结合起来。流程其实不复杂:
- 签名端(你的PC/服务器):
- 对固件镜像计算 SHA256 哈希
- 用私钥对哈希值进行加密(这就是「签名」)
- 将签名附加到固件包中,一起发布
- 验签端(设备Bootloader):
- 读取固件数据,计算 SHA256 哈希
- 用公钥解密签名,得到原始的哈希值
- 比对两个哈希值:一致则签名有效,固件可信
说白了,签名就是「用私钥给固件盖个章」。公钥就是「验章机」,内嵌在你的Bootloader里,谁都改不了。
一个完整的签名验签流程(伪代码)
// === 签名端(PC) ===
uint8_t firmware_hash[32];
sha256(firmware, firmware_size, firmware_hash);
uint8_t signature[256]; // RSA-2048 签名长度
rsa_sign(private_key, firmware_hash, signature);
// 将 signature 附加到固件包末尾,发布
// === 验签端(Bootloader) ===
uint8_t computed_hash[32];
sha256(firmware, firmware_size, computed_hash);
uint8_t decrypted_hash[32];
rsa_verify(public_key, signature, decrypted_hash);
if (memcmp(computed_hash, decrypted_hash, 32) == 0) {
// 签名验证通过 —— 固件既完整又可信
boot_firmware();
} else {
// 签名验证失败 —— 固件来源不可信
halt_and_report_error();
}
关键点:公钥必须一次性烧录到设备的OTP(One-Time Programmable)区域或安全存储中。如果公钥可以被篡改,那整个信任链就崩塌了。我见过一个产品,公钥存在外部Flash里,攻击者直接替换公钥和自己的私钥配对——安全启动形同虚设。
3.4 实际项目中的选型建议
根据我这些年踩过的坑,给你几个实在的建议:
- 资源极度受限(如Cortex-M0,几十KB Flash):用 ECC-256 + SHA256。签名体积小,验签速度快,代码量也少。
- 主流MCU(如Cortex-M4/M7,512KB+ Flash):RSA-2048 完全够用。生态成熟,各种库都支持。
- 高性能平台(如Cortex-A系列,Linux环境):RSA-4096 或 ECC-384,安全冗余更高。
- 永远不要自己实现密码算法:用 mbedTLS、OpenSSL、或者芯片厂商提供的硬件加速库。自己写的RSA,十有八九有侧信道漏洞。
我的习惯:每次选型前,我会先跑一个验签性能测试——在目标芯片上用实际固件大小跑100次验签,取平均时间。如果超过启动时间预算的20%,就换更轻量的算法或者启用硬件加速模块。别凭感觉选,数据说话。