哈希与消息认证:用对算法,守好底线
哈希函数,说白了就是个「数字指纹」。输入任意长度数据,输出固定长度的摘要。这个摘要不可逆,而且哪怕输入只改一个比特,输出也会面目全非。搞加密架构,哈希是基石——数据完整性校验、密码存储、消息认证,哪哪都离不开。
我自己刚入行时犯过一个低级错误:拿MD5做文件校验。后来甲方要求做等保测评,专家一看就笑了。嗯,MD5已被彻底攻破,碰撞构造成本低到可以随意玩。
SHA-2 与 SHA-3:两种思路
SHA-2(包括 SHA-224/256/384/512)是目前最广泛使用的哈希族。它基于 Merkle–Damgård 结构,简单说就是把消息切块、迭代压缩。SHA-2 的安全强度至今够用,但学术界已经在盯它的长度扩展攻击——你知道了 H(M),可以在不知道 M 的情况下算出 H(M||padding||ext)。
SHA-3 是 NIST 在 2015 年定标的新标准,和 SHA-2 完全不同的设计思路——海绵结构。它用固定宽度的置换函数,吸收输入、挤出输出。好处是天然免疫长度扩展攻击,设计上更灵活。
我的建议:新项目优先选 SHA-256 或 SHA-3-256。SHA-256 生态最成熟,硬件加速普遍。SHA-3 适合需要防扩展攻击的场景,比如区块链里的交易哈希。别用 SHA-1,虽然现在还没人公开碰撞攻击,但谷歌已经演示过实际碰撞(SHAttered),企业合规风险太高。
SM3:国产密码标准
SM3 是国家密码管理局发布的商用密码哈希算法,输出 256 位。它和 SHA-256 结构类似,但做了多轮移位和异或优化,安全强度不低于 SHA-256。在党政机关、关键基础设施项目里,SM3 是强制要求。
我在一个政务云项目中遇到过:客户要求所有签名哈希必须用 SM3,但第三方 SDK 只支持 SHA-256。最后我们封装了一层 OID 转换代理,才把两边串起来。
| 算法 | 输出长度 | 结构 | 安全强度(位) | 适用场景 |
|---|---|---|---|---|
| SHA-256 | 256 bit | Merkle–Damgård | 128 | 通用,证书签名,文件完整性 |
| SHA-3-256 | 256 bit | 海绵结构 | 128 | 区块链、防长度扩展场景 |
| SM3 | 256 bit | Merkle–Damgård 变种 | 128 | 国密合规、政企项目 |
HMAC:带密钥的消息认证
哈希本身不防篡改攻击。你收到文件加哈希值,如果中间人同时改了文件值和哈希值,你根本发现不了。解决方案是 HMAC——用密钥参与哈希计算。公式原理不复杂:
HMAC(K, M) = H((K' ⊕ opad) || H((K' ⊕ ipad) || M))
其中 opad/ipad 是固定填充,K' 是密钥处理后值。我习惯把 HMAC 理解为「哈希 + 密钥的混合盐」,它提供了消息认证码——只有知道密钥的双方才能验证。
核心要点:HMAC 的安全性依赖于底层哈希的抗碰撞能力和密钥保密性。哪怕用 SHA-1 做 HMAC(HMAC-SHA1),目前依然未被攻破,但建议用 HMAC-SHA256 或 HMAC-SM3。
使用场景:
- API 请求签名(如 AWS Signature V4、阿里云签名)
- JWT 的 HS256 算法
- 安全通道中的消息完整性校验
我曾经…… 调试过一个生产故障,API 签名总失败。查了半天发现两端对密钥的编码不一致(Hex vs Base64)。嗯,编码约定必须在接口文档里写死,否则坑死人。
PBKDF2:密码存储专用活
用户密码绝不能存明文,甚至不能简单哈希一次。攻击者可以用彩虹表或暴力碰撞把弱口令翻出来。PBKDF2 的核心思路是「慢」——通过可配置的迭代次数,让每次密码验证都消耗固定计算量。
PBKDF2 的公式本质上就是反复 HMAC:
DK = T1 || T2 || ... || Tdklen/hlen
T1 = F(P, S, c, 1)
F(P, S, c, i) = U1 ⊕ U2 ⊕ ... ⊕ Uc
U1 = HMAC(P, S || INT(i))
U2 = HMAC(P, U1)
Uc = HMAC(P, Uc-1)
参数说明:P=密码,S=盐(随机生成),c=迭代次数,dklen=输出长度。
避坑指南:别用 PBKDF2 做密钥派生(比如从密码派生出 AES 密钥)。它的抗 GPU/ASIC 能力不如 bcrypt 或 Argon2。密码存储可以选 PBKDF2,但迭代次数至少 10 万次起步。我见过三年前的项目迭代次数只有 1000,放到今天一秒能破几百万个。
实际代码片段(Python):
import hashlib, os
def hash_password(password: str) -> bytes:
salt = os.urandom(16)
dk = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)
return salt + dk # 盐和哈希一起存
def verify_password(password: str, stored: bytes) -> bool:
salt, dk = stored[:16], stored[16:]
return dk == hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)
我的习惯:盐长度用 16 字节(128 bit),每次注册随机生成。迭代次数每年评估一次,按硬件性能往上调。业务可以接受 200ms 以内的验证耗时,超过的话考虑升级硬件或切到 Argon2。
选型对比
简单总结一下怎么选:
- 文件完整性 / 普通哈希:SHA-256 或 SM3,优先级 SM3(国内合规要求)
- 防长度扩展攻击:SHA-3-256
- 消息认证 / 签名:HMAC-SHA256 或 HMAC-SM3
- 密码存储:PBKDF2(迭代 10 万以上),或用 bcrypt(更抗 GPU)
又一个我踩过的坑:别把密码哈希和消息认证混用。有人图省事,直接拿 PBKDF2 的结果当 HMAC 的 key,结果后续改迭代次数时所有签名都失效。密码哈希和消息认证是两个独立功能,密钥生命周期管理必须分开。