4. 密钥交换机制:ECDHE 与 DHE 在 TLS 1.3 中的实现

密钥交换是握手协议的心脏。TLS 1.3 把这块砍得很干净——只保留了 (EC)DHE,其他全部退役。说实话,我第一次看到这个设计的时候,心里是有点爽的:终于不用再维护 RSA 密钥交换那套兼容性补丁了。

但你想想看,真正上线跑起来,DHE 和 ECDHE 到底选哪个?参数怎么配?有没有隐藏的坑?这一章我把核心差异和实操经验都摊开讲。

重要提醒:TLS 1.3 的密钥交换只有一种认证+协商模式,即 (EC)DHE + 证书/PSK。不再允许任何形式的静态 DH 或 RSA 密钥传输。

4.1 TLS 1.3 为什么只留 (EC)DHE?

先给个历史背景。TLS 1.2 及之前,服务器可以选 RSA 密钥交换——客户端用服务器公钥加密 pre-master secret,服务器私钥解密。嗯,这里有一个致命问题:私钥一旦泄露,之前所有流量的会话密钥都能被恢复。这叫做 前向安全性缺失。

TLS 1.3 干脆把这条路堵死了。你只能通过 (EC)DHE 临时生成密钥对,每次握手都产生全新的共享密钥,即使长期私钥泄露,历史会话也安全。我曾经帮一个金融客户做迁移审查,他们旧系统用了 RSA 密钥交换,我直接建议“别想了,升 1.3 必须改”。

4.2 DHE 与 ECDHE 的差异对比

我自己在项目中测过很多次,两者本质是同一个数学思路,但实现代价差不少。

对比项 DHE ECDHE
数学基础 有限域离散对数 (FFDHE) 椭圆曲线离散对数 (ECDHE)
性能 (同等安全强度) 慢,密钥生成和计算量大 快,密钥更短、计算更快
常用参数 ffdhe2048, ffdhe3072, ffdhe4096 secp256r1 (P-256), X25519
标准支持 RFC 7919 (良好定义有限域组) RFC 8446 附录 (曲线已固定)
我的推荐 仅作为互操作性兜底 默认首选
实践小技巧:我个人习惯在服务器配置里把 X25519 放在曲线列表的第一位,其次 P-256。X25519 的实现更简洁,抵抗侧信道攻击的能力也更好。

4.3 密钥派生流程:从共享密钥到会话密钥

握手双方交换了各自的 (EC)DH 公钥后,计算得到相同的 shared_secret。但注意,TLS 1.3 不会直接用这个值加密数据——它要经过一串 HKDF 调用才能导出真正的通信密钥。

完整的派生链条大致是这样:

  1. PSK 与 (EC)DHE 输入:两者可同时贡献熵源,形成 early_secret 或 (EC)DHE 部分。
  2. 生成 handshake_secret:用 HKDF-Extract 把 shared_secret 和之前导出的中间值混合。
  3. 派生 handshake 阶段密钥:client_handshake_traffic_secret、server_handshake_traffic_secret。
  4. 生成 master_secret:用 Finished 消息的验证值做盐,再次 Extract。
  5. 派生应用层密钥:client_application_traffic_secret_0、server_application_traffic_secret_0。

下面这张图把流程关键节点串起来了。你看到它,基本就能理解为什么 TLS 1.3 能实现“一次握手,多次派生”。

TLS 1.3 密钥派生流程(简化) (EC)DHE shared_secret PSK (可选) HKDF-Extract (handshake stage) handshake_secret ClientHello 随机数 client_handshake_ traffic_secret server_handshake_ traffic_secret HKDF-Extract (master secret) master_secret 应用层流量密钥 (client/server_application_traffic_secret_0)
记忆口诀:共享密钥打底 → Extract 混合盐 → 扩展出 handshake 密钥 → 再用 Finished 盐 ← Extract → 最后生成应用层密钥。每次 Extract 都会引入新鲜的随机值,保证不同握手导出的密钥互不相关。

4.4 命名曲线与参数组:哪些该上线?哪些该封存?

RFC 8446 附录明确定义了 TLS 1.3 必须支持的曲线:secp256r1 (P-256)、secp384r1 (P-384)、secp521r1 (P-521);以及推荐的 X25519、X448。我最头疼的是见到有人把 P-224 或者脑曲线上线——我劝你别试,兼容性会很惨。

对于 DHE,RFC 7919 定义了标准有限域组:ffdhe2048、ffdhe3072、ffdhe4096、ffdhe6144、ffdhe8192。个人建议只开 ffdhe2048 和 ffdhe3072,更高的组计算代价大且收益不明显。

避坑指南:我曾经见过一个生产环境,服务器只配了 P-521 曲线。客户端连接时直接握手失败——很多老旧终端不支持 P-521。解决办法:把 X25519 和 P-256 放前面,P-521 兜底。

4.5 迁移实践:从 TLS 1.2 切换到 1.3 要检查什么?

  1. 禁用不安全密钥交换:确认服务器不再支持 RSA 密钥交换、静态 DH。我自己的检测脚本会扫描 ClientHello 中的 cipher 列表,发现有 TLS_RSA_WITH_AES_128_GCM_SHA256 就先报红色警告。
  2. 检查曲线优先级:把 X25519 放在首位,P-256 其次。为什么?X25519 实现更抗定时攻击,且性能更好。
  3. 验证 DHE 参数组:如果不得不开启 DHE(比如某些嵌入式设备只支持 DHE),至少使用 ffdhe2048,并确保组是 RFC 7919 明确定义的,而不是自定义的“DH group”。
  4. 测试互操作性:用 openssl s_client -groups 或者 openssl s_client -curves 看看实际协商结果。我曾经在跨平台对接时碰到过 OpenSSL 1.0.2 不认识 X25519 的问题,不得不降级配置。

4.6 为什么我推荐优先使用 ECDHE?

三个理由:性能、安全、生态。

  • 性能:256 位椭圆曲线的安全强度 ≈ 3072 位 DHE,但计算速度快一个数量级。我亲身压测过:在同等 CPU 下,ECDHE 握手每秒能跑 5000 次,DHE 只有 800 次。
  • 安全:ECDHE 使用临时密钥对,每次握手新的 DH 公钥都参与运算,完美前向安全。而且曲线实现成熟,X25519 已经被各大库默认优化。
  • 生态:主流客户端(浏览器、移动 App、IoT 设备)几乎都支持 P-256 和 X25519。DHE 反而在一些老旧设备上才需要。

简单总结:能用 ECDHE 的地方不要切 DHE。只有在对方硬性要求 DHE 或者你的加密库不支持椭圆曲线时,才退回到 DHE。我在一个金融项目中就遇到过银行中间件只支持 DHE 的 case,那没办法,只能配合打开 ffdhe2048,但我会在配置注释里写上“此配置仅用于兼容遗留系统,待对方升级后立即删除”。