简短回答:X25519MLKEM768 是 TLS 1.3 的一个密钥交换组,码点为 0x11EC(4588)。它并行运行 ML-KEM-768 和 X25519,并把两个共享密钥一起输入密钥调度。它已在 RFC 10024(2026年8月)中标准化。Chrome、Firefox、Apple 各系统 26 版中的网络 API、OpenSSL 3.5、Go 1.24+ 和 JDK 27 都默认提供它。用 OpenSSL 3.5 运行 openssl s_client -connect example.com:443 -brief,查找 Negotiated TLS1.3 group: X25519MLKEM768 这一行。它能保护被记录下来的流量,使其免遭未来量子计算机的破解。它对身份验证毫无作用,身份验证仍然依靠 RSA 或 ECDSA 证书。
名称的含义
TLS 1.3 通过命名组(named group)协商密钥交换。客户端在 supported_groups 扩展中列出自己支持的组,并在 key_share 中为其中一个或多个组发送公钥。服务器选定一个组,并用自己的密钥共享(key share)作答。多年来,现实可选的只有 X25519 和 P-256 ECDH。在足够大的量子计算机上,二者都会被 Shor 算法攻破;今天记录下来的流量,一旦这样的机器出现就能被解密。这就是“先收集后解密”问题。
混合组解决了这个问题,又不必把一切都押在新的数学上。X25519MLKEM768 在一次协商中同时完成两种密钥协商:
- ML-KEM-768:FIPS 203(2024年8月)中基于模格的密钥封装机制,使用安全类别 3 的参数。这一部分能抵御量子攻击。
- X25519:RFC 7748 中的椭圆曲线 Diffie-Hellman 函数,经受过长期的密码分析,为较新的组件提供后备保障。
通用构造见 RFC 9954(Hybrid Key Exchange in TLS 1.3,2026年7月),它明确提出的目标是:“即使有人找到办法,攻破了除一个组件算法之外所有组件算法的加密”,安全性依然成立。具体的组在 RFC 10024(2026年8月)中定义:
| 组 | 码点 | 推荐 | 组成部分 |
|---|---|---|---|
| X25519MLKEM768 | 0x11EC(4588) | 是 | ML-KEM-768 + X25519 |
| SecP256r1MLKEM768 | 0x11EB(4587) | 否 | P-256 ECDH + ML-KEM-768 |
| SecP384r1MLKEM1024 | 0x11ED(4589) | 否 | P-384 ECDH + ML-KEM-1024 |
较早的抓包中可能会出现 X25519Kyber768Draft00,码点为 0x6399(25497)。这是 Chrome 最先上线的、标准化之前的 Kyber 混合方案。FIPS 203 对算法做了细小但不兼容的改动,因此 Chrome 131 改用 0x11EC,并移除了草案组。Google 的公告说明了不同时支持两者的原因:“后量子密码的数据量太大,无法同时提供两个后量子密钥共享预测。”RFC 10024 将草案码点标记为已废弃。
密钥共享的内部结构
混合密钥共享就是两个组件值的简单拼接,内部没有长度前缀。各部分的大小由 FIPS 203 和 RFC 7748 固定,因此仅凭长度就能检查每个字段:
| 字段 | 布局 | 字节数 |
|---|---|---|
客户端 key_exchange | ML-KEM-768 封装密钥(1184)‖ X25519 公钥(32) | 1216 |
服务器 key_exchange | ML-KEM-768 密文(1088)‖ X25519 公钥(32) | 1120 |
| 共享密钥 | ML-KEM 共享密钥(32)‖ X25519 共享密钥(32) | 64 |
注意顺序。名称以 X25519 开头,字节却以 ML-KEM 开头。RFC 10024 称,这一顺序“出于历史原因”相对命名惯例“被颠倒了”。它的第 5 节还给出了一个合规方面的理由:按照 NIST SP 800-56C,只有当第一个输入来自经批准的方案时,才允许在 FIPS 批准的密钥派生中使用组合后的共享密钥,而在这一对组件中,经批准的方案是 ML-KEM。出于同样的原因,两种 NIST 曲线混合组把 ECDH 密钥共享放在前面。如果解析器在第 32 字节而不是第 1184 字节处切分该字段,每次都会出错。
下面是 OpenSSL 3.5.7 发往 cloudflare.com 的一条真实 ClientHello,于2026年10月20日用 -trace 捕获(密钥字节已截短):
Sent TLS Record
Content Type = Handshake (22)
Length = 1551
ClientHello, Length=1547
extension_type=supported_groups(10), length=18
X25519MLKEM768 (4588)
ecdh_x25519 (29)
secp256r1 (P-256) (23)
ecdh_x448 (30)
secp384r1 (P-384) (24)
secp521r1 (P-521) (25)
ffdhe2048 (256)
ffdhe3072 (257)
extension_type=key_share(51), length=1258
NamedGroup: X25519MLKEM768 (4588)
key_exchange: (len=1216): A4696B297C695DEA602FB55157BC359F...
NamedGroup: ecdh_x25519 (29)
key_exchange: (len=32): 919D957E3D7E402ECF27C3DD0B38DDA0...
客户端发送了两个密钥共享:一个混合密钥共享,一个普通的 X25519 密钥共享。这是 OpenSSL 3.5 的默认行为。Go 1.27.3 发送同样的一对,Chromium 156 也是如此,后者还会加入一个 GREASE 条目,发出的是一条 1704 字节的记录。JEP 527 为 JDK 27 规定了同样的一对。普通密钥共享只多占 32 字节,却能让不认识 0x11EC 的服务器在一次往返内完成握手,而不必再索要另一个密钥共享。key_share 扩展的字节数恰好对得上:2 字节的列表长度,然后混合条目占 2 + 2 + 1216,X25519 条目占 2 + 2 + 32,共计 1258。服务器选择了混合组:
ServerHello, Length=1206
extension_type=key_share(51), length=1124
NamedGroup: X25519MLKEM768 (4588)
key_exchange: (len=1120): C01BC77BB1F9985B0DE82CDD1D0DD14B...
作为对比,同一客户端加上 -groups X25519 时发送的 ClientHello 为 313 字节。混合方案在每个方向上大约增加 1.2 KB,而大多数兼容性问题正源于这一体积(见下文)。
从两个共享密钥到一个密钥调度
每一方都执行两个相互独立的操作:
- 客户端生成一个 ML-KEM-768 密钥对和一个 X25519 密钥对,并发送两个公钥。
- 服务器用该 ML-KEM 密钥执行封装,得到一个 32 字节的共享密钥和一个 1088 字节的密文。它还用自己的临时密钥运行 X25519,然后发送密文和自己的 X25519 公钥。
- 客户端对密文执行解封装,恢复出同一个 ML-KEM 共享密钥,并在自己这一侧完成 X25519 计算。
- 双方把两个结果拼接成 64 字节,并把这个值用在 TLS 1.3 通常使用 (EC)DHE 共享密钥的位置:作为 HKDF-Extract 的输入,由它派生出握手秘密值(RFC 9846 §7.1,即2026年7月发布、取代 RFC 8446 的 TLS 1.3 修订版)。
此后的一切都是普通的 TLS 1.3。密码套件、证书、CertificateVerify 和流量密钥都没有变化。RFC 10024 增加了实现必须执行的校验步骤:按 FIPS 203 的要求检查封装密钥,拒绝长度不对的密文,并在 X25519 结果全为零时中止。任何一项失败都会以 illegal_parameter 或 internal_error 告警结束握手。
安全性论证很简单。记录了握手的攻击者必须同时恢复出两个共享密钥,才能算出握手秘密值。只要 ML-KEM 没有被攻破,用未来的量子计算机攻破 X25519 也无济于事。只要 X25519 没有被攻破,针对这种年轻的格方案出现意外的密码分析突破也无济于事。如果你想在真实密钥上看看封装和解封装,ML-KEM 实验室可以在你的浏览器中运行它们。
哪些软件默认启用它
截至2026年10月,所有主流客户端都默认提供它:
| 软件 | 默认启用的版本 | 来源 |
|---|---|---|
| Chrome(桌面版) | Chrome 131,取代 Kyber 草案组 | Google 安全博客 |
| Firefox | 自 Firefox 132(TLS)和 Firefox 135(HTTP/3)起支持。首选项 security.tls.enable_kyber 默认为 true | 版本说明 |
| Apple:URLSession 和 Network 框架 | 各系统的 26 版:iOS、iPadOS、macOS Tahoe、visionOS(网络指南),以及 tvOS 和 watchOS(安全指南) | Apple 支持、平台安全保护 |
| OpenSSL | 3.5.0:混合组进入默认列表,默认密钥共享为 X25519MLKEM768 + X25519 | NEWS.md |
Go crypto/tls | Go 1.24,前提是 Config.CurvePreferences 为 nil。Go 1.26 加入了两种 NIST 曲线混合组 | Go 1.24、Go 1.26 |
| Java(JSSE) | JDK 27,JEP 527:X25519MLKEM768 位于默认列表首位 | JEP 527 |
凡是链接 OpenSSL 3.5 的软件也会沿用这一默认值,只要其配置没有覆盖组列表。这包括 Debian 13 的软件包,以及自带 OpenSSL 3.5 的 Node.js 24。在 Debian 13 上,Python 的 ssl 模块与只接受这一个组的 openssl s_server -groups X25519MLKEM768 完成了握手,curl 和 nginx 也无需额外配置就协商出了混合组。Apple 的 TLS 安全性页面指出了一处缺口:“使用 SecureTransport API 的 TLS 客户端无法使用 TLS 1.3”,因此仍在使用该 API 的应用得不到混合密钥交换。
在服务端,很大一部分 HTTPS 流量已经终止于这样的协议栈:只要客户端提供混合组,它们就会选择它。2026年10月20日用 OpenSSL 3.5.7 进行的探测中,与 cloudflare.com、google.com、www.amazon.com、www.microsoft.com、www.apple.com 和 example.com 协商出的是 X25519MLKEM768,与 github.com 和 letsencrypt.org 协商出的是普通 X25519。
检查服务器、客户端和浏览器
用命令行检查:OpenSSL 3.5 的 -brief 输出会直接给出组名:
$ openssl s_client -connect cloudflare.com:443 -servername cloudflare.com -brief </dev/null
Connecting to 104.16.132.229
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN=cloudflare.com
Hash used: SHA256
Signature type: ecdsa_secp256r1_sha256
Verification: OK
Negotiated TLS1.3 group: X25519MLKEM768
DONE
如果所连接的服务器选择的是经典组,输出中会改为显示 Peer Temp Key: X25519, 253 bits。要测试服务器到底能否使用混合组,就只提供混合组。此时与只支持经典算法的服务器握手会失败:
$ openssl s_client -groups X25519MLKEM768 -connect github.com:443 -servername github.com -brief </dev/null
...:error:0A000410:SSL routines:ssl3_read_bytes:ssl/tls alert handshake failure:...:SSL alert number 40
这种探测只用于测试。真实的客户端如果只提供混合组,就是白白牺牲了兼容性。基于 OpenSSL 3.5 构建的 curl 会在详细输出的连接信息行中打印组名:
$ curl -sv -o /dev/null https://www.google.com/ 2>&1 | grep 'SSL connection'
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519MLKEM768 / id-ecPublicKey
用代码检查:Go 1.25 及更高版本通过 ConnectionState().CurveID 提供协商出的组,Node.js 24 则通过 getEphemeralKeyInfo() 报告它:
conn, err := tls.Dial("tcp", "cloudflare.com:443", &tls.Config{ServerName: "cloudflare.com"})
if err != nil {
log.Fatal(err)
}
fmt.Println(conn.ConnectionState().CurveID) // X25519MLKEM768 (Go 1.27.3)
import tls from "node:tls";
const socket = tls.connect({ host: "cloudflare.com", port: 443, servername: "cloudflare.com" }, () => {
console.log(socket.getEphemeralKeyInfo()); // { type: 'TLSGroup', name: 'X25519MLKEM768', size: undefined } (Node 24.22.0)
socket.end();
});
在浏览器中检查:Chrome 在开发者工具(DevTools)“安全”面板的连接详情中显示组名。通过编程方式也能拿到同一个值,即 DevTools 协议 Network.responseReceived 事件中的 securityDetails.keyExchangeGroup。2026年10月20日,无头模式的 Chromium 156 对 www.cloudflare.com 报告的是 X25519MLKEM768,对 github.com 报告的是 X25519。在 macOS 26 上,Apple 建议使用 nscurl --tls-diagnostics https://your.host。要查看你自己的浏览器发送了什么,打开浏览器后量子检测。
在你的服务器上启用它
只要底层是 OpenSSL 3.5,默认设置就已经是正确的。让它失效最常见的原因,是从旧的加固指南里复制来的一份显式组列表。下面这个在 Debian 13(OpenSSL 3.5.7)上进行的 nginx 1.30.5 测试只改动了 ssl_ecdh_curve 这一行,并用 openssl s_client -brief 探测:
--- no ssl_ecdh_curve (OpenSSL default list)
Protocol version: TLSv1.3
Negotiated TLS1.3 group: X25519MLKEM768
--- ssl_ecdh_curve X25519:prime256v1:secp384r1;
Protocol version: TLSv1.3
Peer Temp Key: X25519, 253 bits
--- ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1:secp384r1;
Protocol version: TLSv1.3
Negotiated TLS1.3 group: X25519MLKEM768
所以修复方法有两种:删除这一行,或者把 X25519MLKEM768 放在其中的首位。旧版 OpenSSL 不认识这个名称,所以要先升级 TLS 库。运行在 OpenSSL 1.1.1w 上的 nginx 1.24.0 会拒绝这样的配置行(这里是 ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1;),并报错 nginx: [emerg] SSL_CTX_set1_curves_list("X25519MLKEM768:X25519:prime256v1") failed。其他服务器也有同样的配置项,只是名称不同:Apache httpd 中是 SSLOpenSSLConfCmd Groups,HAProxy 中是 curves,Go 中是 CurvePreferences,Java 中是 jdk.tls.namedGroups。如果你需要为某种服务器找一个起点,TLS 配置生成器可以生成已包含混合组的配置。
还有两个地方容易被忽略:
- CDN 或负载均衡器后面的源站:浏览器的连接在边缘终止,而边缘到你的源站的连接是另一个独立的 TLS 会话,有自己的协商。这第二段上被记录的流量同样可能被收集,留待日后解密。
- 内部服务之间的 TLS:Go 服务能否用上混合组取决于工具链版本。Java 服务从 JDK 27 起获得它,JEP 527 在该版本中加入了混合组。凡是写死了
CurvePreferences、jdk.tls.namedGroups或其他显式组列表的服务,在有人修改之前都得不到它。
密钥共享预测与 HelloRetryRequest
TLS 1.3 客户端必须猜测服务器会选择哪个组,并预先为它发送密钥共享。如果猜错,服务器会回复一条 HelloRetryRequest,指明它想要的组,客户端随后从头再来。这要多花一整个往返。对于后量子密钥共享,这个猜测更加重要,因为“以防万一”发送一个 1216 字节的密钥共享是有代价的。让 OpenSSL 在列表中包含混合组、但只为 X25519 发送密钥共享,就能观察到重试的过程:
ClientHello, Length=315
extension_type=key_share(51), length=38
NamedGroup: ecdh_x25519 (29)
ServerHello, Length=84 <- HelloRetryRequest
extension_type=key_share(51), length=2
NamedGroup: X25519MLKEM768 (4588)
ClientHello, Length=1499
extension_type=key_share(51), length=1222
NamedGroup: X25519MLKEM768 (4588)
ServerHello, Length=1206
Negotiated TLS1.3 group: X25519MLKEM768
尽管客户端把 X25519 列在前面,服务器仍然偏好混合组,于是要求重试。在 OpenSSL 3.5 的组字符串中,前缀 * 标记出在第一轮消息(first flight)中就发送密钥共享的组。-groups '*X25519:*X25519MLKEM768' 会同时发送两个密钥共享,ClientHello 为 1535 字节,从而避免重试。主流客户端的默认设置同时发送两者,也是出于同样的原因。TLS 工作组的一份草案 draft-ietf-tls-key-share-prediction 允许服务器在 DNS HTTPS 记录中发布自己偏好的组,让客户端第一次就能猜对。截至2026年10月,它仍是一份互联网草案(Internet-Draft)。
会出什么问题:过大的 ClientHello 与老旧的中间设备
上面实测的 ClientHello 在一条 TLS 记录中装了 1551 字节的载荷。加上记录、TCP 和 IP 的头部之后,在 MTU 为 1500 字节的典型路径上,它已经无法装进一个数据包。规范一直允许这种情况,但有些实现想当然地认为,从套接字第一次 read() 就能读到完整的 ClientHello。当混合方案让它分散到两个分段中时,这些实现就会出错。
tldr.fail 项目追踪这一类缺陷。按它的描述,这些服务器“没有考虑到可能需要多次调用 read() 才能读完整个 ClientHello”。它的列表包括多家厂商的 TLS 检查代理、防火墙和负载均衡器。其中一些已经打了补丁,一些标记为情况未知。症状看起来像网络故障,而不是密码学问题:Chrome 中的 ERR_CONNECTION_RESET 或 ERR_SSL_PROTOCOL_ERROR、握手超时,或者紧接在 ClientHello 之后的连接重置。这个缺陷往往只有发送混合组的客户端才会遇到。Apple 在 26 版系统的说明中也警告了同样的故障:设备可能无法连接某些较旧的服务器,因为这些服务器的 TLS 代码“无法读取较大的 ClientHello 消息”。
遇到这个问题时:
- 找出重置连接的设备:通过同一路径,对比
openssl s_client -groups X25519和默认组列表的结果。如果只有大的 ClientHello 失败,问题就在于大小,而不是证书。 - 修补或更换该设备:在客户端上禁用混合组,会关闭所有连接的保护,包括原本正常的连接。
- 客户端开关只能作为权宜之计:Go 有
GODEBUG=tlsmlkem=0,Go 1.24 版本说明提到它适用于“无法正确处理大记录的有缺陷 TLS 服务器”。Chrome 曾有PostQuantumKeyAgreementEnabled企业策略,Google 称其为“临时措施”,并表示它“将在 Google Chrome 145 版之后的某个时候移除”。在围绕它做规划之前,先确认你的 Chrome 版本仍然支持这一策略。
对终止 TLS 的代理还要补充一点。解密流量的代理要完成两次握手。你的笔记本电脑与代理协商出的结果,完全说明不了代理与外部世界之间的连接。只有当代理自身的 TLS 协议栈支持并优先选择混合组时,这一段才是后量子的。
它不保护什么
X25519MLKEM768 保护的是机密性,防范的是现在记录流量、日后再解密的攻击者。它不会让握手的身份验证变成后量子的。服务器仍然用 RSA 或 ECDSA 证书以及 CertificateVerify 签名证明自己的身份。能用量子计算机伪造这些签名的人,可以在实时连接中冒充服务器。这种威胁只有在这样的计算机出现之后才存在,这也是为什么密钥交换先行一步,而签名是一项独立且更缓慢的迁移。本系列的其他文章介绍了这部分工作:ML-DSA 密钥和签名见面向开发者的 ML-DSA,包括盘点和截止日期在内的整体计划见2026年的后量子迁移。
它对 TLS 1.3 以外的流量也毫无作用。TLS 1.2 连接、SSH 会话和 VPN 隧道都有各自的密钥交换和各自的升级路径。在把你的连接算作已受保护之前,先查清其中哪些仍在协商 TLS 1.2。
参考资料
- RFC 10024:TLS 1.3 的后量子与传统(PQ/T)混合密钥协商机制(2026年8月)。组的定义、字节顺序、校验、IANA 码点。
- RFC 9954:TLS 1.3 中的混合密钥交换(2026年7月)。
- FIPS 203:基于模格的密钥封装机制标准(2024年8月)。
- RFC 9846 §7.1:TLS 1.3 密钥调度。
- Google 安全博客,Kyber 在 Web 上的新路径(2024年9月)。
- Apple 支持,为 TLS 中的量子安全加密准备好你的网络,以及《Apple 平台安全保护》指南。
- OpenSSL 3.5 NEWS.md,Go 1.24 和 1.26 版本说明,JEP 527。
- tldr.fail:大 ClientHello 缺陷及受影响的产品。
本页所有命令输出均来自2026年10月20日在容器中的运行:OpenSSL 3.5.7、curl 8.14.1、Node.js 24.22.0 和 Debian 自带的 Python 3 运行在 node:24.22.0-trixie 中,Go 1.27.3 运行在 golang:1.27.3-trixie 中,nginx 1.30.5 运行在 nginx:1.30.5 中,搭配 OpenSSL 1.1.1w 的 nginx 1.24.0 运行在 nginx:1.24.0 中,Chromium 156 运行在 mcr.microsoft.com/playwright:v1.64.1-noble 中。