2. TLS 1.3握手与密钥调度回顾

TLS 1.3(RFC 8446)把握手压缩到一次往返(1-RTT),并把密钥派生从"握手协商算法、记录层再推导"改成了"握手过程中同步推进的密钥调度(Key Schedule)"。理解这条主线,是判断PQC能插在哪里的前提:PQC不是替换某个算法名字那么简单,它改变的是ClientHello里携带的密钥共享、ServerHello里选择的组、以及共享秘密进入HKDF-Extract的位置。

一、1-RTT握手的消息序列

标准1-RTT握手(无PSK、无客户端证书)的消息顺序如下:

  1. ClientHello:客户端发送 random、legacy_session_id、cipher_suites、supported_groups、signature_algorithms、key_share、supported_versions等扩展。其中key_share携带客户端为若干命名组生成的临时公钥(如X25519的32字节公钥)。
  2. ServerHello:服务端选定一个组,返回自己的key_share,并给出supported_versions = TLS 1.3。此时双方各自用ECDH计算出共享秘密Z。
  3. {EncryptedExtensions}、{Certificate}、{CertificateVerify}、{Finished}:从ServerHello之后的所有握手消息都被握手密钥加密,花括号表示受保护。
  4. {Finished}:客户端发送自己的Finished,握手完成,随后切换到应用流量密钥。

关键点在于:ServerHello之后的消息已经加密。这意味着任何"在握手后期才协商的PQC算法"都无法保护ServerHello本身,也无法参与早期握手密钥的派生。PQC的插入点必须落在ClientHello/ServerHello这一对明文消息所能承载的扩展与密钥交换数据里。

二、密钥调度:共享秘密如何变成各类密钥

TLS 1.3的密钥调度以HKDF为核心,分两个阶段:

  • HKDF-Extract:把输入密钥材料(IKM)与盐(salt)压缩成固定长度的伪随机密钥(PRK)。第一次Extract的salt是0,IKM是PSK或0;第二次Extract的salt是上一阶段的Derive-Secret(., "derived", ""),IKM是(EC)DHE共享秘密。
  • HKDF-Expand-Label:从PRK派生出具体用途的密钥,标签包括"c hs traffic"、"s hs traffic"、"c ap traffic"、"s ap traffic"、"exp master"、"res master"等。

简化后的派生链为:

0
 | HKDF-Extract(salt=0, IKM=PSK 或 0)
 v
Early Secret
 | Derive-Secret(., "derived", "")
 v
Derive-Secret 输出
 | HKDF-Extract(salt=上一步, IKM=(EC)DHE)
 v
Handshake Secret
 | Derive-Secret(., "c hs traffic", ClientHello...ServerHello)
 | Derive-Secret(., "s hs traffic", ClientHello...ServerHello)
 v
Master Secret
 | Derive-Secret(., "c ap traffic", ClientHello...server Finished)
 | Derive-Secret(., "s ap traffic", ClientHello...server Finished)
 v
应用流量密钥

注意Derive-Secret的第二个参数是握手上下文哈希(Transcript-Hash),即到当前消息为止所有握手消息的哈希。这意味着:任何进入握手的PQC公钥、密文或扩展,只要出现在ClientHello/ServerHello中,就会被纳入Transcript-Hash,从而影响后续所有密钥。这是PQC与TLS 1.3集成的安全基础——攻击者无法在不被发现的情况下篡改PQC密钥交换数据。

三、可插入PQC的扩展与密钥交换环节

结合上面的消息序列与密钥调度,PQC的插入点可以归纳为四类:

插入点位置作用典型方案
混合密钥交换ClientHello/ServerHello的key_share把PQC KEM公钥/密文与ECDH共享秘密一起作为IKMX25519MLKEM768、SecP256r1MLKEM768
PQC签名认证Certificate/CertificateVerify用ML-DSA/SLH-DSA替换或混合RSA/ECDSA签名ML-DSA-65、SLH-DSA-SHA2-128s
额外密钥共享扩展ClientHello的key_share多组客户端预发多个组的公钥,服务端选一个X25519 + MLKEM768 并列
PSK/会话恢复NewSessionTicket / pre_shared_keyPQC认证的会话票据,恢复时免去证书验证ML-DSA签名的ticket

其中混合密钥交换是最优先、影响面最小的插入点。它的做法是:客户端在key_share中同时发送X25519公钥和ML-KEM-768公钥(或封装后的密文),服务端返回自己的X25519公钥和ML-KEM密文;双方分别计算Z_ecdh和Z_kem,再拼接成Z = Z_ecdh || Z_kem作为HKDF-Extract的IKM。这样即使ML-KEM被未来量子计算机攻破,X25519仍然提供经典安全性;反之亦然。

混合密钥交换的共享秘密拼接顺序必须在客户端和服务端保持一致,通常按ECDH || KEM或按组名排序。顺序不一致会导致双方派生出不同的Handshake Secret,握手在Finished校验时失败。

四、用一张图看清插入位置

下面的SVG把1-RTT握手的消息流、密钥调度阶段和PQC插入点放在同一张图上。左侧是消息序列,中间是密钥调度链,右侧标注可插入PQC的环节。可以看到:PQC密钥交换数据进入key_share后,直接参与Handshake Secret的Extract;PQC签名则影响CertificateVerify,但不改变密钥调度结构。

TLS 1.3 1-RTT握手 · 密钥调度 · PQC插入点 消息序列(1-RTT) ClientHello supported_groups / key_share signature_algorithms ServerHello 选定组 + 服务端 key_share supported_versions = 1.3 {EncryptedExtensions} {Certificate} {CertificateVerify} {Finished} 握手密钥加密 {Finished} 客户端完成 Application Data 应用流量密钥 密钥调度(HKDF) Early Secret IKM = PSK 或 0 Handshake Secret IKM = (EC)DHE 共享秘密 salt = derived(Early Secret) c hs traffic 客户端握手密钥 s hs traffic 服务端握手密钥 Master Secret salt = derived(Handshake Secret) c ap traffic 客户端应用密钥 s ap traffic 服务端应用密钥 PQC 插入点 ① 混合密钥交换 key_share: X25519 + ML-KEM-768 Z = Z_ecdh || Z_kem ② PQC 签名认证 Certificate: ML-DSA-65 CertificateVerify 签名 ③ 多组 key_share 客户端预发多个组公钥 服务端选一个返回 ④ PSK / 会话恢复 NewSessionTicket PQC 认证的 ticket 进入 IKM 签名替换 扩展承载 注:虚线表示PQC数据进入握手的位置;Transcript-Hash覆盖ClientHello至当前消息,确保PQC密钥交换数据不可篡改

五、为什么PQC不能"晚插"

从图中可以清楚看到,Handshake Secret的Extract发生在ServerHello之后、EncryptedExtensions之前。如果PQC密钥交换数据放在EncryptedExtensions或更晚的消息里,它就无法参与Handshake Secret的派生,也就无法保护后续的Certificate和Finished。更严重的是,攻击者可以在ServerHello到EncryptedExtensions之间篡改PQC数据而不被Transcript-Hash发现——因为那段数据还没进入哈希。因此,PQC密钥交换必须放在ClientHello/ServerHello的key_share中,PQC签名必须放在Certificate/CertificateVerify中,这是TLS 1.3协议结构决定的硬约束。

不要把PQC公钥放在自定义扩展里"稍后发送"。TLS 1.3的密钥调度是单向推进的,错过Extract点就无法回填。混合密钥交换的正确做法是在ClientHello的key_share中直接携带PQC KEM公钥,让它在第一次Extract时就进入Handshake Secret。

六、一个可观察的握手参数示例

假设客户端配置X25519MLKEM768混合组,抓包可以看到:

ClientHello
  supported_groups: x25519_mlkem768 (0x11ec), x25519 (0x001d)
  key_share:
    group: x25519_mlkem768
    key_exchange: 1216 字节(32字节X25519公钥 + 1184字节ML-KEM-768公钥)

ServerHello
  key_share:
    group: x25519_mlkem768
    key_exchange: 1120 字节(32字节X25519公钥 + 1088字节ML-KEM-768密文)

双方计算Z = Z_x25519 || Z_mlkem(32 + 32 = 64字节),送入HKDF-Extract得到Handshake Secret。如果服务端不支持该混合组,会回退到x25519,此时Z只有32字节,握手仍然成功但失去PQC保护。因此部署后必须通过抓包或openssl s_client -groups X25519MLKEM768确认实际协商的组,而不是只看配置。