第4章:TLS握手协议概览——完整握手与简化握手
TLS握手协议是整个TLS安全通路的奠基环节。说白了,就是在客户端和服务器正式传输数据之前,先互相打个照面、亮个身份、商量好用什么加密方式。我经常跟团队里的新人说:TLS握手就像你第一次去一家高端餐厅,先跟主厨互报姓名、确认菜单、再约定好上菜节奏——这套流程一旦乱了,后面的菜谁也吃不下。
让我从两个最核心的握手场景讲起:完整握手和简化握手。前者是初次建连,后者是复用会话。嗯,这里每一个细节都可能是你线上故障的源头,我踩过的坑后面会一一交代。
4.1 完整握手(Full Handshake)
完整握手是客户端和服务器之间第一次建立TLS连接时走的流程。它包括四个消息往返(1.5 RTT 或 2 RTT,取决于TLS版本),包含证书校验、密钥协商和握手完整性验证。我习惯把它的核心步骤拆成下图这样:
上面这张图把完整握手的四个阶段拆得很清楚。我个人习惯把阶段1和阶段2叫做“信道建立期”,阶段3和阶段4叫“密钥确认期”。
4.1.1 阶段1:问候与协商
客户端发送ClientHello,包里包含支持的TLS版本列表、密码套件列表、一个随机数(client_random),以及可选扩展(比如SNI)。服务器收到后,挑一个双方都支持的最高版本和最强密码套件,发回ServerHello,同时附上server_random。这一轮交换后,双方就有了共识:用什么协议、什么算法、以及两个随机数。
很多人在这个地方栽过跟头——比如服务器没发ServerHello而是直接中断连接。我记得有一次排查线上问题,发现客户端发了TLS 1.3的ClientHello,但服务器只支持TLS 1.2,结果服务器直接报了TLS版本不匹配的alert。嗯,这种情况其实很常见,解决方法是确保服务端的TLS版本配置别太死。
4.1.2 阶段2:证书与密钥交换素材
服务器接着发Certificate,把数字证书链丢给客户端。客户端要验证这条链是否可信(根证书是不是预埋的、有没有被吊销、域名是否匹配等)。然后服务器发ServerKeyExchange——不是所有密码套件都需要,像DH/ECDH就需要传递公钥参数,而RSA密钥交换则不需要。最后发ServerHelloDone告诉客户端:“我这边能说的都说了,轮到你了”。
如果客户端也需要证书认证(双向TLS),客户端会回复自己的ClientCertificate。否则就跳过。我参与过金融支付系统的双向认证部署,那时踩了一个坑:客户端的证书私钥丢失了,导致握手一直卡在阶段2。后来我们在客户端加了一个健康检测,提前检查证书是否存在。
4.1.3 阶段3:客户端密钥计算与Finish
客户端收到服务器所有参数后,生成一个预主密钥(pre-master secret),然后用服务器公钥加密传过去(RSA场景)或者通过DH交换参数算出(DH场景)。预主密钥配合client_random和server_random,通过PRF(伪随机函数)扩展出主密钥(master secret),再派生出后续加密用的会话密钥。这个过程我建议你用OpenSSL的命令行验证一下:
# 用OpenSSL查看TLS 1.2握手记录的完整结构
openssl s_client -connect example.com:443 -tls1_2 -msg
# 输出中会清晰看到 ClientHello、ServerHello、Certificate 等每个消息
然后客户端发送ChangeCipherSpec——这个是一个“开关”,告诉服务器接下来我要用协商好的密钥加密了。紧接着发送Finished消息(已加密),包含之前所有握手消息的MAC摘要。服务器验证Finished,如果匹配,说明握手过程没有被篡改。
4.1.4 阶段4:服务器确认与完成
服务器同样计算自己的主密钥和派生密钥,发送ChangeCipherSpec + Finished。客户端验证服务器的Finished,没有问题的话,双方就可以愉快地发送加密的应用数据了。
核心概念对照:完整握手中,每一步都进行了完整性校验。你想想看,如果中间人修改了ServerKeyExchange,客户端计算的Master Secret就会跟服务器不同,随后客户端发送的Finished就会验证失败,从而终止连接。这就是TLS史上对抗中间人攻击的核心手段。
4.2 简化握手(Abbreviated Handshake / Session Resumption)
完整握手每次都要传证书、算密钥,开销挺大。尤其是移动端,切换基站或重新建连时如果都走完整握手,体验会很糟糕。简化握手的核心思想就是:复用之前协商好的会话状态,省掉证书交换和密钥协商的开销。
4.2.1 基于Session ID的简化握手
TLS 1.2最常见的简化方式是Session ID。服务器在第一次完整握手结束后,生成一个Session ID,并缓存对应的主密钥、密码套件等信息。客户端也保存这个ID。下次建连,客户端在ClientHello里带上Session ID,服务器查一下缓存,如果存在且没过期,就直接回复ServerHello + ChangeCipherSpec + Finished。客户端收到后也直接算密钥、回Finished。整个流程缩短到1 RTT,而且没有证书传输。
这种方式有个坑:Session ID是绑定到单个服务器的。如果线上用了负载均衡,第二次请求落到了另一台服务器,缓存里没有这个Session ID,那就只能降级到完整握手。我曾在CDN场景里吃过这个亏——明明配了Session ID复用,但流量一大,不同边缘节点之间没同步,握手效率直接回到解放前。后来我们改用了Session Ticket来解决。
4.2.2 基于Session Ticket的简化握手
Session Ticket是另一种机制,它把会话状态加密成一张Ticket,服务器不缓存任何东西。客户端在第一次完整握手结束时,从服务器拿到这张Ticket,第二次建连时直接塞进ClientHello的扩展里。服务器解密Ticket,恢复主密钥,然后走跟Session ID一样的快速路径。
# 使用OpenSSL测试Session Ticket复用
openssl s_client -connect example.com:443 -reconnect -no_ticket -msg
# 对比加上 -reconnect 选项后,观察第二次连接是否有Ticket被使用
Ticket机制的好处是不依赖服务器共享状态,适合分布式部署。不过需要注意:Ticket的加密密钥需要妥善保护。如果密钥泄漏,所有历史的会话都能被解密。所以建议定期轮换Ticket密钥,并用HSM或KMS管理。
我的经验:日常运维中,我建议在TLS 1.2下优先使用Session Ticket,因为它的可扩展性更好。如果你有严格的合规要求(比如金融行业需要审计所有会话),Session ID的共享缓存反而更容易记录和追踪。
4.3 完整握手 vs 简化握手:一个表格看明白
| 对比维度 | 完整握手 | 简化握手(Session ID) | 简化握手(Session Ticket) |
|---|---|---|---|
| RTT消耗 | 2 RTT (TLS 1.2) | 1 RTT | 1 RTT |
| 证书传输 | 需要 | 不需要 | 不需要 |
| 服务器状态 | 无状态(每个连接从头算) | 需要会话缓存 | 加密Ticket,无状态或轻状态 |
| 适用场景 | 首次建连、会话失效、安全要求极高的场合 | 单机部署或共享缓存集群 | 分布式、负载均衡、CDN、移动端 |
| 安全注意事项 | 证书链完整、密钥强度足够即可 | Session ID碰撞风险低,但缓存占用内存 | Ticket密钥需定期轮换,不能泄漏 |
⚠ 我曾经在线上碰到过一个典型问题:Session Ticket的有效期设置太长(比如72小时),导致客户端用旧的Ticket重连后,服务器虽然解密成功,但派生的密钥已经过期,客户端应用层报错。解决方法是:Ticket有效期设置为24小时以内,并且配合应用层的会话刷新机制(比如HTTP 401重新握手)。
4.4 握手协议的其他关键点
除了上面两种主要握手类型,还需要注意几个细节点:
- HelloRetryRequest(TLS 1.3新增):如果服务器不喜欢客户端推荐的加密参数,可以发这个消息让客户端重试。相当于“你换个姿势再来”。
- 密钥更新:长连接中,有时候需要在不中断握手的情况下重新派生密钥。TLS 1.3增加了
KeyUpdate消息,可用于定期更换会话密钥。 - 0-RTT(TLS 1.3):基于PSK(预共享密钥)或Session Ticket,允许客户端在第一个消息里就携带应用数据。安全性稍弱,适合对性能极致敏感的场景(比如HTTP/3)。
我个人习惯把完整握手叫“初次见面”,简化握手叫“老朋友重逢”。前者要验明正身,后者靠信任票(Session ID/Ticket)快速通行。理解了这两个模型的差异,你就能在最基础的层面判断一个TLS性能瓶颈是出在延迟还是出在计算上。
好了,握手协议的全景图和两个基本模式就讲到这里。下一趟我们会深入握手中最核心的密码学运算——密钥交换算法,看看ECDHE和RSA到底谁更稳、谁更快。不过那是后面的内容,先消化今天的知识吧。