4. 密钥协商基础:Diffie-Hellman密钥交换
说白了,没有密钥协商,TLS就是个空壳子。
你想想看,通信双方从来没有见过面,怎么就能商量出一个只有你我知道的共享密钥?早期的对称加密需要提前把密钥分发给对方,这在互联网上根本不现实。直到1976年,Diffie和Hellman提出了一个颠覆性的方案——利用数学难题在公开信道上建立共享秘密。嗯,这就是我们今天要聊的DH密钥交换。
核心思想
DH的核心不复杂,就是利用了离散对数问题的“单向性”。把它的流程用大白话说:
- 双方先约定两个公开参数:一个大素数
p和一个生成元g(这两个谁都能看) - 各自在本地生成一个随机私钥(这个保密)
- 通过私钥算出一个公钥,发给对方
- 收到对方的公钥后,结合自己的私钥做一次幂运算,得到完全相同的共享密钥
我在项目中第一次亲眼看到这个流程时,觉得像变魔术。但实际上它就是利用了指数运算和模运算的交换律:(ga)b ≡ (gb)a (mod p)。两边都算gab mod p,结果当然一样。
这就是整个交换流程。我画这张图的时候想了半天,怎么把“公开信道”和“私有计算”同时表现出来——你注意看箭头都是虚线,代表传输内容是公开的;而私钥计算那几步,全都锁在方框里,没有任何传输。
安全性基础
为什么DH安全?核心在于离散对数难题:你能拿到 p、g 和 A = ga mod p,但想从 A 反推出私钥 a,在计算上几乎不可能。这就好比你有一张照片,但想从照片里精确还原出相机镜头的焦距、光圈和快门速度的组合——理论上可行,但实际操作量太大,大到没有意义。
p 必须足够大。我建议不要低于2048位,最好用4096位以上。曾经有人用1024位参数,后来在学术界激起了不少争议——并非完全没有被攻破的风险。
DH的主要变体
我自己做TLS项目时用过的DH变体主要有三种,这里给你整理一下:
| 变体名称 | 描述 | 密钥类型 | 安全级别 | 使用场景 |
|---|---|---|---|---|
| 静态DH (DH) | 双方使用固定私钥,固定公钥可提前分发 | 长期密钥对 | 较低(无前向安全) | 早期TLS、某些VPN |
| 临时DH (DHE) | 每次握手重新生成临时密钥对 | 临时短期密钥 | 高(完美前向安全) | TLS 1.2、TLS 1.3 |
| 椭圆曲线DH (ECDHE) | 椭圆曲线上的DH,同等安全下更短密钥 | 临时短期密钥 | 极高(完美前向安全+高效) | 现代TLS 1.3首选 |
看到表格了吗?静态DH基本被淘汰了。原因很简单:如果私钥泄露,之前所有会话都会被回溯解密。我特别推荐ECDHE,它几乎成了现代TLS的实际标准。
如何实现一个简单的DH
口说无凭,写段代码让你感受一下。这是最简化的Python实现:
import os
import hashlib
# 生成大素数 p 和生成元 g
# 实际场景不应该自己写生成器,用专业库
p = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
g = 2
# Alice 生成私钥和公钥
alice_private = int.from_bytes(os.urandom(32), 'big')
alice_public = pow(g, alice_private, p)
# Bob 生成私钥和公钥
bob_private = int.from_bytes(os.urandom(32), 'big')
bob_public = pow(g, bob_private, p)
# 双方交换公钥(这里模拟交换)
# 收到对方公钥后计算共享密钥
alice_shared = pow(bob_public, alice_private, p)
bob_shared = pow(alice_public, bob_private, p)
assert alice_shared == bob_shared
# 将共享密钥导出为对称密钥(通常需要KDF处理)
shared_key = hashlib.sha256(alice_shared.to_bytes(32, 'big')).digest()
print("共享密钥(十六进制):", shared_key.hex()[:64])
pow(g, private, p) 比先算 g ** private 再取模效率高得多,而且内置支持大数运算。我曾见过同事直接用 ** 然后 % p,在密钥很大时直接内存溢出了……老实用三层参数的 pow() 吧。
一个关键避坑:不要用错q值
我曾经在集成一个安全库时,发现DH协商出来的密钥老是跟对端不一致。排查了半天发现,两边选的生成元g不一样——我把生成元和参数搞混了。是的,虽然是同一个p,但只要g一个不同,结果天差地别。
还有一点:p必须是一个安全素数,即(p-1)/2也是素数。否则可能会有小子群攻击的风险。我看过一些旧协议库的代码,这一点检查得不够严格。建议直接用标准化的参数组,别自己发明创造。
到这里,DH的基础已经讲完了。核心就两点:利用离散对数难题让双方在公开信道协商密钥,以及临时密钥才能保证前向安全。接下来实际看TLS 1.3是怎么用ECDHE来完成握手的——嗯,那将是一次更精妙的配合。