Diffie-Hellman密钥交换 — 即使被窃听,秘密也不会泄露的原因

“如果敌人正在窃听所有对话,我们如何才能创造一个只属于你我的秘密?”

1976年,两位天才对这个看似矛盾的问题给出了答案。

>

>

 

本文涵盖内容

  • Diffie-Hellman (DH)出现的历史背景
  • 通过油漆比喻理解其直观工作原理
  • 模幂运算和离散对数问题的数学基础
  • 使用Python代码亲手实现密钥交换
  • 中间人攻击 (MITM)及其向ECDH的演进
  • 在TLS·SSH等实际环境中的应用

引言 — 如何在见面之前共享秘密密钥

出乎意料的是,加密最大的难题并非“加密算法”本身。真正困难的是如何安全地共享用于加密的密钥

1976年以前,两个人若要进行秘密通信,必须事先亲自见面共享相同的密钥,或者通过可信的信使(courier)传递写有密钥的纸张。您可以联想到冷战时期的谍战电影。

然而,在互联网时代该如何操作呢?从未谋面的服务器和客户端,如何在所有通信都可能被窃听的公共网络上,创建出只属于他们两人的秘密密钥呢?

解决这个看似矛盾的问题的,正是由Whitfield DiffieMartin Hellman提出的Diffie-Hellman密钥交换算法。这项发明奠定了现代互联网安全的基础,并于2015年为两人赢得了图灵奖。


油漆比喻 — 5分钟理解DH

在深入数学原理之前,我们先来看一个最直观地解释DH的油漆比喻

  1. 爱丽丝和鲍勃公开约定一种黄色油漆。敌人也能看到。
  2. 爱丽丝将她自己的秘密颜色(红色)混入黄色中,制成橙色,然后发送给鲍勃。
  3. 鲍勃也将他自己的秘密颜色(蓝色)混入黄色中,制成绿色,然后发送给爱丽丝。
  4. 爱丽丝将她收到的绿色与她自己的秘密颜色(红色)混合 → 黄+蓝+红
  5. 鲍勃将他收到的橙色与他自己的秘密颜色(蓝色)混合 → 黄+红+蓝

结果,两人都得到了相同的颜色。然而,窃听者只看到了黄色、橙色和绿色。由于从已经混合的油漆中分离出原始的秘密颜色实际上是不可能的,窃听者无法得知最终的秘密颜色。

这种“易于混合但难以分离”的单向性是DH的本质。


数学原理 — 模幂运算与离散对数问题

将油漆比喻转化为数学,如下所示:

公开参数 (所有人都知道的值)

  • 大素数 p
  • p 的原根 g (生成元, generator)

密钥交换步骤

  1. 爱丽丝选择一个秘密值 a,计算公开值 A = g^a mod p,并发送给鲍勃。
  2. 鲍勃选择一个秘密值 b,计算公开值 B = g^b mod p,并发送给爱丽丝。
  3. 爱丽丝计算 K = B^a mod p。
  4. 鲍勃计算 K = A^b mod p。

两人得到的值都精确地等于 g^(ab) mod p。这就是只有两人知道的共享秘密密钥 (shared secret)

窃听者可以知道 p, g, A, B 的所有值,但必须从 A = g^a mod p 中反推出 a。这个问题被称为离散对数问题 (Discrete Logarithm Problem, DLP),对于足够大的 p(通常2048位以上),在现实时间内无法解决。

这与RSA的直观感受类似:乘法容易,但因式分解困难。DH利用了“指数计算容易,但其逆运算(离散对数)困难”的不对称性。


使用Python亲手实现

为了巩固直观理解,我们用小数字亲自操作一下。

# 教学示例 — 严禁用于实际生产环境
p = 23      # 公开素数 (实际应用中至少2048位)
g = 5       # 公开生成元

# 爱丽丝的秘密值
a = 6
A = pow(g, a, p)   # g^a mod p
print(f"앨리스가 공개하는 값 A = {A}")

# 鲍勃的秘密值
b = 15
B = pow(g, b, p)   # g^b mod p
print(f"밥이 공개하는 값 B = {B}")

# 各自计算共享秘密密钥
K_alice = pow(B, a, p)   # B^a mod p
K_bob   = pow(A, b, p)   # A^b mod p

print(f"앨리스의 공유 키: {K_alice}")
print(f"밥의 공유 키:     {K_bob}")
assert K_alice == K_bob

执行后,您会发现双方都得到了相同的密钥 2。

在实际应用中,不应自行实现,而应使用经过验证的库。以下是使用 cryptography 库执行安全DH密钥交换的示例:

from cryptography.hazmat.primitives.asymmetric import dh
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF

# 1) 生成公开参数 (通常由服务器生成一次后重用)
parameters = dh.generate_parameters(generator=2, key_size=2048)

# 2) 爱丽丝、鲍勃各自生成密钥对
alice_priv = parameters.generate_private_key()
bob_priv   = parameters.generate_private_key()

# 3) 交换公钥后导出共享秘密
shared_alice = alice_priv.exchange(bob_priv.public_key())
shared_bob   = bob_priv.exchange(alice_priv.public_key())
assert shared_alice == shared_bob

# 4) 原始秘密不直接使用,而是通过KDF派生对称密钥
derived_key = HKDF(
    algorithm=hashes.SHA256(),
    length=32,
    salt=None,
    info=b"handshake data"
).derive(shared_alice)

这里有一个重要点是,DH生成的共享秘密(raw shared secret)不应直接用作对称密钥。由于其分布可能不均匀,因此必须通过HKDF等密钥派生函数(KDF)进行提炼后才能使用。


⚠️ 注意事项 — DH并非万能

1. 易受中间人攻击 (MITM)

DH不验证对方的身份。如果攻击者马洛里介入爱丽丝和鲍勃之间,会发生以下情况:

  • 爱丽丝 ↔ 马洛里:建立共享密钥 K1
  • 马洛里 ↔ 鲍勃:建立共享密钥 K2
  • 马洛里用 K1 解密所有消息 → 检查/篡改内容 → 用 K2 重新加密后转发

因此,在实际应用中,必须通过数字签名、证书 (PKI)、预共享密钥等方式验证对方的身份。在TLS中,服务器证书就扮演了这一角色。

2. 弱参数是致命的

2015年发布的Logjam攻击利用了许多服务器使用512位短DH参数的弱点。此外,如果未使用足够随机的秘密值 a, b,则会暴露于猜测攻击。使用至少2048位以上的安全素数群(如RFC 7919的ffdhe群)是安全的。

3. 静态DH vs 临时DH (DHE)

静态DH是指一次性生成密钥对并持续重用,这意味着一旦秘密密钥泄露,过去的所有通信都将被解密。相比之下,DHE (Ephemeral DH)为每个会话生成新的密钥对,提供前向保密性 (Forward Secrecy)。这也是TLS 1.3只允许DHE或ECDHE的原因。

4. 对量子计算机脆弱

离散对数问题可以通过Shor算法在多项式时间内解决。如果足够强大的量子计算机出现,DH将被破解。NIST正在推动的后量子密钥交换 (PQC),特别是将ML-KEM(原Kyber)与DH结合的混合方式,正在成为下一代标准。


ECDH — 更小、更快、更强

在椭圆曲线上实现相同思想的便是ECDH (Elliptic Curve Diffie-Hellman)

项目 传统DH ECDH
基础问题 离散对数 (DLP) 椭圆曲线离散对数 (ECDLP)
等效强度密钥大小 2048位 256位
运算速度 较慢 较快
带宽 较大 较小

通过更短的密钥实现相同的安全强度,ECDHE(临时ECDH)已成为现代TLS 1.3、SSH、Signal协议等几乎所有地方的标准。


✅ 总结 / 结束语

  • Diffie-Hellman是第一个允许双方在公开信道上协商秘密密钥的公钥算法。
  • 其核心原理是 g^(ab) mod p 的对称结果以及离散对数问题的计算难度
  • DH仅负责密钥交换,不验证对方身份。为确保安全,必须与签名·证书结合使用。
  • 在实际应用中,使用临时密钥的DHE/ECDHE是标准,它负责TLS的前向保密性
  • 下一步,建议您了解TLS 1.3握手Signal协议的X3DH以及抗量子密钥交换算法ML-KEM (Kyber)

Comments

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注