本节摘要:本节把 TLS 1.3 的完整握手逐条消息推一遍:每条消息携带什么密码学材料、它存在的理由、以及"删除实验"——如果删掉它,哪一类攻击立刻得手。顺带拆解 1-RTT 与 0-RTT 的取舍,0-RTT 的重放窗口是评审里最常被放过的坑。读完你应当能徒手画出握手时序,并对每个字段答出"为什么在场"。
一个安全信道握手必须同时办成三件事:协商(双方就使用的算法套件达成一致)、认证(确认对方身份)、密钥建立(产生只有双方知道的会话密钥)。三件事互相牵制:协商要在双方还互不认证时进行——于是"协商结果可被中间人操纵"成为首要威胁;认证依赖证书体系——于是证书链验证是整条生命线的锚;密钥建立用临时密钥——于是前向保密成为可能。TLS 1.3 的每个设计决定都能还原为这三件事的某种权衡。
先立命题(按 2.3 节模板):在敌手可截获、篡改、注入、重放全部流量,且不掌握服务器私钥与双方临时密钥的前提下,握手结束后,除两端外无人能导出会话密钥(机密性),且任何对握手消息的篡改都会使握手以失败告终(握手完整性)。留意这个命题的两个边界:不承诺客户端默认被认证(那是可选的客户端证书),不承诺 0-RTT 数据防重放(后面细说)。
第一条消息 ClientHello。 客户端发出:支持的密钥交换组与签名算法偏好列表、随机数、以及一个关键设计——提前带上自己的临时公钥(对每个支持的组各带一个)。为什么提前带?为了把往返次数压到一次:服务器收到后可以立即完成密钥交换计算,不用再等客户端的第二轮材料。防什么:套件偏好列表是明文的,中间人可以删掉好选项诱导降级——所以 TLS 1.3 把整个握手纳入后续的完整性校验,任何删改都会在最终验证时炸出来(这就是"降级防御",6.3 节展开)。随机数在场的原因之一是保证即使临时密钥生成有偏差,握手输入也带新鲜熵。
第二条消息 ServerHello 与随后的加密Flight。 服务器选定套件,带上自己的临时公钥与随机数。至此双方各持对方临时公钥与自己的临时私钥,可以独立算出同一个共享秘密——1.3 节演示过的那套运算。紧接着服务器发送加密的扩展、证书、以及证书验证消息:用长期私钥对"至此全部握手消息的哈希"签名。这里正是 3.1 节教训的落点:签名对象是整段握手转录(含双方临时公钥、套件选择、随机数),身份与整场协商被显式绑定在一起——敌手不可能把自己插入协商却让签名依然通过。NS 案例里缺的那个字段,在 TLS 1.3 里以"对转录哈希签名"的形式无处不在。
第三条消息客户端 Finished。 客户端验证证书链(域名是否匹配、签发链是否可信、有效期与吊销状态),然后发出自己的完成验证:一个用派生密钥计算的校验值,覆盖全部握手消息。服务器验它,双方各自再算一次"服务器完成验证"交换。为什么完成验证要双向各一次?因为认证必须是"对同一份转录的相互确认"——任何一方看到不同的转录(被篡改过),完成值就对不上,握手失败。三件事至此办结:协商被转录锁定、身份经证书与签名确认、会话密钥从临时材料派生。
【TLS 1.3 完整握手 · 手推笔记】 客户端 服务端 |---- ClientHello ------------------------>| | 套件偏好 + 随机数 + 临时公钥 | | | 选套件, 生成临时密钥对 |<--- ServerHello -------------------------| | 选定套件 + 随机数 + 服务器临时公钥 | |<--- {扩展, 证书, 证书验证签名, 服务端完成}--| ← 从这里起已加密 | 验证证书链: 域名匹配, 链可信, 未吊销 | | 验证签名对象 = 全部握手消息的哈希 | |---- {客户端完成} ------------------------>| | | |========= 应用数据, 用派生密钥加密 =========| 删除实验: 删"证书验证签名" → 中间人各与两端协商, 重演 3.1 案例 删"转录哈希进入签名" → 篡改套件偏好不被察觉, 降级攻击得手 删"临时密钥改用静态RSA" → 今日录制的流量, 待私钥泄露之日全部可解
手推通常到 Finished 就停,但有一条暗线值得补一笔:会话密钥不是一步算出来的,而是分层派生的。双方拿到共享秘密后,用密钥派生函数串出一条链——先派生"握手期加密密钥"(保护服务器证书那一段),再派生"应用期加密密钥"(保护真正的业务数据),派生过程的每一步都把整段握手转录的哈希搅进去。这个设计有两个妙处。其一,握手消息与应用数据的加密材料分离:握手还在协商中时就有加密保护可用,而应用密钥只有在全部握手消息验证通过后才启用——半成品状态攻不进成品区。其二,转录哈希反复入场,等于把"我们协商了什么"反复烧进每一把钥匙——任何对协商过程的篡改,最终都会表现为"双方派生出不同的密钥、第一条应用消息解不开",篡改自动暴露。
顺带厘清一组评审容易混的概念:会话恢复不等于 0-RTT。会话恢复指重连时复用上次会话派生的恢复密钥来省掉证书往返——它仍有完整的一次往返,且恢复握手同样受转录保护,没有重放问题;0-RTT 是更激进的"第一条消息就带数据",省掉的是那个唯一的往返,也因此暴露了重放面。评审里听到"我们用了会话恢复所以有重放风险"或"我们没用 0-RTT 所以连恢复也是安全的",两种说法都混淆了边界,都要拦下来重新表述。
TLS 1.3 支持重连时的 0-RTT 模式:客户端凭上一次会话恢复的早期密钥,在第一条消息里就捎带应用数据,省掉一个往返。甜点是延迟,毒刺是重放:早期数据用同一份早期密钥加密,协议层没有任何机制阻止同一段 0-RTT 数据被原样投递两次——敌手截获后重放,服务器会当两次独立请求处理。若这段数据是"下单""转账"类非幂等操作,重放就是真金白银的损失。工程结论要背下来:0-RTT 数据必须只承载幂等操作,或者应用层自带去重(一次性序号、请求令牌)。评审里看到"我们开了 0-RTT 提速",第一问就该是"重放窗口谁在管"。
💡 手推任何握手协议时的通用检查表:协商是否被转录锁定(防降级)、认证是否显式绑定身份与转录(防拼接与会话串线)、密钥是否来自临时材料(防历史泄露)、重放窗口是否显式关闭或声明(防 0-RTT 类问题)。四问过完,握手的骨架健康度基本有数。
还有一个评审常被问到的问题:握手把三件事压进一个往返,安全上是赚是赔?答案——多数维度是赚。往返少意味着认证与密钥建立更快完成,"未认证窗口"(连接已建立、身份未确认的间隙)反而更短;提前携带临时公钥没有牺牲任何随机性,密钥交换的熵一个比特没少。真正的代价集中在 0-RTT 一处,而它的边界刚刚讲清。这个问答值得记住,因为它是"性能与安全对立"这类惯性叙事的一个现成反例:好的设计常常两者都拿——前提是设计者像 TLS 1.3 的作者那样,把每个字段的存在理由都对着威胁模型论证过一遍。
下一节把镜头转向另一类信道:没有服务器居中、消息异步到达、双方都是"客户端"——Signal 的双棘轮为此把密钥演化做成了连续翻页的结构,前向保密从"会话级"细化为"消息级"。