4、哈希函数与消息认证码
哈希函数,说白了就是一种「单向映射」—— 它能把你输入的任意长度数据,压缩成一个固定长度的指纹(摘要)。这个指纹没法反向还原,而且哪怕原数据只改动一个比特,算出来的指纹也会天差地别。
我个人习惯把哈希函数比作「数字世界的 DNA 检测」:两份数据是不是一样,不用全量比对,只需要看看它们的哈希值是否一致就行了。TLS 握手里很多环节都依赖这个机制。
消息认证码(MAC)则是在哈希的基础上加了一把「钥匙」。光有哈希,坏人可以伪造数据包,因为他可以自己算哈希、自己改数据。但如果算哈希时混入一个只有通信双方才知道的共享密钥,那得到的结果就叫 MAC—— 对方可以验证数据有没有被篡改,而且认证了「这确实是持有密钥的那一方发来的」。
4.1 哈希函数的基本特性
一个合格的密码学哈希函数,必须满足这几个硬指标:
- 抗碰撞性:很难找到两个不同的输入,它们的哈希值是一样的。
- 抗原像性:给定一个哈希值,几乎不可能反推出原始输入内容。
- 抗第二原像性:给定一个输入及其哈希值,很难找到另一个不同的输入,哈希值相同。
- 雪崩效应:输入哪怕只变一个 bit,输出也会大变样。
我在项目里遇到过一种情况:有人想用 MD5 做完整性校验。MD5 曾经是主力,但它的抗碰撞性早就被突破了。两条不同的 PDF 文件可以算出一模一样的 MD5 值,这在安全场景下是致命的。
4.2 常见的哈希算法
TLS 上下文里,你主要会跟这几类哈希打交道:
| 算法 | 输出位长 | 当前状态 | 推荐使用场景 |
|---|---|---|---|
| MD5 | 128 bit | 已告破,禁用 | 无 |
| SHA-1 | 160 bit | 已不推荐 | 仅用于旧版兼容(尽快迁移) |
| SHA-2 (SHA-256 / SHA-384 / SHA-512) | 256 / 384 / 512 bit | 主流,安全 | TLS 1.2/1.3 首选 |
| SHA-3 (Keccak) | 可变 | 新一代,储备 | 新兴系统,TLS 1.3 已支持 |
我记得在升级某家银行的 TLS 套件时,发现他们还在用 SHA-1 做证书签名。我把证书链一抓,提示「签名算法:sha1WithRSAEncryption」。浏览器已经开始亮警告了。换到 SHA-256 后,校验性能几乎没有下降,安全性却上了一个台阶。
你想想看,SHA-256 的输出是 256 位,比 SHA-1 的 160 位多了 96 位。正向碰撞攻击的复杂度从 2^80 飙升到 2^128。这个差距,足够让一台专用攻击机器算到太阳系毁灭也算不出来。
4.3 消息认证码 (HMAC)
HMAC(Hash-based Message Authentication Code)是目前 TLS 里最常用的 MAC 方案。它把哈希函数和密钥揉合在一起,输出一个不可伪造的认证标签。
它的核心做法是:
- 把密钥与一个叫做 ipad 的固定字节串做异或,得到 K⊕ipad。
- 把 K⊕ipad 拼接上消息 M,做一次哈希:H(K⊕ipad || M)。
- 再把密钥与另一个固定字节串 opad 做异或,得到 K⊕opad。
- 把第二步得到的哈希结果拼接在 K⊕opad 后面,再做一次哈希:H(K⊕opad || H(K⊕ipad || M))。
为什么要搞两次哈希?我刚开始学这玩意儿时也觉得多此一举。后来才明白,这么做能有效抵抗长度扩展攻击 —— 如果不加这个「opad 外层」,攻击者在知道 H(K||M) 的情况下,不需要知道密钥也能算出 H(K||M||附加数据),这问题在 TLS 里是致命的。
4.4 HMAC 的工作流程
下面这张图把 HMAC-SHA256 的完整流程画清楚了:
实际 HMAC 的计算固定为两个步骤,不可缩减。我见过有人为了性能只做一次哈希,这是严重的安全缩减。HMAC 的双层结构不是设计冗余,是安全红线。
4.5 哈希函数在 TLS 中的具体作用
哈希和 MAC 在 TLS 里无处不在,我简单列几个关键位置:
- 证书签名验证:CA 用哈希函数对证书内容做摘要,再用私钥签名摘要。客户端用公钥验签并比对哈希值。
- Finished 消息校验:TLS 握手完成后,双方计算一个 PRF+SHA-256 的摘要,互相验证对方确实持有完整会话密钥。
- 密钥派生(KDF):TLS 1.2 的 PRF 使用 HMAC 模式生成主密钥和会话密钥;TLS 1.3 的 HKDF 更是完全基于 HMAC 构建。
- 消息完整性保护:记录层每条数据都附带一个 MAC 标签(TLS 1.2 的 CBC 或 GCM 模式各有用到 MAC 思想)。
我在项目里见过一个典型案例:某 CDN 厂商只升级了证书签名算法到 SHA-256,但密码套件里仍然保留了依赖 MD5 的 TLS_RSA_WITH_RC4_128_MD5。握手里的 Finished 消息依然能用 MD5 碰撞攻击伪造。这说明哈希升级要全面,不能只改面不改里。
4.6 你该用什么哈希?
这是最直接的问题。我的建议很明确:
- TLS 1.2:SHA-256 或 SHA-384。密码套件里带有 SHA-256 后缀的,比如 ECDHE-RSA-AES128-GCM-SHA256,就是优先选择。
- TLS 1.3:只能用 SHA-256 / SHA-384。协议已强制要求,你不需要自己选。
- HMAC:永远选择 HMAC-SHA256。不推荐 HMAC-MD5 或 HMAC-SHA1,哪怕它们理论上还能用。
# 用一个简单的 Python 示例看下 HMAC 使用
import hmac
import hashlib
key = b"shared_secret_32bytes"
message = b"hello tls"
# 计算 HMAC-SHA256
mac = hmac.new(key, message, hashlib.sha256).hexdigest()
print(f"HMAC-SHA256: {mac}")
# 输出: 一个64位的十六进制字符串
嗯,多看多练。你很快就会发现,掌握了哈希和 MAC 的原理后,TLS 握手的每一步都能看懂「为什么这样设计」了。