4. Diffie-Hellman 密钥交换原理
聊到 TLS 1.3 的密钥交换,绕不开 Diffie-Hellman。说实话,我第一次接触 DH 时觉得它像魔术——两个人公开交换几个数字,最后能算出只有他俩才知道的秘密。后来亲手实现了两遍,才明白背后其实就靠一个数学难题:离散对数问题。
4.1 离散对数问题:DH 的安全根基
先看一个简单场景。我们有一个素数 \(p\) 和一个生成元 \(g\)(\(g\) 是模 \(p\) 的原根)。给定 \(a\),计算 \(g^a \bmod p\) 很容易。但反过来,给定 \(g^a \bmod p\),要找出 \(a\) 就极其困难。这就是离散对数问题。
我当年在学校里觉得这抽象得很,直到做了实际项目才理解:“嗯,这里的难点不是正向计算,而是单向性。” 说白了,DH 安全依赖的就是这个不对称的难度。
核心结论:离散对数问题(DLP)是目前公认的“计算上不可行”的问题。只要素数 \(p\) 足够大(比如 2048 位),攻击者无法在合理时间内逆向推进。
一个小提醒:并不是所有 \(g\) 和 \(p\) 都安全。我曾在遗留系统里见过有人用了 \(g=2\) 且 \(p\) 是安全素数,结果没问题;但如果你自己随便挑一个组合,搞不好会留下弱点。建议直接用标准化的 DH 参数组(RFC 7919 或 RFC 3526)。
4.2 DH 参数:选对才能防坑
DH 参数就三个东西:素数 \(p\)、生成元 \(g\)、模运算。看起来简单,但参数选不好,安全归零。
| 参数 | 说明 | 推荐值 / 注意事项 |
|---|---|---|
| \(p\) | 安全素数(形式 \(2q+1\),\(q\) 为素数) | 至少 2048 位;TLS 1.3 固定使用 FFDHE 组 |
| \(g\) | 模 \(p\) 的生成元或循环子群生成元 | 常用 2;需验证 \(g^{(p-1)/2} \neq 1 \bmod p\) |
| 私钥 \(a, b\) | 通信方各自随机选取的整数 | 长度与 \(p\) 相当;必须使用安全随机数 |
曾经踩过的坑:我在一个嵌入式设备中发现固件写死了 \(p=512\) 位,且 \(g=5\)。用现代硬件几分钟就能破解。所以——DH 参数不能自己瞎编,用标准组是最省心的选择。
4.3 共享密钥计算:一步步拆解
现在看 DH 的具体流程。假设 Alice 和 Bob 想协商一个共享密钥。
- 约定参数:双方预先知道 \(p\) 和 \(g\)(比如从证书或配置中获取)。
- 生成私钥:Alice 选随机数 \(a\),Bob 选 \(b\)。私钥始终保密。
- 计算公钥:Alice 算 \(A = g^a \bmod p\),Bob 算 \(B = g^b \bmod p\)。公钥公开传输。
- 交换公钥:Alice 收到 \(B\),Bob 收到 \(A\)。中间人可以看见 \(A\) 和 \(B\)。
- 计算共享密钥:Alice 算 \(S = B^a \bmod p\),Bob 算 \(S = A^b \bmod p\)。结果相同。
为什么相同?因为 \(B^a = (g^b)^a = g^{ab} \bmod p\),而 \(A^b = (g^a)^b = g^{ab} \bmod p\)。数学保证了两边一致。
举个例子,参数很小只是为了演示:
p = 23, g = 5
Alice 选 a = 6, 计算 A = 5^6 mod 23 = 15625 mod 23 = 8
Bob 选 b = 15, 计算 B = 5^15 mod 23 = 30517578125 mod 23 = 19
共享密钥:Alice 算 19^6 mod 23 = 47045881 mod 23 = 2
Bob 算 8^15 mod 23 = 35184372088832 mod 23 = 2
最后双方都得到 2,而中间人只能看到 \(p=23, g=5, A=8, B=19\)。要算出 2 就必须知道 \(a\) 或 \(b\),而这等价于解离散对数问题。
实际工程注意:示例中的参数太小,几秒钟就能攻破。生产环境必须用 2048 位以上。TLS 1.3 强制使用 FFDHE 或 ECDHE(椭圆曲线版本),后者效率更高。
4.4 知识体系总览
4.5 常见误区与避坑
- 重用私钥:DH 私钥最好每次连接都重新生成。我见过一个项目把私钥写死,结果重放攻击直接攻破。
- 不验证公钥:如果中间人篡改了公钥,双方计算出的共享密钥就不同。TLS 1.3 通过签名和握手哈希解决了这一点。
- 参数太弱:500 位的 \(p\) 在 2010 年后就不可靠了。现在至少 2048 位,或者直接用椭圆曲线。
- 忽略随机数质量:随机数不随机,私钥就能被猜出。用
/dev/urandom或系统提供的 CSPRNG。
曾经踩过的坑:调试一个 DH 库时,输出总对不上。查了半天发现是字节序问题——大端小端不统一导致共享密钥不同。记得在协议层面明确编码约定。