
本文主要翻译自 Practical-Cryptography-for-Developers-Book,笔者额外补充了 DHKE/ECDH 的代码示例,以及「PFS 完美前向保密协议 DHE/ECDHE」一节。
《写给开发人员的实用密码学》系列文章目录:
在密码学中密钥交换是一种协议,功能是在两方之间安全地交换加密密钥,其他任何人都无法获得密钥的副本。通常各种加密通讯协议的第一步都是密钥交换。
密钥交换技术具体来说有两种方案:
密钥交换协议无时无刻不在数字世界中运行,在你连接 WiFi 时,或者使用 HTTPS 协议访问一个网站,都会执行密钥交换协议。
密钥交换可以基于匿名的密钥协商协议如 DHKE,一个密码或预共享密钥,一个数字证书等等。有些通讯协议只在开始时交换一次密钥,而有些协议则会随着时间的推移不断地交换密钥。
认证密钥交换(AKE)是一种会同时认证相关方身份的密钥交换协议,比如个人 WiFi 通常就会使用 password-authenticated key agreement (PAKE),而如果你连接的是公开 WiFi,则会使用匿名密钥交换协议。
目前有许多用于密钥交换的密码算法。其中一些使用公钥密码系统,而另一些则使用更简单的密钥交换方案(如 Diffie-Hellman 密钥交换);其中有些算法涉及服务器身份验证,也有些涉及客户端身份验证;其中部分算法使用密码,另一部分使用数字证书或其他身份验证机制。下面列举一些知名的密钥交换算法:
迪菲-赫尔曼密钥交换(Diffie–Hellman Key Exchange)是一种安全协议,它可以让双方在完全没有对方任何预先信息的条件下通过不安全信道安全地协商出一个安全密钥,而且任何窃听者都无法得知密钥信息。
这个密钥可以在后续的通讯中作为对称密钥来加密通讯内容。
DHKE 可以防范嗅探攻击(窃听),但是无法抵挡中间人攻击(中继)。
DHKE 有两种实现方案:
为了理解 DHKE 如何实现在「大庭广众之下」安全地协商出密钥,我们首先使用色彩混合来形象地解释下它大致的思路。
跟编程语言的 Hello World 一样,密钥交换的解释通常会使用 Alice 跟 Bob 来作为通信双方。
现在他俩想要在公开的信道上,协商出一个秘密色彩出来,但是不希望其他任何人知道这个秘密色彩。他们可以这样做:

分步解释如下:
DHKE 协议也是基于类似的原理,但是使用的是离散对数(discrete logarithms)跟模幂(modular exponentiations )而不是色彩混合。
首先介绍下「模幂」,它是指求 \(g\) 的 \(a\) 次幂模 \(p\) 的值 \(c\) 的过程,其中 \(g\) \(a\) \(c\) 均为整数,公式如下:
已知使用计算机计算上述「模幂」是非常快速的,但是求它的逆运算——即离散对数,在已知 \(g\) \(p\) \(c\) 的情况下,求幂指数 \(a\)——却是非常难的。
下面该轮到 Alice 跟 Bob 出场来介绍 DHKE 的过程了,先看图(下面绿色表示非秘密信息,红色表示秘密信息):

在最常见的 DHKE 实现中(RFC3526),基数是 \(g = 2\),模数 \(p\) 是一个 1536 到 8192 比特的大素数。
而整数 \(A\) \(B\) 通常会使用非常大的数字(1024、2048 或 4096 比特甚至更大)以防范暴力破解。
DHKE 协议基于 Diffie-Hellman 问题的实际难度,这是计算机科学中众所周知的离散对数问题(DLP)的变体,目前还不存在有效的算法。
使用 Python 演示下大概是这样:
# pip install cryptography==36.0.1from cryptography.hazmat.primitives import hashesfrom cryptography.hazmat.primitives.asymmetric import dh# 1. 双方协商使用两个独特的正整数 g 与 p## generator => 即基数 g,通常使用 2, 有时也使用 5## key_size => 模数 p 的长度,通常使用 2048-3096 位(2048 位的安全性正在减弱)params = dh.generate_parameters(generator=2, key_size=2048)param_numbers = params.parameter_numbers()g = param_numbers.g # => 肯定是 2p = param_numbers.p # => 一个 2048 位的整数print(f"{g=}, {p=}")# 2. Alice 生成自己的秘密整数 a 与公开整数 Aalice_priv_key = params.generate_private_key()a = alice_priv_key.private_numbers().xA = alice_priv_key.private_numbers().public_numbers.yprint(f"{a=}")print(f"{A=}")# 3. Bob 生成自己的秘密整数 b 与公开整数 Bbob_priv_key = params.generate_private_key()b = bob_priv_key.private_numbers().xB = bob_priv_key.private_numbers().public_numbers.yprint(f"{b=}")print(f"{B=}")# 4. Alice 与 Bob 公开交换整数 A 跟 B(即各自的公钥)# 5. Alice 使用 a B 与 p 计算出共享密钥## 首先使用 B p g 构造出 bob 的公钥对象(实际上 g 不参与计算)bob_pub_numbers = dh.DHPublicNumbers(B, param_numbers)bob_pub_key = bob_pub_numbers.public_key()## 计算共享密钥alice_shared_key = alice_priv_key.exchange(bob_pub_key)# 6. Bob 使用 b A 与 p 计算出共享密钥## 首先使用 A p g 构造出 alice 的公钥对象(实际上 g 不参与计算)alice_pub_numbers = dh.DHPublicNumbers(A, param_numbers)alice_pub_key = alice_pub_numbers.public_key()## 计算共享密钥bob_shared_key = bob_priv_key.exchange(alice_pub_key)# 两者应该完全相等, Alice 与 Bob 完成第一次密钥交换alice_shared_key == bob_shared_key# 7. Alice 与 Bob 使用 shared_key 进行对称加密通讯Elliptic-Curve Diffie-Hellman (ECDH) 是一种匿名密钥协商协议,它允许两方,每方都有一个椭圆曲线公钥-私钥对,它的功能也是让双方在完全没有对方任何预先信息的条件下通过不安全信道安全地协商出一个安全密钥。
ECDH 是经典 DHKE 协议的变体,其中模幂计算被椭圆曲线的乘法计算取代,以提高安全性。
ECDH 跟前面介绍的 DHKE 非常相似,只要你理解了椭圆曲线的数学原理,结合前面已经介绍了的 DHKE,基本上可以秒懂。
我会在后面「非对称算法」一文中简单介绍椭圆曲线的数学原理,不过这里也可以先提一下 ECDH 依赖的公式(其中 \(a, b\) 为常数,\(G\) 为椭圆曲线上的某一点的坐标 \((x, y)\)):
这个公式还是挺直观的吧,感觉小学生也能理解个大概。
下面简单介绍下 ECDH 的流程:
这样两方就通过 ECDH 完成了密钥交换。
而 ECDH 的安全性,则由 ECDLP 问题提供保证。
这个问题是说,「通过公开的 \(kG\) 以及 \(G\) 这两个参数,目前没有有效的手段能快速求解出 \(k\) 的值。」
从上面的流程中能看到,公钥就是 ECDLP 中的 \(kG\),另外 \(G\) 也是公开的,而私钥就是 ECDLP 中的 \(k\)。
因为 ECDLP 问题的存在,攻击者破解不出 Alice 跟 Bob 的私钥。
代码示例:
# pip install tinyec # ECC 曲线库from tinyec import registryimport secretsdef compress(pubKey): return hex(pubKey.x) + hex(pubKey.y % 2)[2:]curve = registry.get_curve('brainpoolP256r1')alicePrivKey = secrets.randbelow(curve.field.n)alicePubKey = alicePrivKey * curve.gprint("Alice public key:", compress(alicePubKey))bobPrivKey = secrets.randbelow(curve.field.n)bobPubKey = bobPrivKey * curve.gprint("Bob public key:", compress(bobPubKey))print("Now exchange the public keys (e.g. through Internet)")aliceSharedKey = alicePrivKey * bobPubKeyprint("Alice shared key:", compress(aliceSharedKey))bobSharedKey = bobPrivKey * alicePubKeyprint("Bob shared key:", compress(bobSharedKey))print("Equal shared keys:", aliceSharedKey == bobSharedKey)前面介绍的经典 DHKE 与 ECDH 协议流程,都是在最开始时交换一次密钥,之后就一直使用该密钥通讯。
因此如果密钥被破解,整个会话的所有信息对攻击者而言就完全透明了。
为了进一步提高安全性,密码学家提出了「完全前向保密(Perfect Forward Secrecy,PFS)」的概念,并在 DHKE 与 ECDH 的基础上提出了支持 PFS 的 DHE/ECDHE 协议(末尾的 E 是 ephemeral 的缩写,即指所有的共享密钥都是临时的)。
完全前向保密是指长期使用的主密钥泄漏不会导致过去的会话密钥泄漏,从而保护过去进行的通讯不受密码或密钥在未来暴露的威胁。
下面使用 Python 演示下 DHE 协议的流程(ECDHE 的流程也完全类似):
# pip install cryptography==36.0.1from cryptography.hazmat.primitives import hashesfrom cryptography.hazmat.primitives.asymmetric import dh# 1. 双方协商使用两个独特的正整数 g 与 p## generator => 即基数 g,通常使用 2, 有时也使用 5## key_size => 模数 p 的长度,通常使用 2048-3096 位(2048 位的安全性正在减弱)params = dh.generate_parameters(generator=2, key_size=2048)param_numbers = params.parameter_numbers()g = param_numbers.g # => 肯定是 2p = param_numbers.p # => 一个 2048 位的整数print(f"{g=}, {p=}")# 2. Alice 生成自己的秘密整数 a 与公开整数 Aalice_priv_key = params.generate_private_key()a = alice_priv_key.private_numbers().xA = alice_priv_key.private_numbers().public_numbers.yprint(f"{a=}")print(f"{A=}")# 3. Bob 生成自己的秘密整数 b 与公开整数 Bbob_priv_key = params.generate_private_key()b = bob_priv_key.private_numbers().xB = bob_priv_key.private_numbers().public_numbers.yprint(f"{b=}")print(f"{B=}")# 4. Alice 与 Bob 公开交换整数 A 跟 B(即各自的公钥)# 5. Alice 使用 a B 与 p 计算出共享密钥## 首先使用 B p g 构造出 bob 的公钥对象(实际上 g 不参与计算)bob_pub_numbers = dh.DHPublicNumbers(B, param_numbers)bob_pub_key = bob_pub_numbers.public_key()## 计算共享密钥alice_shared_key = alice_priv_key.exchange(bob_pub_key)# 6. Bob 使用 b A 与 p 计算出共享密钥## 首先使用 A p g 构造出 alice 的公钥对象(实际上 g 不参与计算)alice_pub_numbers = dh.DHPublicNumbers(A, param_numbers)alice_pub_key = alice_pub_numbers.public_key()## 计算共享密钥bob_shared_key = bob_priv_key.exchange(alice_pub_key)# 上面的流程跟经典 DHKE 完全一致,代码也是从前面 Copy 下来的# 但是从这里开始,进入 DHE 协议补充的部分shared_key_1 = bob_shared_key # 第一个共享密钥# 7. 假设 Bob 现在要发送消息 M_b_1 给 Alice## 首先 Bob 使用对称加密算法加密消息 M_bM_b_1 = "Hello Alice, I'm bob~"C_b_1 = Encrypt(M_b_1, shared_key_1) # Encrypt 是某种对称加密方案的加密算法,如 AES-256-CTR-HMAC-SHA-256## 然后 Bob 需要生成一个新的公私钥 b_2 与 B_2(注意 g 与 p 两个参数是不变的)bob_priv_key_2 = parameters.generate_private_key()b_2 = bob_priv_key.private_numbers().xB_2 = bob_priv_key.private_numbers().public_numbers.yprint(f"{b_2=}")print(f"{B_2=}")# 8. Bob 将 C_b_1 与 B_2 一起发送给 Alice# 9. Alice 首先解密数据 C_b_1 得到原始消息 M_b_1assert M_b_1 == Decrypt(C_b_1, shared_key_1) # Dncrypt 是某种对称加密方案的解密算法,如 AES-256-CTR-HMAC-SHA-256## 然后 Alice 也生成新的公私钥 a_2 与 A_2alice_priv_key_2 = parameters.generate_private_key()## Alice 使用 a_2 B_2 与 p 计算出新的共享密钥 shared_key_2bob_pub_numbers_2 = dh.DHPublicNumbers(B_2, param_numbers)bob_pub_key_2 = bob_pub_numbers_2.public_key()shared_key_2 = alice_priv_key_2.exchange(bob_pub_key_2)# 10. Alice 回复 Bob 消息时,使用新共享密钥 shared_key_2 加密消息得到 C_a_1# 然后将密文 C_a_1 与 A_2 一起发送给 Bob# 11. Bob 使用 b_2 A_2 与 p 计算出共享密钥 shared_key_2# 然后再使用 shared_key_2 解密数据# Bob 在下次发送消息时,会生成新的 b_3 与 B_3,将 B_3 随密文一起发送## 依次类推通过上面的代码描述我们应该能理解到,Alice 与 Bob 每次交换数据,实际上都会生成新的临时共享密钥,公钥密钥在每次数据交换时都会更新。
即使攻击者破解了花费了很大的代价破解了其中某一个临时共享密钥 shared_key_k(或者该密钥因为某种原因泄漏了),它也只能解密出其中某一次数据交换的信息 M_b_k,其他所有的消息仍然是保密的,不受此次攻击(或泄漏)的影响。