4、TLS 1.3 密钥交换:ECDHE 原理,DH 参数
说到 TLS 1.3 的密钥交换,我第一个想到的就是 ECDHE。
为什么?因为它是整个握手的“安全地基”。没有它,后面的加密都是白搭。
你想想看,客户端和服务器要在不安全的信道上协商出一个只有他俩知道的共享密钥。而且这个密钥还得是“前向安全的”——就算以后私钥泄了,以前的会话记录也解不出来。
TLS 1.3 搞定了这件事。它只用了一套方案:ECDHE。
从 DH 到 ECDHE:数学原理
先说传统 Diffie-Hellman(DH)。
它的核心就是一个模幂运算:
g^a mod p
通信双方选一个大素数 p 和一个生成元 g。各自选一个随机数 a(私钥),算出公钥 A = g^a mod p 发给对方。对方也发来 B = g^b mod p。然后自己算 B^a mod p = g^(ab) mod p,对方算 A^b mod p = g^(ab) mod p。共享密钥就这么诞生了。
嗯,这个叫“有限域上的 DH”(FFDHE)。数学上没问题,但在实践中,p 往往很大(2048 bit 甚至 3072 bit),运算慢,而且 p 一旦选不好就会出安全问题。
椭圆曲线 Diffie-Hellman(ECDH)用的是另一套代数结构——椭圆曲线上的点群。私钥是一个随机整数 d,公钥是 Q = d * G,其中 G 是曲线上的基点。ECDHE 里的“E”代表“Ephemeral”,临时,每次握手都重新生成密钥对。
说白了,ECDHE 就是把 DH 从“整数乘法群”搬到了“椭圆曲线点群”上。同样能交换密钥,但运算更快、密钥更短。举个例子,256 bit 的椭圆曲线安全性≈ 3072 bit 的 RSA,但计算量少了一个数量级。
我在早期的一个嵌入式项目里遇到过坑:设备用的老旧 DH 参数,p 是 1024 bit,计算结果慢得离谱。后来换成 ECDHE,握手时间直接降了 70%。
ECDHE 的工作流程
在 TLS 1.3 中,ECDHE 是握手的第一步。具体流程如下:
- Client 生成临时密钥对
客户端随机选一个私钥sk_c,算出公钥pk_c = sk_c * G。 - Client 发送公钥和密码套件
在ClientHello里带上自己支持的和首选的一个椭圆曲线(Named Group),以及对应的公钥。 - Server 生成临时密钥对
服务器也从自己支持的曲线里选一个(通常和客户端相同),生成自己的sk_s和pk_s。 - Server 发送公钥
在ServerHello里返回选中的曲线和服务器公钥。 - 各自计算共享密钥
客户端:shared_secret = sk_c * pk_s
服务器:shared_secret = sk_s * pk_c
因为椭圆曲线上的点乘法满足结合律,结果一致。
共享密钥算出来之后,后续的会话密钥就靠它派生。注意,这里的“临时”意味着每次握手都会生成新的密钥对——前向安全性就靠这一点保证。
// 伪代码示意
// 客户端
sk_c = random_scalar()
pk_c = sk_c * G
// 服务器
sk_s = random_scalar()
pk_s = sk_s * G
// 共享密钥
client_shared = sk_c * pk_s
server_shared = sk_s * pk_c // 结果相同
TLS 1.3 中的 DH 参数(Named Groups)
TLS 1.3 不再允许随便选 p 和 g。它指定了一组“命名的群(Named Groups)”,我习惯叫它们“标准曲线”。这些曲线都经过密码学界严格审查,性能和安全性有保证。
常用的曲线如下:
| Named Group | 曲线类型 | 位安全强度 | 备注 |
|---|---|---|---|
| x25519 | Curve25519 | 128 bit | 由 Daniel J. Bernstein 设计,安全且高性能 |
| secp256r1 | NIST P-256 | 128 bit | 最广泛的兼容曲线,几乎所有平台都支持 |
| secp384r1 | NIST P-384 | 192 bit | 兼容高安全需求场景 |
| x448 | Goldilocks | 224 bit | 比 x25519 更安全但更慢,少用 |
我个人习惯优先用 x25519。为什么?它在实现上难以被侧信道攻击,而且运算速度奇快。除非遇到老旧设备不兼容,否则我基本不选 secp256r1。
ECDHE 密钥交换流程图
下图展示了 ECDHE 的一次完整交换过程:
为什么 TLS 1.3 选择 ECDHE 作为唯一密钥交换?
在 TLS 1.2 时代,密钥交换选项五花八门:RSA、DH、DHE、ECDHE、PSK……光密钥交换就够你头疼的。
到了 1.3,IETF 直接砍掉了 RSA 和静态 DH。为什么?因为 RSA 密钥交换没有前向安全性——一旦服务器私钥泄露,之前所有的会话记录都能被解密。静态 DH 同理。
DHE(有限域上的临时 DH)虽然安全,但速度慢,而且参数选择容易出错。我之前就见过一个库用 1024 bit 的 DHE 组,直接被爆攻击破。
所以最后只剩下 ECDHE 和 PSK(预共享密钥)。PSK 用于 0-RTT 等场景,但不是主流。ECDHE 成了默认且几乎必选的那一个。
避坑指南:Named Groups 协商
我曾经维护过一个旧版 Nginx 服务器,当时只配了 secp384r1,觉得 384 位肯定比 256 位安全。结果客户端全是移动端,只支持 x25519 和 secp256r1,握手全失败。
后来我发现,很多开源库对 secp384r1 的实现有 bug,运算效率也低。从那以后,我的最佳实践是:
- 服务器至少配 2~3 个曲线:x25519、secp256r1、secp384r1
- 客户端兼容时优先选 x25519
- 不要手工写死曲线参数,让库自动协商
另外,TLS 1.3 还支持 FFDHE(有限域 DH)作为可选组,但实际使用率极低。除非你对接的设备不支持椭圆曲线,否则完全不用考虑它。