第4章 Diffie-Hellman 密钥交换:两方 DH 协议原理、安全性假设与中间人攻击

从对称加密到密钥协商——为什么需要 DH?

刚接触密码学的时候,我总觉得对称加密挺完美:AES 跑得又快,密钥管理麻烦点而已。后来真正做联邦学习项目,遇到一个尴尬场景——两个参与方想一起训练模型,但网络信道是公开的,密钥怎么安全地传给对方?

说白了,对称加密的前提是双方手里已经有同一个密钥。可在不可信网络上,“先有密钥”这件事本身就是个鸡生蛋问题。Diffie-Hellman 就是这个问题的破局点——它让双方在公开信道上协商出一个共享密钥,而不需要提前约定任何秘密。

两方 DH 协议核心流程

先看最经典的版本。Alice 和 Bob 想得到一个只有他俩知道的共享密钥。公开参数有两个:一个大素数 \( p \),一个模 \( p \) 下的生成元 \( g \)。这两个参数可以公开,哪怕被攻击者知道也无所谓。

  1. 生成私钥:Alice 随机选一个整数 \( a \)(私密),Bob 随机选一个整数 \( b \)(私密)。
  2. 计算公钥:Alice 计算 \( A = g^a \bmod p \),Bob 计算 \( B = g^b \bmod p \)。
  3. 交换公钥:Alice 把 \( A \) 发给 Bob,Bob 把 \( B \) 发给 Alice。这个交换全程公开。
  4. 计算共享密钥: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)可乘之机。我来描述一下典型攻击流程:

  1. 攻击者 Eve 拦截 Alice 发给 Bob 的公钥 \( A \),将其替换为自己的 \( A' \)。
  2. Eve 拦截 Bob 发给 Alice 的公钥 \( B \),将其替换为自己的 \( B' \)。
  3. Alice 以为在和 Bob 通信,实际与 Eve 共享了 \( K_{AE} = (B')^a \)。
  4. Bob 以为在和 Alice 通信,实际与 Eve 共享了 \( K_{BE} = (A')^b \)。
  5. Eve 可以解密 Alice 发给 Bob 的任何消息,再用自己的密钥重新加密转发给 Bob,反之亦然。

为什么会这样?因为 DH 只保证“如果你和正确的人交换了公钥,就能协商出只有你俩知道的密钥”,但它不保证“你收到的公钥一定来自正确的人”。这就像你给朋友寄了个带锁的盒子,半路被人换了锁,你还浑然不知。

重要提醒:纯 DH 协议在公开信道下不安全。必须在协议外部附加身份认证机制,比如数字签名、证书、预共享密钥等。联邦学习场景中,我通常会让每个参与方先通过 CA 注册公钥证书,再运行 DH,这样既保证了密钥协商安全,也防止了中间人。

一张图看懂 DH 与中间人攻击

下面这张 SVG 图展示了正常 DH 流程,也标注了中间人攻击的介入点。你可以对比看看哪里出了问题。

两方 DH 协议流程 & 中间人攻击示意 公共参数 p, g Alice 私钥 a Bob 私钥 b A = g^a mod p B = g^b mod p Eve 替换为 A' Eve 替换为 B' K_AE = B'^a K_BE = A'^b Alice 和 Bob 各自以为自己协商成功 实际上都只与 Eve 共享了密钥

避坑指南与实战经验

我曾经在一个联邦学习原型里,因为赶进度直接用 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,接下来就可以把它推广到多方场景,那就是后面要讲的内容了。