密码学基础(上):对称加密、非对称加密与哈希函数
各位同学,咱们今天聊点硬核的——密码学。别一听这词就头大,其实它没那么玄乎。说白了,密码学就是给数据上锁、开锁、验锁的一套手艺。在固件热更新里,这三样东西缺一不可:你得保证更新包不被偷看(加密),得保证它确实是官方发的(签名),还得保证它没被篡改过(哈希校验)。
我个人习惯把密码学比作一个工具箱。今天咱们先打开这个工具箱,看看最常用的三把工具:AES(对称加密)、RSA/ECC(非对称加密)、SHA-256(哈希函数)。
一、对称加密:AES
先说说对称加密。你想想看,什么叫对称?就是加密和解密用的是同一把钥匙。就像你家的门锁,用这把钥匙锁上,还得用这把钥匙打开。
AES(Advanced Encryption Standard)是目前最主流的对称加密算法。它支持三种密钥长度:128位、192位、256位。我个人建议,在固件更新场景里,至少用AES-128,有条件直接上AES-256。
核心要点:AES是分组加密算法,每次处理128位(16字节)数据。如果数据长度不是16的倍数,需要做填充(Padding)。
我在项目中遇到过一个问题:某次做OTA升级,固件包加密后长度变了,结果设备端解出来全是乱码。后来一查,是填充模式没对齐。嗯,这里要注意——加密和解密必须使用相同的填充模式。
AES有几种工作模式,我列个表给你看:
| 模式 | 特点 | 适用场景 |
|---|---|---|
| ECB | 简单,但相同明文块会生成相同密文块 | 不推荐,有安全风险 |
| CBC | 需要初始向量(IV),安全性较好 | 固件加密常用 |
| CTR | 可并行计算,支持随机访问 | 流式数据加密 |
| GCM | 同时提供加密和认证 | 推荐,兼顾安全与效率 |
我的建议:做固件热更新,优先选AES-256-GCM模式。它自带认证标签,能检测数据是否被篡改。省得你额外再算一遍哈希。
来看一段C语言代码示例,演示如何使用AES-128-CBC加密一个固件块:
#include <openssl/aes.h>
// 加密一个16字节的固件块
void aes_encrypt_block(uint8_t *plaintext, uint8_t *key, uint8_t *iv, uint8_t *ciphertext) {
AES_KEY aes_key;
AES_set_encrypt_key(key, 128, &aes_key);
AES_cbc_encrypt(plaintext, ciphertext, 16, &aes_key, iv, AES_ENCRYPT);
}
这段代码看着简单,但实际用的时候有几个坑。我曾经在STM32上移植OpenSSL,发现内存占用太大,后来改用mbedTLS才搞定。说白了,嵌入式环境里,库的选择比算法本身更关键。
二、非对称加密:RSA与ECC
对称加密有个硬伤——密钥怎么安全地传给对方?你想想看,如果密钥在传输过程中被截获,那加密就形同虚设了。
非对称加密就是来解决这个问题的。它用一对钥匙:公钥公开给所有人,私钥自己藏好。公钥加密的数据,只有私钥能解开;反过来,私钥签名的数据,公钥可以验证。
在固件热更新里,非对称加密主要干两件事:
- 密钥交换:用RSA或ECC加密AES密钥,安全地传给设备端
- 数字签名:用私钥对固件哈希值签名,设备端用公钥验证签名
RSA是最经典的非对称算法。它基于大整数分解的数学难题。密钥越长越安全,但计算也越慢。我建议至少用2048位,1024位已经不安全了。
ECC(椭圆曲线密码学)是后起之秀。同样的安全强度下,ECC的密钥长度比RSA短得多。比如,256位的ECC相当于3072位的RSA。这对嵌入式设备来说太友好了——省内存、省带宽、省电量。
对比一下:同样达到128位安全强度,RSA需要3072位密钥,ECC只需要256位。在资源受限的MCU上,ECC几乎是唯一的选择。
我记得有一次做物联网网关的固件更新,设备只有64KB RAM。用RSA-2048做签名验证,光密钥加载就占了8KB,再加上运算时的临时缓冲区,差点爆内存。后来换成ECC-256,一切顺畅。
避坑指南:我曾经在项目中直接用了OpenSSL的默认RSA填充模式(PKCS#1 v1.5),结果被安全审计指出存在填充预言攻击。后来改用OAEP填充才过关。记住,不要用默认参数,一定要显式指定安全的填充模式。
来看一段ECC签名验证的伪代码:
// 使用ECC私钥对固件哈希签名
ecdsa_sign(firmware_hash, private_key, &signature);
// 设备端使用ECC公钥验证签名
result = ecdsa_verify(firmware_hash, signature, public_key);
if (result == VALID) {
// 签名验证通过,固件是官方发布的
apply_update();
} else {
// 签名无效,拒绝更新
abort_update();
}
三、哈希函数:SHA-256
哈希函数,说白了就是数据指纹。不管你的输入是一段文字、一个固件包、还是一整部电影,哈希函数都会输出一个固定长度的摘要。而且,哪怕输入只改了一个比特,输出的摘要也会面目全非。
SHA-256是SHA-2家族的一员,输出256位(32字节)的哈希值。它在固件更新里主要用来做完整性校验:
- 计算固件包的SHA-256哈希值
- 用私钥对这个哈希值签名
- 设备端收到固件后,先算哈希,再用公钥验证签名
你想想看,为什么要先哈希再签名?直接签名整个固件包不行吗?行是行,但效率太低了。RSA签名一个256位的哈希值,比签名几兆字节的固件包快得多。这就是哈希函数的妙用——把任意长度的数据压缩成固定长度的摘要。
我的经验:在固件更新中,我习惯把SHA-256哈希值和固件版本号、设备ID一起打包签名。这样既能验证固件完整性,又能防止版本回滚攻击。一举两得。
SHA-256的计算过程,我简单说一下:
- 对输入数据进行填充,使长度成为512位的倍数
- 将填充后的数据分成512位的块
- 每个块经过64轮压缩函数处理
- 最终输出256位的哈希值
来看一段计算SHA-256的代码:
#include <openssl/sha.h>
void compute_firmware_hash(uint8_t *firmware, size_t len, uint8_t *hash_out) {
SHA256_CTX ctx;
SHA256_Init(&ctx);
SHA256_Update(&ctx, firmware, len);
SHA256_Final(hash_out, &ctx);
}
// 使用示例
uint8_t firmware_hash[32];
compute_firmware_hash(firmware_data, firmware_size, firmware_hash);
// 现在 firmware_hash 里就是固件的SHA-256指纹
这里有个细节要注意:哈希函数是单向的。你从哈希值推不出原始数据。所以,如果有人拿到了固件的SHA-256值,他也没法反推出固件内容。这就是为什么哈希适合做完整性校验,但不适合做加密。
总结一下三者的关系:
- AES:给固件数据上锁,防止被偷看
- RSA/ECC:安全传递AES密钥,并给固件打上官方烙印(签名)
- SHA-256:给固件生成指纹,确保内容没被改过
三者配合,才能构建一个完整的固件热更新安全方案。
好了,今天的内容就到这里。密码学这东西,光看理论是不够的。我建议你找个开发板,实际跑一遍AES加密、ECC签名、SHA-256校验的流程。踩过几个坑之后,你才能真正理解这些算法在嵌入式环境里的脾气秉性。
公众号:蓝海资料掘金营,微信deep3321