Skip to content

第 16 章 TLS 1.3 完整解析 ​

16.1 TLS在字节流上同时运行握手与记录协议 ​

TLS为应用建立一个经过认证、能够保护后续字节的安全通道。在常见的HTTPS连接中,TCP提供有序字节流,TLS从中识别记录、完成握手和解密,HTTP再消费解密后的字节。对于VLESS+TLS,最先消费这些应用字节的是代理协议。TLS并不要求内部一定是HTTP,也不自动解析代理目标。

本章以RFC 8446与RFC 8448构成历史格式和公开计算向量的复现基线。2026年7月发布的RFC 9846已替代RFC8446,保持TLS1.3版本号及向后互通,同时收紧和澄清若干要求;本章相应位置注明现行差异,旧编号用于定位向量所依据的定义。补充的服务身份验证按RFC9525说明。这里分析的是TLS1.3标准基线,不把某个浏览器的扩展集合、Xray的指纹实现或未来新密码组当作所有TLS1.3连接的必备字段。

TLS包含两套不能混为一谈的结构。记录协议负责把字节切成有界单元,并在适当阶段加密和认证;握手协议负责协商参数、认证身份和建立秘密。握手消息被放进记录的负载里,应用数据随后也由记录承载。网络上的TCP分段、TLS记录和握手消息是三个不同边界。

未加密记录的头固定为5字节:

偏移长度字段
01ContentType,负载内容种类
1—22legacy_record_version,历史兼容字段
3—42length,后续fragment字节数,大端
5起lengthfragment

握手消息则另有4字节头:1字节消息类型,3字节大端消息体长度,随后是消息体。记录头的长度不包括5字节自身;握手长度不包括4字节自身。假设一条ClientHello消息体长192字节,则握手头是01 00 00 c0,完整握手消息196字节;如果放在一条初始记录里,该记录可以以16 03 01 00 c4开头,总计201字节。这正是RFC8448简单握手中的尺寸,后续会解析全部字段。

所属命名空间例子及其十六进制值含义
记录内容类型16这条未保护记录承载握手字节
记录内容类型17受保护记录的外层类型application_data
握手消息类型01、02ClientHello、ServerHello
握手消息类型08、0b、0f、14EncryptedExtensions、Certificate、CertificateVerify、Finished
记录内容类型14历史ChangeCipherSpec兼容记录,不是Finished消息

记录类型16是十六进制,即十进制22;Finished消息类型14是十六进制,即十进制20。不同命名空间可以重复使用数值,不能只看一个14就断言发生了同一件事。

版本字段也有兼容含义。TLS1.3实际版本值为0304,却不要求记录头直接写0304。除初始ClientHello等规定情形外,TLS1.3记录使用0303;初始ClientHello记录还允许0301,标准为兼容推荐这种形式。ClientHello内部的legacy_version又是0303,实际支持和选定版本由supported_versions扩展表达。因此只看记录头03 03不能断言会话协商成TLS1.2。RFC 8446 §5.1。

普通证书认证的完整TLS1.3握手先以明文发送ClientHello和ServerHello。双方据此得到握手保护材料,之后的EncryptedExtensions、服务器证书、签名和Finished受到握手密钥保护;完成相应验证后,应用数据使用另一组应用流量密钥。因此同一连接的“加密”分为阶段,不能理解为TCP建立后立即用一把固定密钥包住所有内容。

一个握手消息可以跨多个记录,一个记录也可以包含多个连续握手消息,但不能任意交错其他内容或跨越密钥切换边界。ClientHello、ServerHello、Finished等可能紧邻密钥变化的消息,必须按标准对齐记录边界;记录头也不能因TCP一次读取只返回一部分就被当成损坏。RFC 8446 §5.1。

QUIC使用TLS1.3握手与密钥派生,却不把这些5字节TLS记录塞进QUIC:握手字节由CRYPTO帧传输,应用字节由QUIC自身的包保护与流服务处理。第12章已经见到这个接口差别,本章后段会把其Initial包公开向量的保护材料也实际推导出来。

16.2 ClientHello声明能力,也送出本次交换材料 ​

ClientHello首先回答“客户端愿意使用什么”。它不是用来直接发送网站HTTP请求,也不是把服务器地址编码成一个TLS专用目标命令。TCP已经连接到某个端点;ClientHello在该连接上声明版本、密码套件、交换组和其他能力,并通常带上本次公开交换值。RFC 8446 §4.1.2。

TLS的可变长向量需要先读长度。长度前缀本身有几字节,由该字段规范的最大范围决定,不是每个地方都统一使用两字节。ClientHello中的legacy_session_id以1字节长度开头,cipher_suites以2字节字节数开头;套件本身又各占2字节。把长度6误读成“6个套件”就会越过真实边界。

以RFC8448 §3的196字节ClientHello为例,以下偏移从握手消息类型字节开始,不包括外层5字节记录头:

偏移长度内容与解释
0—3401 00 00 c0,类型01、消息体192字节
4—52legacy_version=03 03
6—3732random,本次公开随机值
38100,旧会话ID长度0,无ID内容
39—40200 06,套件列表占6字节
41—46613 01 13 03 13 02,3个套件
47—48201 00,压缩方法列表长1,唯一项00
49—50200 91,扩展区145字节
51—195145按类型与长度排列的扩展

random是公开输入,不是私钥。它参与区分当前交互及握手绑定,不能因为有32字节随机数就推断已经建立秘密。legacy_session_id也不是TLS1.3恢复密钥;兼容模式可带32字节值并要求服务器回显,但真正的1.3恢复使用PSK机制。

三个套件依次为TLS_AES_128_GCM_SHA256、TLS_CHACHA20_POLY1305_SHA256、TLS_AES_256_GCM_SHA384。TLS1.3套件指定记录AEAD和派生所用散列,不再同时指定证书签名算法与交换组。因此1301并不代表“必须RSA交换”,也不代表“必须X25519”。签名能力由signature_algorithms等扩展表达,交换组由supported_groups和key_share关联。

每个扩展以2字节类型、2字节内容长度开头,后跟指定字节数。本例的全部扩展边界如下:

扩展头偏移类型内容长度作用
510000,server_name11本例名称为ASCII的server
66ff01,renegotiation_info1历史兼容信息,不表示1.3允许重新协商
71000a,supported_groups20声明可用交换组列表
950023,session_ticket0历史扩展,不是1.3恢复PSK本身
990033,key_share38携带一个X25519公开交换值
141002b,supported_versions302 03 04,列表长2,支持1.3
148000d,signature_algorithms32签名方案列表,内部还有2字节长度
184002d,psk_key_exchange_modes201 01,一个模式psk_dhe_ke
190001c,record_size_limit2记录接收限制扩展,本例值4001

各扩展内容之和109字节,9个4字节扩展头共36字节,合计145。公开向量保留了兼容旧版本的部分能力列表,不代表列表中每个旧签名方案都允许用于TLS1.3的CertificateVerify。服务器必须按最终版本的规则筛选,不能把“客户端列过”当成全部场景有效。

SNI扩展的完整15字节是:

text
00 00 00 0b 00 09 00 00 06 73 65 72 76 65 72

前4字节为扩展头;0009表示名称列表9字节;00是host_name类型;0006表示名称6字节;后6字节是server。这是标准向量使用的测试名称,不是本书给出的可部署站点。SNI用于服务器选择服务上下文,不是对该名字的认证证明;证书身份匹配仍须另做检查。

key_share内部开头为00 24 00 1d 00 20,随后32字节。0024表示条目列表36字节,001d标识X25519,0020是该公开值32字节长度。公开值为:

text
99 38 1d e5 60 e4 bd 43 d2 3d 8e 43 5a 7d ba fe
b3 c0 6e 51 c1 3c ae 4d 54 13 69 1e 52 9a af 2c

supported_groups可以声明多个组,却只在key_share中提前提供一部分实际交换值,以平衡首包大小与计算。现行RFC9846禁止跨连接重用key_share,交换值必须按其生命周期要求重新生成。服务器若需要另一个客户端已声明支持、但尚未提供交换值的组,可能发出HelloRetryRequest;二者不能合并理解成同一张列表。

ALPN则协商TLS里面将使用的应用协议,例如h2或http/1.1。该RFC8448向量没有ALPN。另举仅声明h2的构造,其扩展字节为00 10 00 05 00 03 02 68 32:扩展内容长5,协议列表长3,协议名字长2。ALPN不是记录加密算法,也不自动创建HTTP/2流;它让双方知道随后应怎样解释应用字节。

这些最初交换材料在普通TLS1.3中尚未由该会话的记录加密隐藏。之后握手证明会把它们纳入认证,从而检测篡改;“后来被认证”与“起初不被观察者读到”是不同性质。涉及额外保护ClientHello的机制时,必须另行说明,不能从TLS1.3版本号直接推出初始域名隐私。

16.3 ServerHello只确认建立保护所需的核心选择 ​

服务器不能任意创造一套客户端没提议的参数。TLS1.3 ServerHello选择一个客户端提供的密码套件,回显legacy_session_id,压缩字段为00,通过supported_versions明确选择0304,再提供key_share、选定PSK的索引或二者兼有。只有建立密码上下文所需的扩展放在这里,ALPN等其余协商响应通常放进后续受保护的EncryptedExtensions。RFC 8446 §4.1.3。

继续使用RFC8448 §3,ServerHello握手消息共90字节,消息体86字节,头为02 00 00 56。相对于本消息起点,其布局是:

偏移长度内容
0—34消息类型02及体长0056
4—52legacy_version=0303
6—3732服务器独立生成的random
381回显空旧会话ID,长度00
39—402cipher_suite=1301,AES-128-GCM与SHA-256
411压缩方法00
42—432扩展区长度002e,即46字节
44—8340key_share扩展,内容36字节
84—896supported_versions扩展,内容0304

服务器key_share的内容是00 1d 00 20后接32字节公开值:

text
c9 82 88 76 11 20 95 fe 66 76 2b db f7 c6 72 e1
56 d6 cc 25 3b 83 3d f1 dd 69 b1 b0 4e 75 1f 0f

注意这里没有客户端key_share内容开头的2字节列表总长,因为ServerHello只发送选定的一个条目。两者不能共用一个不看消息位置的解析规则。此时客户端用自己的临时私有值和上述公钥,服务器用自己的临时私有值和ClientHello公钥,得到相同Z。两条Hello的字节也已经足以派生握手流量材料,但服务器身份还要等待后面的证书、签名和Finished验证。

接收方必须检查回显ID、套件和交换选择符合提议及规范;不符合时中止,而不是把服务器回复当作新的无限授权。ServerHello也不是给全部扩展任意回显的容器,收到禁止出现在这里的扩展需要按错误处理。格式能解析,只说明字段边界成立,不能替代协商约束。

若服务器支持的参数与客户端能力有交集,但客户端没有预先送来足够的交换材料,可以发HelloRetryRequest,简称HRR。典型例子是客户端声明支持X25519和另一交换组,却只提前发了另一组的key_share,而服务器要求X25519。HRR请求一个新公开值,增加一次握手往返;它不是要求重新进行TCP握手。

为兼容中间设备,HRR沿用ServerHello的消息类型02和结构,通过32字节固定random识别:

text
cf 21 ad 74 e5 9a 61 11 be 1d 8c 02 1e 65 b8 91
c2 a2 11 16 7a bb 8c 5e 07 9e 09 e2 c8 a8 33 9c

它是文本HelloRetryRequest的SHA-256结果,不是本次新生成的服务器随机挑战。HRR中的key_share只含2字节selected_group,没有服务器公开值;若选择X25519,该扩展为00 33 00 02 00 1d。标准还允许cookie等规定内容。cookie帮助服务器把某些状态交给客户端带回,不等同于客户端已经取得代理账号资格。RFC 8446 §4.1.4、§4.2.2、§4.2.8。

客户端第二个ClientHello只能作标准允许的修改,例如按要求替换key_share、带回cookie、移除early_data、更新相关PSK绑定及适当填充;不能借重试任意重写先前全部协商。HRR如果不会带来任何ClientHello变化,需要拒绝;同一连接再收到第二次HRR也必须中止。最终ServerHello的版本与套件还要同HRR保持一致。

重试历史不会被清空。TLS用一个合成message_hash消息替代原始ClientHello1在后续摘要中的位置,然后接上HRR、ClientHello2以及后续消息。若散列为SHA-256,替代块为:

text
fe 00 00 20 | SHA-256(ClientHello1完整握手消息),32字节

fe是合成握手类型,0020是摘要32字节长度,它不是额外在线上发给对端的消息。ClientHello1摘要包括其4字节握手头,不包括TLS记录头。这个安排让服务器可以只保存或带回原始消息摘要,同时仍把第一次提议纳入后续认证。RFC 8446 §4.4.1。

TLS还在某些向旧版本协商的ServerHello.random末8字节设置降级标记:支持1.3的服务器选择1.2时为44 4f 57 4e 47 52 44 01,更旧情形使用末字节00。客户端按其版本规则检查,发现矛盾则拒绝。这是兼容旧协议时的一项额外防护,不是允许忽略握手验证的替代品,也不是本书建议启用旧TLS版本的配置。正常TLS1.3交换的协商完整性仍要由完整握手证明建立。

16.4 HMAC、HKDF与TLS的分阶段密钥树 ​

TLS不会把X25519结果原封不动地当成唯一AES密钥。它需要把PSK、临时交换结果、握手历史和用途组织起来,形成多组相互区分的材料。这里先把所用工具展开,再给出完整依赖图;没有使用PSK的连接也要按图执行规定步骤。

HMAC是基于散列的消息认证码构造。它先把密钥按散列分组长度规范化:过长时先散列,不足时补零得到K₀;然后用两种固定垫片分别混合密钥,执行内外两次散列。若H表示选定散列,双竖线表示字节串联,则定义为:

text
HMAC(K,M) = H((K₀ ⊕ opad) || H((K₀ ⊕ ipad) || M))
ipad:分组长度个36字节;opad:分组长度个5c字节

其中ipad是字节36重复到分组长度,opad是字节5c重复到分组长度;36与5c均为十六进制值。SHA-256内部散列分组为64字节,摘要为32字节;两种长度不要混为一谈。HMAC不是简单的H(K后接M),也不是把一把密钥重复异或在业务正文上。其构造定义见RFC 2104 §2,TLS实际采用套件指定的现代散列,不能沿用该历史文档示例中的MD5作为本章选项。

HKDF是基于HMAC的提取与扩展派生函数。RFC 5869 §2把它分成两个步骤:

text
Extract(salt, IKM) = PRK = HMAC(salt, IKM)
Expand(PRK, info, L) = 长度为L字节的输出

IKM是输入密钥材料;salt在HMAC的密钥位置,IKM在消息位置,二者不能颠倒。通用HKDF中的salt可以公开,在TLS密钥树中它也可能是上一步得到的秘密状态。“盐必须公开”不是所有调用的硬性性质。提取把适用的输入材料整理成摘要长度的PRK,却不会给一个完全可猜的输入凭空增加秘密。

扩展先计算T₁=HMAC(PRK,info后接单字节01),再计算T₂=HMAC(PRK,T₁后接info后接02),依此类推。串联T₁、T₂等,取最前L字节。空的T₀不占字节;块编号是单字节,所以L最多为255倍摘要长度。SHA-256下是8160字节,而非无限输出。这里的重复HMAC是定义所需的数学关系,不是读者实验程序。

info把派生结果限定到用途和上下文。使用同一个PRK但不同用途标签,可以得到不同结果;标签可以公开,它们的作用是区分派生域,不负责提供额外熵。收发方向、阶段与协议名称都应在设计中明确区分,不能只把一段输出复制给所有位置。

TLS1.3在Expand上加一层精确编码,称HKDF-Expand-Label。其info,即HkdfLabel,为:

顺序字段编码
1需要的输出Length2字节无符号大端
2完整标签长度1字节
3完整标签ASCII的tls13 后接指定Label,含空格,不含末尾NUL
4Context长度1字节
5Context指定字节串

例如,派生16字节记录key,Label为key,Context为空,其info正好13字节:

text
00 10 09 74 6c 73 31 33 20 6b 65 79 00

0010是输出16字节;09是完整字符串tls13 key的9字节;最后00是空Context长度,不是字符串结束符。派生12字节IV则使用Label为iv,得到info:00 0c 08 74 6c 73 31 33 20 69 76 00,共12字节。RFC 8446 §7.1、§7.3。

TLS还定义Derive-Secret:用握手消息序列的摘要作为Context,输出长度取散列摘要长度。因此Derive-Secret(S,标签,空消息序列)中的Context是H(空串),不是空Context。以SHA-256为例,H(空串)是32字节,开头为e3b0c442;而直接Expand-Label(S,key,空Context,16)的Context确实长0。把两者混同会使所有后续输出不同。

现在定义符号:0ₕ表示摘要长度个零字节,Z表示本次(EC)DHE共同结果,D(S,L,M)表示Derive-Secret(S,L,M)。TLS密钥树的主干是:

text
Early Secret     = Extract(0ₕ, PSK)
Handshake Secret = Extract(D(Early Secret, derived, 空消息), Z)
Master Secret    = Extract(D(Handshake Secret, derived, 空消息), 0ₕ)

没有PSK时用0ₕ代替PSK;适用模式没有新DHE时按规范用零材料代替Z。不是省掉相应Extract。Early Secret在纯证书握手中仍存在,而且此时可以由公开零输入计算;真正的新会话秘密从Z进入主干。随机字段与握手摘要则进入相应分支的上下文,不能与秘密输入的位置互换。

输出分支从哪个主干秘密派生Label握手摘要范围
外部或恢复PSK绑定密钥Early Secretext binder或res binder空消息序列的摘要
客户端早期流量秘密Early Secretc e trafficClientHello
早期导出秘密Early Secrete exp masterClientHello
客户端握手流量秘密Handshake Secretc hs trafficClientHello至ServerHello
服务器握手流量秘密Handshake Secrets hs trafficClientHello至ServerHello
客户端应用流量秘密第0代Master Secretc ap trafficClientHello至服务器Finished
服务器应用流量秘密第0代Master Secrets ap trafficClientHello至服务器Finished
导出秘密Master Secretexp masterClientHello至服务器Finished
恢复主秘密Master Secretres masterClientHello至客户端Finished

这些范围包含实际发送的握手消息及其4字节头,不包含TLS记录头;有HRR时采用16.3节的替代规则。表中“至服务器Finished”包括该Finished本身,而计算该Finished的认证值时又只覆盖它之前的握手,下一节会区分。导出秘密用于应用按指定接口取得与流量密钥分离的材料;恢复主秘密用于将来的恢复PSK,不等同于当前AEAD key。

拿到某方向、某阶段的traffic secret后,再用Expand-Label分别以key与iv作为Label、空Context,得到实际AEAD密钥与nonce基础IV。若套件是1301,散列与traffic secret均为32字节,AES key却为16字节,IV为12字节。阶段或方向不同,其traffic secret不同;相同的key标签不意味着派生出相同记录密钥。

RFC8448 §3的公开向量把这些关系具体化。两条Hello总计196+90=286字节,散列为:

text
860c06edc07858ee8e78f0e7428c58edd6b43f2ca3e6e95f02ed063cf0e1cad8

其X25519结果Z为8bd4054fb55b9d63fdfbacf9f04b9f0d35e6d63f537563efd46272900f89492d。从零PSK开始,得到以下32字节状态:

text
Early Secret:
33ad0a1c607ec03b09e6cd9893680ce210adf300aa1f2660e1b22e10f170f92a
Handshake Secret:
1dc826e93606aa6fdc0aadc12f741b01046aa6b99f691ed221a9f0ca043fbeac
服务器握手traffic secret:
b67b7d690cc16c4e75e54213cb2d37b4e9c912bcded9105d42befd59d391ad38

最后得到服务器握手记录key=3fce516009c21727d0f2e4e86ee403bc,IV=5d313eb2671276ee13000b30。本书已从公开临时输入独立复算到这些结果,与标准逐字节一致。它们是公开向量材料,不是任何真实连接秘密。

握手流量密钥此时足以让客户端解开服务器后续记录,但还不能跳过身份验证。先能计算共享材料、随后检查证明,是TLS1.3的正常阶段安排。RFC9846将Master Secret改称Main Secret,exporter_master_secret与resumption_master_secret等也采用不含master的新名称;线上派生标签如exp master和res master为保持互通仍然保留。本章计算图沿用RFC8448对应的旧变量名,不能擅自把这些标签里的字符替换。主干秘密在完成所有必要派生后应按标准删除;保留或泄露更高层秘密的影响范围,通常大于只暴露一个末端记录key。密钥树既是计算图,也是理解安全边界与清除时机的依据。

16.5 证书、CertificateVerify与Finished怎样共同完成认证 ​

客户端解开服务器握手记录以后,仍要回答三个问题:证书里的公钥为什么可信,它是否属于客户端原本要连接的服务,以及私钥持有者是否参与了这一场握手。TLS把这些职责分到不同结构里,再用Finished确认最终协商和秘密一致。

常见X.509证书以DER二进制编码,不是把配置中PEM的BEGIN行原样传进握手。PEM是在DER外加Base64等表示的文件形式。证书内部包含被签署的证书体、签名算法标识和签名值;证书体又包括主体、签发者、序列号、有效期、公钥信息及扩展等。签发者的签名关联这些信息,具体结构和验证规则见RFC 5280。

TLS Certificate消息在证书外还有自己的容器。普通握手中的certificate_request_context长0,以一个00字节表达;后面是3字节certificate_list总长度。每个条目有3字节cert_data长度、证书DER,以及2字节条目扩展长度和对应扩展。列表首先放发送者自己的终端证书,再放用于建立路径的后续证书;不要求把本地已经信任的根证书一并发送。RFC 8446 §4.4.2。

RFC8448测试向量中的Certificate共445字节:4字节握手头、1字节空context长度、3字节列表长、3字节证书长、432字节DER、2字节空条目扩展长度。列表长437,消息体441。把它们相加是4+1+3+3+432+2=445,而不是“445字节全是证书”。实际站点的链长和证书尺寸可以大不相同。

验证证书路径时,客户端使用自己已认可的信任锚,检查每一级签名、有效期、约束和用途等条件。一个根证书出现在服务器发来的列表中,不会因此自动成为可信根;一个证书能够自签,也没有凭空取得客户端信任。CA证书的基本约束、路径长度、关键扩展和密钥用途都可能限制它签发什么。终端证书还应符合服务器认证等实际用途,不能只验证“某个签名在数学上正确”。

证书路径成立之后,还要匹配服务身份。对通常的域名服务,客户端先依据请求或可信配置确定预期标识,再与证书subjectAltName中的适当标识比较。现代规则不能把主体Common Name作为通用回退;IP地址标识需要与iPAddress类型匹配,不能因为某个dNSName字符串恰好看起来像IP就当成同类。RFC 9525 §4、§6—7。

例如,预期proxy.example.com与证书的同名DNS-ID相符;若允许通配符,*.example.com可以覆盖这个一层名字,却不覆盖example.com本身,也不覆盖a.b.example.com。RFC9525把允许的通配符限制为完整最左标签。具体应用可以施加更严格规则,不能把通配符当任意子串匹配。

连接IP与预期服务名可以不同。客户端解析proxy.example.com得到某个IP,或通过可信配置指定连接IP,仍可能以proxy.example.com验证证书。关键是这个参考名字从哪里来,而不是先接受服务器给出的任意名字再反向检查“它果然匹配自己的证书”。SNI通常用于服务选择,也常与预期名字相同,却不能取代验证规则。

撤销状态、证书透明度、私有信任锚和具体应用策略还会影响实际接受决定;不同客户端实现不一定执行相同的在线查询。不能把“每次握手一定向CA查询一次”当作TLS工作原理。证书路径与名称检查提供身份依据,认证安全仍依赖本地信任配置与实际验证行为。

CertificateVerify继续证明本次握手与证书私钥相关。其消息体是2字节SignatureScheme、2字节签名长度和签名字节。实际待签字节串严格为:

text
64个20字节
| ASCII字符串 TLS 1.3, server CertificateVerify
| 单字节00
| 直到服务器Certificate为止的握手记录摘要

这里20为十六进制空格。服务器上下文字符串长33字节;SHA-256摘要长32字节,所以本例待签输入总计64+33+1+32=130字节。客户端证书签名则换用client上下文。固定前缀和角色字符串用于区分证明用途,不是随机填充,也不作为另一份线上字段完整发送。RFC 8446 §4.4.3。

签名方案要与证书公钥兼容、符合客户端声明及TLS1.3规则。套件1301只指定AEAD与派生散列,没有替CertificateVerify选定签名方案。在RFC8446基线下,TLS1.3的RSA握手签名使用PSS,不能因为兼容列表出现PKCS#1 v1.5就改用它;SHA-1也不能用于CertificateVerify。2026年的RFC9963另分配0420、0520、0620,仅在服务器明确提议时支持特定旧硬件的客户端CertificateVerify,且应默认禁用;不能把这些值用于服务器签名或ClientHello提议。服务器RSA签名仍需PSS,原有0401等旧编码也没有因此获准用于这里。证书本身的签发签名与本次握手签名仍是不同的两次签名。

RFC8448本例CertificateVerify头部为0f 00 00 84 08 04 00 80:消息体132字节,方案0804为rsa_pss_rsae_sha256,签名长128字节,整个握手消息136字节。本书已按实际130字节输入独立验证其签名。该向量使用1024位RSA历史测试密钥,测试证书还没有现代公开网站所需的完整身份条件,且在本书编写日期已过其有效期;它用于复现标准计算,不能作为现行部署参数或“浏览器应该信任”的示例。

最后的Finished不是又一份公钥签名,而是用当前握手流量秘密派生的认证码。对服务器:

text
finished_key = Expand-Label(server_handshake_traffic_secret,
                            finished, 空Context, HashLen)
verify_data = HMAC(finished_key, 到服务器Finished之前的握手摘要)

客户端Finished采用客户端握手流量秘密。这里基础输入是对应方向的握手traffic secret,不是主干Handshake Secret,更不是刚派生出的AES key。Finished不把自己包含在自身认证输入里;服务器的输入包括CertificateVerify,客户端的输入则还包括服务器Finished及其后实际发送的有关握手消息。RFC 8446 §4.4.4。

在SHA-256套件下,verify_data为32字节,Finished完整消息为36字节。本例服务器verify_data为:

text
9b9b141d906337fbd2cbdce71df4deda4ab42c309572cb7fffee5454b78f0718

该结果也已由本书独立复算。接收者必须验证,不匹配就中止,不能因为前面证书签名正确就跳过。记录AEAD证明记录在当前保护材料下有效;CertificateVerify绑定身份与握手;Finished确认当前秘密及握手记录一致。这些检查处于不同层次,不能相互抵销。

普通服务器证书握手并未认证一个具体客户端账号。客户端即使正确发出Finished,也可能只是一个没有网站登录状态或代理使用资格的网络参与者。若服务器请求客户端证书,客户端按规定发送Certificate、必要的CertificateVerify及Finished;没有可用证书时的空列表及服务器接受策略又是独立分支。下一节先按只有服务器证书认证的完整流程追踪状态切换。

16.6 完整1-RTT握手中的三个完成时刻 ​

现在把前面所有字段和派生放回实际顺序。限定为新建的TLS1.3连接,使用证书认证服务器、X25519临时交换,没有PSK、0-RTT、HRR或客户端证书;运输是已经建立的可靠TCP字节流。以下流程不把TCP建连与DNS时间混入TLS自己的往返计数。

正在绘制图表…

查看图表原文
sequenceDiagram
    participant C as 客户端
    participant S as 服务器
    C->>S: ClientHello:能力、随机值、X25519公开值
    Note over S: 选择参数,计算Z及双向握手材料
    S-->>C: ServerHello:选择、随机值、X25519公开值
    S-->>C: 握手保护:EncryptedExtensions、Certificate、CertificateVerify、Finished
    Note over S: 已发自身Finished,可按规则发送应用数据
    Note over C: 计算握手材料,解开记录并验证身份与Finished
    C->>S: 握手保护:客户端Finished
    C->>S: 应用保护:请求字节
    Note over C: 已验证对方并发送自身Finished
    Note over S: 收到并验证客户端Finished,服务器握手完成
    S-->>C: 应用保护:响应字节

图上几条箭头不意味着分别占用独立TCP段。服务器可在一个发送批次中放ServerHello记录和受保护握手记录,客户端也可紧接Finished记录发送应用记录。记录保留各自边界与密钥选择,TCP仍只承载连续字节。

EncryptedExtensions接收ServerHello没有携带的协商响应,例如应用协议或其他已提议的能力。它的握手类型为08,消息体为2字节扩展列表长度及扩展列表;不是因为名字里有Encrypted,就在握手消息体里再定义一层独立密码。它之所以保密,是被放进已使用服务器握手key保护的TLS记录。

RFC8448的具体布局使这一点格外清楚。服务器把40字节EncryptedExtensions、445字节Certificate、136字节CertificateVerify与36字节Finished,合成657字节连续握手内容。记录保护时追加1字节内部内容类型16,得到658字节AEAD明文;AES-GCM追加16字节标签,得到674字节输出;再加5字节公开记录头,线上为679字节。

text
公开头:17 03 03 02 a2
受保护部分:657字节握手内容 | 内部类型16 | 16字节标签
尺寸关系:  657 + 1 + 16 = 674 = 0x02a2
整条记录:  5 + 674 = 679

图中“内部类型16”是加密前位置,线上观察者看不到该明文字节。公开头17只表明外层采用受保护记录形式,不能据此断言里面已经是网站应用数据。该记录负载密文实际以d1 ff 33 4a 56 f5 bf f6开头;本书已用16.4节派生出的服务器握手key、序号0对应nonce和5字节头作为A,成功打开完整标准记录,得到上述658字节。

客户端验证完服务器证明后,构造自己的36字节Finished,使用客户端握手key保护。其内部明文是36字节消息再接16,输出53字节,记录头为17 03 03 00 35,总长58字节。该标准记录也已独立打开核对。两个方向各有自己的握手密钥与起始序号,不是服务器已发一条记录就把客户端初始计数也变成1。

握手阶段结束与双方能计算应用材料并非同一个事件。应用traffic secret第0代的摘要范围到服务器Finished为止,所以两端此时已经可以算出应用key;但客户端仍需先验证服务器身份和Finished,发送自身Finished,再按状态使用应用保护。客户端Finished本身仍使用握手保护,不能因为应用key已经算好就提前切换这条消息的密钥。

服务器在发完自己的Finished后,标准允许它尚未收到客户端Finished就发送应用数据。不过它还没有确认客户端在本次交互中的活性,也未完成客户端认证。对于服务器主动发送的数据,这是一项明确的状态限制,不能把“允许发送”误解成“双方都完成所有认证”。接收应用数据的业务若要求特定客户端身份,还要等相应条件成立。

若单程传播为r/2、忽略计算、序列化、排队和丢包,C在t=0发送ClientHello,S约在r/2发送首批回复,C约在r时能完成验证并发送Finished与应用请求,S约在3r/2验证客户端Finished。所谓1-RTT主要描述客户端在一个TLS往返后可以进入正常应用发送阶段;不是说服务器也恰在t=r完成,更不是说请求与响应都已回来。加上新TCP握手时,还需另算运输建立成本。

本向量这部分TLS记录字节为201+95+679+58=1033,尚不含后续会话票据、应用记录、兼容CCS、TCP/IP和链路头。它只是这个测试证书与扩展集合下的结果。真实证书链、更长名字、不同组、客户端认证、分片或重试都能改变尺寸,不能把1033写成所有TLS1.3握手固定开销。

在正常证书模式下,RSA或Ed25519等身份算法并没有逐字节加密整段代理负载,网站正文也不是“用证书公钥加密到服务器”。公钥机制完成交换与身份绑定,握手和应用阶段的数据则各自由派生出的AEAD材料保护。这条实际处理路径会成为后文分析双层TLS及Vision优化的基础。

16.7 应用记录、nonce、密钥更新与关闭 ​

进入应用阶段以后,TLS不再需要知道上层是在发送HTTP、VLESS还是其他字节。它把有限长度content后接1字节真实内容类型,再按需要补零,形成TLSInnerPlaintext;对其执行AEAD,外面放5字节公开头。对于应用字节,内部类型是17;对于加密握手是16;对于警报是15。以上均为十六进制。

text
公开记录头:17 | 03 03 | 加密输出长度,2字节大端
AEAD明文:  content | 内部类型 | 可选零填充
AEAD的A:   完整5字节公开记录头

因此记录头可以读出,但其值也参与认证。攻击者改写长度可能首先导致收集或边界错误,若仍形成某个候选记录,认证也不能按原结果通过。接收者必须先检查尺寸上限,不允许为一个未经验证的长度无限分配缓存。RFC 8446 §5.2。

限定16字节标签且没有填充,传送abc三字节时,AEAD明文是61 62 63 17,加密输出4+16=20字节,公开头为17 03 03 00 14,整条记录25字节。若加8字节零填充,明文变成12字节,记录33字节。这里给出的是布局和长度构造,没有编造密文;实际结果还依赖该方向当前key、IV和序号。

填充放在内部类型后面,接收者认证成功后从末尾向前跳过零,找到第一个非零字节作为内部类型,它之前的内容才交给上层。这样业务内容本身可以以00结尾,不会被当成填充误删。若整个打开结果全零、找不到类型,需要按错误中止;应用内容可以为空,但Handshake与Alert内容不能用空记录替代。

TLS1.3记录nonce不作为一个完整新字段附在每条记录上。两方向分别维护64位记录序号,某把流量key下的第一条受保护记录使用0,此后逐条增加。将序号编码为8字节大端,在左侧补零到IV长度,然后与对应write_iv异或,形成N。AES-GCM和ChaCha20-Poly1305的常见IV长度均为12字节。RFC 8446 §5.3。

例如,RFC8448服务器应用IV为cf 78 2b 88 dd 83 54 9a ad f1 e9 84。该应用key下序号0的nonce等于IV;序号1把最后字节84异或01,变成85,其余不变。它并不是把原IV当12字节大端整数直接加1;到了有进位的情况,两种运算的关系会不同。

序号覆盖该key下所有受保护记录,而不只计网站应用数据。接收者从可靠有序流重建相同状态,所以不必在线显式发送完整序号;删除、插入或调换记录会让后续验证状态不匹配。TCP重传已经发送的密文字节,不会使TLS再把它当成一条新记录增加计数。更换key后记录序号重新从0开始,因为它已经处于不同密钥域;同一key下不允许序号回绕。

默认上限也应区分层次:普通内容最多2¹⁴字节;完整TLSInnerPlaintext最多2¹⁴+1字节,包括内部类型和填充;TLSCiphertext.length的通用上限是2¹⁴+256,允许考虑标准定义的AEAD扩展范围。对16字节标签的常见构造,实际最大加密输出为2¹⁴+1+16,不能因为通用外层上限更大就增加同等数量的内容。

record_size_limit扩展可以进一步降低接收限制。在TLS1.3中,它约束完整TLSInnerPlaintext,包含内部类型与填充,不包含外层5字节头或AEAD标签。RFC8448中的4001就是16385,等于默认完整内部明文上限。它不是“每条业务正文可以有16385字节”。两端分别通告自己能接收的限制,服务器在EncryptedExtensions回应。RFC 8449 §4。

即使序号还远没用完,也必须考虑密码学用量限制。RFC8446 §5.5给出AES-GCM在其分析条件下约2^(24.5)条满尺寸记录的范围,并建议在达到相应限制前更新密钥;现行RFC9846 §5.5将要求收紧为达到限制前必须更新或关闭,并明确范围是同一组key。这一数值不是所有AEAD、任意记录尺寸和任意失败次数共享的万能常数。实现可以更早更新,不能把64位计数空间当成允许同一key无限处理数据的依据。

应用流量密钥通过KeyUpdate更新。其握手消息类型为18,消息体只有1字节request_update,00表示不请求对方更新,01表示请求。请求更新的完整握手消息为18 00 00 01 01,它仍放在内部类型为16的受保护记录中,而非裸发五字节。

发送者先用旧发送key保护并发送KeyUpdate,再计算下一代traffic secret:

text
next_secret = Expand-Label(current_secret, traffic upd,
                            空Context, HashLen)

随后从next_secret派生新key和IV,发送序号重置为0。接收方用旧接收key打开KeyUpdate,再同步切换其接收方向。两个方向更新互相独立;若对方请求更新,本方须在发出下一条应用记录前发送一个不再请求更新的KeyUpdate。不能跳过旧key下的更新消息,直接尝试接受新key记录。RFC 8446 §4.6.3、§7.2。

现行RFC9846 §4.7.3还要求发送实现将epoch即密钥更新次数限制在2^48−1以内,接收实现不能把这个发送上限当作需强制检查的对端规则。达到不能继续更新的状态时,还要在key用量耗尽前终止。KeyUpdate不执行新ECDH,也不是TLS重新协商。已知current_secret的人可能继续算出next_secret,因此它不能单独治愈当前秘密失陷;删除旧代后,新代通常不应反推出旧代,提供的是另一方向的历史隔离。这里正对应第15章的依赖图分析。

正常关闭则使用close_notify警报。警报本身有1字节level与1字节description,close_notify的description为00;在正常已建立状态下,它必须由当前记录保护,不是直接向TCP流写一对明文字节。它表示发送方向不再有TLS消息,TLS1.3允许另一方向按状态继续完成发送,不能与整个Socket立刻双向关闭混同。RFC 8446 §6.1。

若TCP先断开而没有收到close_notify,接收者无法只凭TLS判断发送者是否还发过被截断的数据。应用若已有受认证的明确总长与完成条件,可以按自己的规则判断对象;否则不能把裸EOF当作受认证的正常结束。反过来,close_notify也不证明一笔业务已成功提交,仍要看上层响应。安全通道的记录、方向结束与业务事务各有自己的状态。

16.8 PSK与会话恢复:票据是索引,binder证明掌握秘密 ​

完整证书握手以后,双方已经共同拥有恢复主秘密。服务器可以为将来的连接发NewSessionTicket,客户端保存票据和对应PSK及必要上下文,下次用它们建立恢复连接。恢复减少某些证书与签名工作,不等于复用旧TCP连接,也不必然意味着启用0-RTT。

NewSessionTicket是握手类型04,但在主握手之后使用应用traffic key保护。其结构为:

顺序字段长度与单位
1ticket_lifetime4字节大端,秒
2ticket_age_add4字节大端,随机年龄掩蔽值
3ticket_nonce1字节长度前缀及对应字节
4ticket2字节长度前缀及非空票据
5extensions2字节总长及扩展列表

外面还有4字节握手头。ticket_nonce在本连接发出的各票据之间唯一,用来派生不同PSK;ticket则是客户端下次发回的身份标签。二者不是同一个字段,也不是每条记录使用的AEAD nonce。对应PSK由Expand-Label(恢复主秘密,resumption,ticket_nonce,HashLen)得到,Context直接使用该票据nonce,不再对它先计算握手摘要。RFC 8446 §4.6.1。

ticket可以是服务器数据库的查找键,也可以是由服务器自己加密认证的状态容器。客户端把它当不透明标签,不需要能解开内部。网络上再次出现ticket,不等于直接发送PSK;客户端仍要证明自己持有对应秘密。服务器若采用自包含票据,其票据保护密钥与握手身份密钥又是不同材料,泄露和轮换范围应独立分析。

票据寿命上限为604800秒,即7天,0表示立即丢弃;客户端也可更早删除,服务器可更早拒绝。客户端报告年龄时却以毫秒计算,将从接收票据起经过的年龄与ticket_age_add相加,模2³²放入obfuscated_ticket_age。这只是年龄的掩蔽表示,不是绝对时间戳,更不是单凭它就保证一次性使用。

服务器通常在收到客户端Finished后发送票据;标准还允许在未请求客户端认证等规定条件下预计算剩余握手记录,从而提前发送。不能把这项优化写成“服务器不需要验证Finished”。现行RFC9846还明确处理不支持恢复的客户端应忽略票据,以及发送票据需要相应PSK交换能力提议的条件。

下一次ClientHello通过pre_shared_key扩展0029提供PSK身份列表和对应binder列表。每项身份有2字节身份长度、身份字节和4字节混淆年龄;binder列表有2字节总长,每个binder再以1字节长度开头。一个SHA-256 binder占32字节内容,并不是任意固定16字节认证标签。多个身份与多个binder按顺序一一对应。

这个扩展必须放在ClientHello扩展列表最后。对恢复PSK,先从Early Secret以res binder标签形成binder_key,再以finished标签、空Context派生对应finished_key,最后用HMAC认证截断ClientHello的摘要。截断点在identities字段结束,排除整个binders列表;消息总长、扩展区长度和本扩展长度却按完整binder都存在时填写。这样避免了证明把自己包含进去的循环,也绑定了前面的协商提议。RFC 8446 §4.2.11.2。

若有HRR,第二次binder还必须纳入已规定的ClientHello1合成摘要与HRR历史,不能只对一个全新ClientHello2计算。完整握手的其他摘要仍使用带binder的完整ClientHello,只有binder自身计算才作这项截断。外部分发PSK则用ext binder区分来源,不能任意交换两个标签。

服务器选择一个可接受PSK,验证对应binder,在ServerHello的pre_shared_key中返回2字节selected_identity,即从0开始的列表索引。客户端检查索引范围、PSK关联散列与选择套件一致,并检查所需key_share存在。套件散列兼容并不总要求AEAD名字与旧连接相同;但0-RTT的接受条件更严格,下一节将单独列出。

psk_key_exchange_modes说明两种路径:00是psk_ke,不加入新鲜非对称交换;01是psk_dhe_ke,将PSK与新的交换结果共同投入密钥树。采用后一种时仍有ClientHello和ServerHello的交换材料,但通常省去服务器Certificate和CertificateVerify,用PSK绑定与Finished完成相应认证。只有前者没有新鲜交换贡献,不能将其历史保护性质与后者混同。

未知或不再接受的票据可以导致恢复被拒绝,并在条件允许时转成完整握手;不一定使整个连接永久失败。PSK被接受却binder错误,则不能把这条失败认证当成合法恢复继续。恢复结果也要绑定正确服务与应用上下文,不能把缓存中任何一份“曾经连过”的票据跨任意目标使用。

16.9 0-RTT提前发送的代价是另一组安全条件 ​

TLS1.3的0-RTT不是第一次见到服务器就凭空建立秘密。客户端需要一个已经获得或预先配置、允许早期数据的PSK及配套参数,才能在发出ClientHello后、不等待本次服务器响应,立即发送受保护应用数据。0指不用先等待一次TLS往返,不表示数据无需传播时间,也不自动免除TCP建立或DNS。

早期保护来自16.4节的分支:从PSK提取Early Secret,再以c e traffic标签和本次ClientHello摘要派生client_early_traffic_secret,进而得到key和IV。此时新的服务器交换值还没回来,所以早期密钥不包含本次新鲜服务器秘密。客户端自己刚生成的key_share虽然已在ClientHello里,它的公开字节被纳入摘要,并不能替代双方共同秘密Z。

因此,后来得到相应PSK的攻击者,结合录下的ClientHello,可能恢复这段早期流量;它不具有与新鲜双向ECDHE后应用数据相同的完整前向保密。后续1-RTT数据若正确混入新的交换结果,安全范围又不同。不能因为两者在同一条TCP连接上,就把早期阶段的性质套给所有记录,或把正常阶段的PFS反向套给早期记录。

early_data扩展002a因位置不同而有不同内容:NewSessionTicket中有4字节max_early_data_size,表示允许的早期应用内容字节数;ClientHello与EncryptedExtensions中的该扩展内容为空。后两者的扩展本身仍有4字节类型与长度头,即00 2a 00 00。上限只计应用明文内容,不计内部内容类型和TLS填充,但服务器仍需限制实际收集的资源。

客户端只用PSK列表第一项保护早期数据。服务器要接受,必须选择索引0,并保证关联的TLS版本、完整套件与ALPN等参数符合早期上下文,验证票据年龄和binder等条件,再在EncryptedExtensions中明确回应early_data。仅同意PSK恢复并不表示同意早期数据;恢复可以继续成功而早期部分全部被拒绝。RFC 9846 §4.3.10—4.3.11。

正在绘制图表…

查看图表原文
sequenceDiagram
    participant C as 已有PSK的客户端
    participant S as 服务器
    C->>S: ClientHello:PSK身份、binder、early_data、交换材料
    C->>S: 早期保护:应用请求
    Note over S: 检查PSK、上下文、年龄与防重放状态
    S-->>C: ServerHello
    S-->>C: 握手保护:接受early_data的EE、Finished
    C->>S: 早期保护:EndOfEarlyData
    C->>S: 握手保护:Finished
    C->>S: 应用保护:后续数据

这张图选择PSK恢复且接受早期数据的分支,没有服务器证书消息。EndOfEarlyData是握手类型05、空消息体,因此完整消息05 00 00 00共4字节。它仍使用早期traffic key保护,标记后续记录将切到客户端握手key;不是先用握手key保护它再宣布切换。没有接受早期数据时,客户端不能发送这条消息。QUIC另有接口规则,不发送TLS的EndOfEarlyData,不能把上图直接复制成QUIC包序列。

真正特殊的风险是重放。攻击者可以复制ClientHello与早期密文,另开连接再交给服务器。即使它不知道PSK,也可能让旧binder与早期记录继续通过相应验证,因为这些字节原本就是合法生成的。攻击者不一定能完成新的正常握手,但如果服务器已经执行了早期请求,副作用已经发生。标签防改写,不自动防原封不动复制。

标准要求服务器基础设施中的每个处理实例对同一早期握手至多接受一次,并建议采用更强的整体保护。可用方法包括一次性票据数据库,或记录由已验证ClientHello部分导出的唯一值,在适当时间窗口拒绝重复。若多个节点各有独立缓存,却都接受同一票据,可能每个节点各执行一次;“单机没有重复”不等于全局只执行一次。RFC 9846 §8。

时间窗口用来限制需要保存的状态,却不能单独排除窗口内重放。服务重启、缓存丢失、多个区域之间的一致性和票据的接受范围,也都影响保证。对有误报可能的近似记录结构,标准允许因疑似重复拒绝0-RTT并继续普通握手,不能把误报直接当作应终止全部连接的证据。认证过的binder或相应已验证字段才适合参与重放索引,不应把未验证的其他binder当作唯一性基础。

TLS拒绝早期数据以后,应用可以在正常握手完成后决定是否重发。TLS层不应盲目自动重发;如果ALPN改变,还可能连字节格式都需要重建。更复杂的是一份请求已在另一服务器副本被执行,本次连接却报告早期拒绝,随后应用重试又执行一次。TLS防重放和业务去重因此需要共同考虑,不能把握手最终完成当作“从未在别处执行”的证明。

HTTP为这种情况定义了Early-Data字段和425 Too Early等协作机制,让源站表达请求不适合提前处理;中间节点终止TLS再转发时,也要保留相应语义。方法的安全性与幂等性提供判断线索,但一个标为GET的具体资源仍可能具有实现上的副作用,不能仅凭方法名证明任意重放无害。RFC 8470。

代理请求也有动作语义:建立出站连接、分配UDP关联、消耗配额等都可能发生在目标正文之前。某个代理协议称其路径为0-RTT时,必须进一步检查早期数据依赖什么秘密、服务端何时执行、重放状态存在哪里,以及哪些阶段具有PFS。TLS在这里提供的是精确比较基准,不能把其限制不加追踪地套到其他同名设计,也不能假定其他设计自动消除了这些问题。

16.10 从公开DCID算回QUIC Initial的保护材料 ​

第12章给出一份1200字节QUIC Initial,承诺在密码学基础建立后解释实际保护计算。现在使用RFC 9001附录A完成这条链。它与前几节RFC8448的TCP握手是两份独立标准向量,ClientHello和临时密钥不同,不能把前面服务器握手key直接搬过来。

QUIC v1的Initial使用固定版本盐值,以及客户端所选、明文可见的初始Destination Connection ID进行HKDF提取。后续Handshake与1-RTT才使用TLS提供的相应流量秘密;Initial必须在这些秘密尚未建立时就能处理,所以采用不同来源。

text
v1 initial_salt = 38762cf7f55934b34d179ae6a4c80cadccbb7f0a
本例DCID       = 8394c8f03e515708
initial_secret = Extract(initial_salt, DCID)
               = 7db5df06e7a69e432496adedb0085192
                 3595221596ae2ae9fb8115c1e9ed0a44

v1 Initial固定使用HKDF-SHA-256与AES-128-GCM,而非等ClientHello选完套件再决定如何打开ClientHello。用标准的TLS1.3 Expand-Label编码,Label分别为client in和server in,Context为空,各派生32字节方向秘密;完整标签仍以tls13加空格开头。客户端方向结果为:

text
c00cf151ca5be075ed0ebfb5c80323c42d6b7db67881289af4008f1f6c357aea

再从该秘密以quic key、quic iv、quic hp三个Label和空Context派生:

用途长度本例结果
负载AEAD key161f369613dd76d5467730efcbe3b1a22d
负载IV12fa044b2f42a3fd3b46fb255c
包头保护hp key169f50449e04a0e810283a1e9933adedd2

hp key与负载key不同。Label区分用途,但这些Initial输入没有未知秘密,因此任何得到本包公开信息的人都能重复派生。HKDF不能把公开输入变成只有通信者知道的秘密。这就是Initial虽有加密和认证标签,却不能提供抵抗网络旁观者的内容保密的直接原因。

本包未保护头为22字节:

text
c3 00 00 00 01 08 83 94 c8 f0 3e 51 57 08
00 00 44 9e 00 00 00 02

包号是2,以4字节发送。将重建后的完整包号2左补零到12字节,与IV异或,nonce为fa044b2f42a3fd3b46fb255e。AEAD的A是完整未保护头,包括这里的包号;P是1162字节帧内容,其中CRYPTO帧245字节、PADDING共917字节。CRYPTO以06 00 40 f1开头,承载241字节TLS ClientHello;其握手头01 00 00 ed表示237字节消息体,没有5字节TLS记录头。

AES-GCM输出为1162字节负载密文加16字节标签,共1178字节。加上22字节头正好1200;头中Length=1182则按QUIC规则另计4字节包号,所以是4+1162+16,不是1178。密文最前16字节为:

text
d1 b1 c9 8d d7 68 9f b8 ec 11 d2 42 b1 23 dc 9b

下一步才保护包头。QUIC从包号字段起点加4的位置取16字节密文样本;这里包号恰好4字节,所以样本正好是上述密文开头。固定加4这一取样规则允许接收者在不知道真实包号长度前先定位样本,不能随意改成“从包号后立即开始”,因为其他包的包号编码可能不足4字节。

用hp key对16字节样本执行AES单分组变换,取输出前5字节得到mask=43 7b 9a ec 36。长包头只把首字节最低4位与mask首字节对应位异或,c3于是变成c0;4字节包号与mask后4字节异或,00000002变成7b9aec34。最终受保护头为:

text
c0 00 00 00 01 08 83 94 c8 f0 3e 51 57 08
00 00 44 9e 7b 9a ec 34

接收顺序反过来:依据未隐藏的长头字段找到包号起点与样本,算mask,先还原首字节,再读出真正包号长度,还原发送的包号字节,结合接收状态重建完整包号,最后形成nonce和A打开负载。不能先把c0最低两位当作真实长度,只取一个包号字节解密。一般包号可能是截断表示,不能把本例已知完整值2的便利推广到所有包。

本书已经从公开盐值与DCID独立派生所有材料,重新加密1162字节帧内容、计算mask、形成完整1200字节包,与标准逐字节相同,并核对能够打开。这兑现的是公开向量的计算链,不是某个真实用户连接抓包,也没有宣称其测试ALPN就是一个生产HTTP/3服务。

Initial的公开可推导材料也意味着有能力的主动攻击者可形成某些有效Initial保护,不能靠该标签独立认证服务器身份。后续TLS认证、握手记录一致性和不同级别的秘密仍然必要。相反,Handshake和1-RTT的保护材料不是直接由公开DCID产生,不能因为Initial可还原就推断所有QUIC内容都可还原。协议阶段与密钥来源再次决定保证范围。

16.11 从记录与密钥阶段还原一条会话 ​

继续观察RFC8448 §3的完整示例,可以把已经解释的格式、密钥和状态连在一起。下面按该文展示顺序列出记录;握手期间两条Hello明文可读,其余记录都需选择正确方向、正确阶段的材料。表中的序号是在对应key下的隐含序号,不在线上新增一个字段。

发送方向与内容整条记录字节数保护状态对应序号
C→S ClientHello201未保护Hello—
S→C ServerHello95未保护Hello—
S→C EE、证书、签名、Finished679服务器握手key0
C→S Finished58客户端握手key0
S→C NewSessionTicket227服务器应用key第0代0
C→S 50字节应用内容72客户端应用key第0代0
S→C 50字节应用内容72服务器应用key第0代1
C→S close_notify24客户端应用key第0代1
S→C close_notify24服务器应用key第0代2

这一组记录总计1452字节,不包含运输和链路封装。它的应用内容是从00到31的50个连续字节,不是浏览器HTTP抓包,也不能据此断言上层一定采用某种代理协议。两端发送相同应用明文,密文仍然不同,因为方向key与nonce不同。

NewSessionTicket揭示了一个容易误读的位置:它的内部内容类型是握手16,但采用的已经是应用traffic key。所以服务器发送第一条网站或代理应用记录时,其应用序号已经是1。不能只在看见内部类型17时增加计数,也不能在每个应用请求开始时把序号清零。握手消息种类与用于保护它的密码阶段不是同一个维度。

客户端应用方向没有先发送票据,因此其第一条50字节应用记录用序号0。收到服务器的票据也不影响客户端发送计数,计数是分别维护的。随后两个方向的关闭记录各使用自己当前的下一序号。close_notify的2字节内容后接内部类型15,形成3字节AEAD明文;加16字节标签和5字节头就是24字节,而不是又进行一次独立关闭握手才得到这个尺寸。

本书独立打开了该向量的227字节票据记录、两条72字节应用记录与两条24字节关闭记录,确认上述序号、内部类型和应用内容。若误将服务器应用记录的nonce设为序号0,就不能通过认证。这个差别完全可以由协议状态说明,不需要把认证失败笼统归因于网络质量或“加密不兼容”。

从只观察网络的一方看,后面几条记录的公开类型都为17。它能看到尺寸和方向,却不能仅由这一字节分辨票据、应用内容或警报。若能取得测试向量公开给出的秘密,就可以解开;真实新鲜ECDHE连接则不能因为证书私钥后来可得,就照搬这种复现条件。公开向量故意披露秘密,与生产连接的可解密性不是同一事实。

实际分析还要保留接收缓冲状态。一条679字节记录可能分多次读取,第一次只取得头和一部分密文,此时还没有完整标签,不能交付候选明文;反过来,一次读取可以同时获得票据记录和下一条应用记录,必须按长度逐条消费。TCP流定界、记录认证和上层消息解析仍然逐层衔接,不因握手已完成而合成一个无边界的大块。

如果受保护记录失败,先检查使用的方向、阶段、代数、序号、完整头和密文字节是否与状态一致;但接收协议不能为了容错而无限尝试任意历史密钥或交付失败明文。HRR、早期数据拒绝和兼容CCS等场景各有标准规定的状态分支,不能把它们推广成“遇到错误就跳过一些字节继续猜”。

对于嵌套网站TLS的代理,以上状态还会在不同层分别出现。外层应用content可能恰好是一个内层TLS记录头及密文;外层解密成功后交给代理,再转发给网站,内层握手和应用密钥仍由浏览器与网站管理。后文讨论Vision识别和直接转发时,需要判断识别的是哪一层、当前哪一阶段,不能看到17就直接推断可以绕开任何保护。

TLS1.3已经把协商、身份、临时交换、派生、记录与恢复组织为一套完整状态机。但其中的X25519与RSA、Ed25519等经典公钥工具,对未来有足够能力的量子计算存在另一类风险。这个风险和密钥生命周期、PFS不是同一条轴线,下一章将从具体威胁与KEM接口开始说明。

从字节与状态机出发,理解每一层的职责。