密码学基础:TLS 握手的数学基石
如果你做过网络协议优化,肯定知道 TLS 握手不仅仅是密钥协商。背后的密码学算法才是真正的「地基」。说白了,握手过程就是在用密码学工具解决三个问题:你是谁、你发给我的东西有没有被篡改、我们之间能不能建立一个只有彼此知道的秘密。
这一节我会把对称加密、非对称加密、哈希函数、数字签名和密钥派生串起来讲。别怕数学,咱们只关心它们在握手里的实际角色。
对称加密:速度与安全的平衡点
对称加密用同一个密钥加密和解密。就像你给行李箱上了一把锁,自己用钥匙打开。TLS 握手里真正传输数据用的就是对称加密——原因很简单:它快。
常用的有 AES-GCM、ChaCha20-Poly1305。我自己在项目中偏好 AES-GCM,硬件加速下吞吐量能到几十 Gbps。但注意:对称加密的关键是密钥本身不能泄露。所以握手其实是在安全地交换这个对称密钥。
关键指标:TLS 1.3 中对称加密只允许 AEAD 算法,一次完成加密+认证,避免了 CBC 模式的历史漏洞。我见过不少老系统还在用 AES-CBC,迁移到 1.3 后性能反而提升了。
避坑指南:我曾经接手一个项目,他们的对称加密密钥是写死在配置文件里的。这不是加密,这是掩耳盗铃。密钥必须通过握手阶段的密钥派生得到,不能静态存储。
非对称加密:不共享秘密也能安全通信
非对称加密有一对钥匙:公钥公开,私钥保密。你用公钥加密的数据,只有私钥能解开。反过来,私钥签名的数据,任何持有公钥的人都能验证真伪。
RSA 和 ECDSA 是最常见的。我个人更推荐 ECDSA——相同的安全强度下密钥更短、性能更好。RSA 2048 位的签名计算量比 ECDSA P-256 大了不止一个数量级。
在 TLS 握手里,非对称加密主要用于两个地方:
- 证书验证:服务器用私钥签名证书,客户端用公钥验签,证明服务器身份。
- 密钥交换:TLS 1.2 里可以用 RSA 密钥传输(现在已经不推荐了),TLS 1.3 统一用 ECDHE 做临时密钥协商。
注意:千万别把非对称加密直接用来加密大数据块!它的加解密速度只有对称加密的千分之一。我在一次压力测试中见过有人用 RSA 加密 1MB 的文件……结果那个请求超时了。
哈希函数:数据的指纹
哈希函数把任意长度的输入变成固定长度的输出,比如 SHA-256 输出 256 位。好的哈希函数具有两个特性:
- 单向性:从哈希值反推原始数据几乎不可能。
- 抗碰撞性:几乎找不到两个不同输入拥有相同哈希值。
我在 TLS 握手里,哈希函数主要负责:
- 生成消息摘要(用于数字签名)
- 派生密钥(配合其他算法)
- 计算握手消息的完整性校验(Finished 消息)
SHA-1 已经被淘汰了,现在最低要求 SHA-256。你想想看,2017 年 Google 和 CWI 就成功演示了 SHA-1 碰撞攻击,所以别再用老旧的哈希了。
| 算法 | 输出长度 | 安全状态 | 我的建议 |
|---|---|---|---|
| MD5 | 128 bit | 已攻破 | 不要用 |
| SHA-1 | 160 bit | 存在碰撞攻击 | 迁移到 SHA-256 |
| SHA-256 | 256 bit | 安全 | 标准配置 |
| SHA-384 | 384 bit | 安全 | 更高安全度可选 |
数字签名:让消息不可抵赖
数字签名 = 哈希 + 非对称加密。签名方先对消息算哈希,然后用私钥加密这个哈希。验证方用公钥解密哈希,再自己算一遍消息的哈希,如果一致就说明消息没被篡改且确实是签名方发的。
实际项目里最常见的场景就是证书链验证。我记得有一次排查一个证书异常,发现根证书的签名算法是 SHA-1/RSA,但中间证书用了 SHA-256/ECDSA。操作系统和新浏览器都报了警告,最后只能重新颁发了整个证书链。
原则:签名算法必须和证书密钥类型匹配。EC 密钥不要用 RSA 签名,反之亦然。混合使用会导致握手失败。
经验分享:我曾经把签名验证的顺序搞反了——先验签名再检查证书链。结果被中间人攻击了。正确的做法是:先验证证书链的合法性(包括 CA、有效期、吊销状态),再用证书里的公钥验签。
密钥派生:从共享秘密到会话密钥
握手之后,客户端和服务器有了一个共享秘密(比如通过 ECDHE 交换)。但这个秘密不能直接用做对称加密密钥——它不够随机,长度也可能不够。所以需要密钥派生函数(KDF)来「拉伸」它。
TLS 1.3 使用 HKDF(HMAC-based Key Derivation Function)分两步:
- Extract:从原始的共享秘密中提取出一个伪随机的中间密钥。
- Expand:把这个中间密钥扩展出多个不同用途的子密钥(加密密钥、认证密钥、初始化向量等)。
我手写过一个简易的 HKDF 实现做测试,核心代码其实只有十几行:
def hkdf_extract(salt, ikm):
return hmac_sha256(salt, ikm)
def hkdf_expand(prk, info, length):
t = b""
okm = b""
for i in range(1, (length // 32) + 2):
t = hmac_sha256(prk, t + info + bytes([i]))
okm += t
return okm[:length]
注意盐(salt)很重要。TLS 1.3 把共享秘密和 PSK(预共享密钥)一起作为输入,盐是固定的序列号——这就是为什么握手消息的顺序不能乱。
警告:不要自己写 KDF!我曾经看到一个团队直接拿 SHA-256 对共享秘密做哈希,然后截取前 128 位当密钥。这样生成的密钥缺乏随机性,容易被侧信道攻击。请用标准的 HKDF 或 PBKDF2。
知识体系速览
下面这张图把五个密码学概念在 TLS 握手中的角色串起来了。你可以对照着看:
这张图里每个组件都是 TLS 握手不可或缺的一环。你想想看,缺了哈希函数,数字签名就成了无根之木;缺了密钥派生,共享秘密就没法安全地变成加密密钥。下次遇到握手失败,按这个图逐层排查,大概率能找到问题。
好了,这就是密码学基础的核心。下一话我们正式进入 TLS 握手的具体流程。先消化一下这些概念,后面会频繁用到。