第4章 Diffie-Hellman 密钥交换:两方 DH 协议原理、安全性假设与中间人攻击
从对称加密到密钥协商——为什么需要 DH?
刚接触密码学的时候,我总觉得对称加密挺完美:AES 跑得又快,密钥管理麻烦点而已。后来真正做联邦学习项目,遇到一个尴尬场景——两个参与方想一起训练模型,但网络信道是公开的,密钥怎么安全地传给对方?
说白了,对称加密的前提是双方手里已经有同一个密钥。可在不可信网络上,“先有密钥”这件事本身就是个鸡生蛋问题。Diffie-Hellman 就是这个问题的破局点——它让双方在公开信道上协商出一个共享密钥,而不需要提前约定任何秘密。
两方 DH 协议核心流程
先看最经典的版本。Alice 和 Bob 想得到一个只有他俩知道的共享密钥。公开参数有两个:一个大素数 \( p \),一个模 \( p \) 下的生成元 \( g \)。这两个参数可以公开,哪怕被攻击者知道也无所谓。
- 生成私钥:Alice 随机选一个整数 \( a \)(私密),Bob 随机选一个整数 \( b \)(私密)。
- 计算公钥:Alice 计算 \( A = g^a \bmod p \),Bob 计算 \( B = g^b \bmod p \)。
- 交换公钥:Alice 把 \( A \) 发给 Bob,Bob 把 \( B \) 发给 Alice。这个交换全程公开。
- 计算共享密钥:Alice 计算 \( K = B^a \bmod p \),Bob 计算 \( K = A^b \bmod p \)。由于 \( B^a = (g^b)^a = g^{ab} = (g^a)^b = A^b \),两人得到相同的 \( K \)。
核心数学原理:离散对数困难性。从 \( A = g^a \bmod p \) 反推 \( a \) 在计算上不可行,因此就算攻击者截获了 \( A \) 和 \( B \),也无法算出 \( g^{ab} \bmod p \)。
我习惯把 DH 比作一个“颜料混合”游戏:双方各自选一种秘密颜色,混合公开颜料后交换混合结果,最后各自加入自己的秘密颜色得到最终色。攻击者虽然看到了中间混合色,但无法分离原始颜色。
安全性假设——你到底信什么?
DH 的安全性不是无条件的,它依赖以下几个关键假设。我在项目中踩过其中一个的坑,后面详细说。
| 假设 | 说明 | 实际影响 |
|---|---|---|
| 离散对数困难 | 给定 \( g^a \bmod p \),求 \( a \) 在计算上不可行 | 必须选择足够大的素数 \( p \)(至少 2048 位) |
| 计算 Diffie-Hellman(CDH)假设 | 给定 \( g^a \)、\( g^b \),无法计算 \( g^{ab} \) | 比离散对数稍强,是安全证明的基础 |
| 判定 Diffie-Hellman(DDH)假设 | 无法区分 \( g^{ab} \) 和随机数 | 用于证明协议能抵抗被动窃听 |
| 信道不可篡改 | 公钥交换过程未被篡改或替换 | 如果信道被中间人控制,DH 不提供任何保护 |
实际部署时,我建议优先使用标准化素数(如 RFC 3526 中的组),而不是随机生成。自己生成素数容易引入弱点,我曾见过一个项目用 512 位素数,结果被暴力破解,教训很深。
中间人攻击——DH 最大的软肋
你看,DH 协议里没有任何身份认证机制。这就给了中间人攻击(MITM)可乘之机。我来描述一下典型攻击流程:
- 攻击者 Eve 拦截 Alice 发给 Bob 的公钥 \( A \),将其替换为自己的 \( A' \)。
- Eve 拦截 Bob 发给 Alice 的公钥 \( B \),将其替换为自己的 \( B' \)。
- Alice 以为在和 Bob 通信,实际与 Eve 共享了 \( K_{AE} = (B')^a \)。
- Bob 以为在和 Alice 通信,实际与 Eve 共享了 \( K_{BE} = (A')^b \)。
- Eve 可以解密 Alice 发给 Bob 的任何消息,再用自己的密钥重新加密转发给 Bob,反之亦然。
为什么会这样?因为 DH 只保证“如果你和正确的人交换了公钥,就能协商出只有你俩知道的密钥”,但它不保证“你收到的公钥一定来自正确的人”。这就像你给朋友寄了个带锁的盒子,半路被人换了锁,你还浑然不知。
重要提醒:纯 DH 协议在公开信道下不安全。必须在协议外部附加身份认证机制,比如数字签名、证书、预共享密钥等。联邦学习场景中,我通常会让每个参与方先通过 CA 注册公钥证书,再运行 DH,这样既保证了密钥协商安全,也防止了中间人。
一张图看懂 DH 与中间人攻击
下面这张 SVG 图展示了正常 DH 流程,也标注了中间人攻击的介入点。你可以对比看看哪里出了问题。
避坑指南与实战经验
我曾经在一个联邦学习原型里,因为赶进度直接用 Python 的 pow(g, a, p) 实现 DH,参数填了 512 位的 p,结果被安全审计一顿批。后来改成 2048 位,并加了签名认证,才真正可用。
- 素数 p 的位数不能低:2025 年的标准是至少 2048 位,联邦学习场景中建议用 3072 位或更高。
- 一定要检验接收的公钥:收到对方公钥后,检查是否在 \( [2, p-2] \) 范围内。我遇到过有人传了 0 或 1 导致密钥退化的 bug。
- 配合身份认证使用:纯 DH 不防中间人,必须叠加数字签名或证书。联邦学习的参与者通常有 ID 证书,正好拿来签名公钥交换消息。
- 小心随机数生成:私钥 a、b 必须是强随机数。我曾经在生产环境发现某硬件随机数生成器在刚开机时输出弱随机,导致密钥可预测。现在我会在生成后做一次健康检查。
聊一点我个人的习惯:在多参与方场景下,我倾向于使用 ECDH(椭圆曲线 Diffie-Hellman),它用更短的密钥达到相同安全等级,通信带宽也小很多。联邦学习里参与方往往有几十个,用 ECDH 能显著减少公钥传输量。
最后总结下 DH 的定位:它解决的是“无预先秘密的密钥协商”问题,但不解决身份认证。把这两个概念混在一起,是我见过最多新手踩的坑。搞懂了 DH,接下来就可以把它推广到多方场景,那就是后面要讲的内容了。