3. 密码学算法精简:只保留必要的对称加密、哈希和公钥算法
做嵌入式TLS,第一个要面对的现实就是:内存是借来的,不是白给的。我见过太多团队,上来就把OpenSSL全套搬进去,结果Flash爆了,RAM也撑不住。其实,大部分场景下你根本不需要那么多算法。
这一章,我们就聊聊怎么把密码学算法集砍到只剩三样:AES、SHA256和ECDSA。为什么是它们?怎么砍?砍完怎么保证安全?我用自己的踩坑经验跟你一一说清楚。
3.1 为什么是这三样?
你想想看,TLS握手和通信到底需要什么?
- 对称加密:用来加密实际传输的数据。AES是目前最广泛支持、硬件加速最多的选择。
- 哈希:用来做消息完整性校验、证书验证。SHA256是TLS 1.2/1.3的标配。
- 公钥算法:用来做密钥交换和身份认证。ECDSA比RSA快得多,密钥也短,对嵌入式更友好。
说白了,这三样就能覆盖TLS的核心功能。其他的像ChaCha20、Poly1305、RSA、DH……在资源受限的场景下,能砍就砍。
核心原则:只保留握手和加密链路中必须的算法,其余全部移除。每多一个算法,代码体积、内存占用、测试工作量都会翻倍。
3.2 算法裁剪的具体策略
3.2.1 对称加密:只留AES
AES有三种常见模式:ECB、CBC、GCM。我个人习惯是只保留AES-GCM。为什么?
- GCM是认证加密模式,同时提供机密性和完整性,不需要额外搭配HMAC。
- ECB不安全,CBC需要单独处理填充和MAC,代码量更大。
- GCM在TLS 1.3中是强制要求,在TLS 1.2中也是推荐套件。
我在一个IoT网关项目里,最初保留了AES-CBC和AES-GCM两套实现。结果发现CBC模式占了将近4KB的代码空间,还要额外实现PKCS#7填充和HMAC-SHA1。后来全部砍掉,只留GCM,Flash占用直接降了30%。
小技巧:如果你的MCU有AES硬件加速器,优先用硬件实现。软件实现AES-GCM时,注意查表法会占用大量RAM,可以考虑用位切片(bitslice)实现来节省空间。
3.2.2 哈希:只留SHA256
SHA1已经不安全了,SHA512对嵌入式来说太胖。SHA256是平衡点。
我记得有一次做固件升级校验,原方案用了SHA1+MD5双重哈希,说是为了兼容旧设备。我一看代码,两个哈希实现占了6KB。后来跟产品经理吵了一架,把所有旧设备强制升级到支持SHA256,代码体积直接砍半。
需要裁剪SHA256时,可以关注以下几点:
- 去掉不用的变体:比如SHA224、SHA512/224、SHA512/256,这些在TLS里几乎用不到。
- 合并初始化向量:如果只用一个哈希上下文,可以复用结构体,减少RAM占用。
- 考虑硬件加速:很多Cortex-M4/M7芯片内置SHA256硬件引擎,用起来几乎不占Flash。
注意:不要为了省空间而使用SHA1。2017年Google已经成功实现了SHA1碰撞攻击。在TLS场景下,使用SHA1等于把安全大门敞开了。
3.2.3 公钥算法:只留ECDSA
公钥算法是TLS握手里最重的部分。RSA密钥长、计算慢;DH需要额外实现;ECDSA是当前最优解。
我建议只保留ECDSA with P-256曲线。为什么是P-256?
- NIST P-256是TLS 1.3强制支持的曲线。
- 相比P-384/P-521,P-256的密钥和签名更小,计算更快。
- 相比Curve25519(X25519),P-256在嵌入式生态中支持更广泛,硬件加速也更多。
如果你实在想省空间,可以考虑只保留ECDSA的验证功能,不保留签名功能。因为嵌入式设备通常只需要验证服务器证书,不需要自己签名。我在一个传感器节点项目里就是这么干的,ECDSA代码体积从12KB降到了7KB。
| 算法 | Flash占用(典型值) | RAM占用(典型值) | 是否推荐保留 |
|---|---|---|---|
| AES-GCM | 4-8 KB | 256-512 B | 是 |
| AES-CBC | 3-6 KB | 256-512 B | 否(可被GCM替代) |
| SHA256 | 2-4 KB | 128-256 B | 是 |
| SHA1 | 1.5-3 KB | 128-256 B | 否(不安全) |
| ECDSA P-256 | 8-15 KB | 1-2 KB | 是 |
| RSA 2048 | 15-25 KB | 2-4 KB | 否(太胖) |
3.3 算法裁剪后的代码结构
下面是我在一个实际项目中使用的裁剪后密码学模块结构。你可以参考这个来组织你的代码。
crypto/
├── aes_gcm.c # AES-GCM加密/解密
├── aes_gcm.h
├── sha256.c # SHA256哈希
├── sha256.h
├── ecdsa_p256.c # ECDSA P-256签名验证
├── ecdsa_p256.h
├── crypto_config.h # 编译开关,控制哪些算法被编译
└── crypto_internal.h # 内部宏和类型定义
在crypto_config.h里,我用宏来控制编译:
#ifndef CRYPTO_CONFIG_H
#define CRYPTO_CONFIG_H
// 只启用必要的算法
#define ENABLE_AES_GCM 1
#define ENABLE_SHA256 1
#define ENABLE_ECDSA_P256 1
// 禁用所有其他算法
#define ENABLE_AES_CBC 0
#define ENABLE_SHA1 0
#define ENABLE_RSA 0
#define ENABLE_DH 0
#endif // CRYPTO_CONFIG_H
这样做的好处是:编译时直接砍掉不需要的代码,链接器不会把未启用的函数链接进来。我曾经用这种方法,把整个TLS库从200KB压缩到了45KB。
3.4 算法裁剪的避坑指南
我曾经在一个项目中,为了省空间,把ECDSA的曲线参数表给删了,只保留P-256。结果发现TLS握手时,服务器发来的证书用的是P-384曲线,设备直接验证失败。嗯,这里要注意:裁剪算法时,一定要确认你的通信对端支持什么。
另一个坑是:不要裁剪掉随机数生成器。AES-GCM需要IV,ECDSA签名需要随机数k。如果随机数质量不好,整个加密体系就形同虚设。我见过有人为了省RAM,用了一个简单的LFSR来生成随机数,结果被攻击者轻松预测出密钥。
警告:算法裁剪必须配合充分的测试。每砍掉一个算法,都要重新跑一遍TLS握手测试、加密解密测试、签名验证测试。不要相信“应该没问题”。
3.5 本章知识体系
下面这张图总结了算法精简的核心逻辑和决策路径。
这张图展示了从TLS密码学需求出发,经过三层决策,最终达到资源节省的效果。你可以把它贴在项目看板上,提醒团队不要随意加算法。
3.6 小结
算法精简不是简单地把代码删掉,而是有策略地选择。AES-GCM、SHA256、ECDSA P-256这三样,能覆盖95%以上的嵌入式TLS场景。剩下的5%,要么是特殊合规需求,要么是遗留系统兼容——这些可以单独处理,不要污染通用代码。
我个人习惯是在项目初期就定好算法清单,写在设计文档里,然后严格执行。每次有人想加新算法,我都会问一句:“不加它,TLS能跑吗?” 如果答案是“能”,那就别加。