第4章:TLS握手流程:双向握手、会话恢复、密钥交换

说实话,我觉得TLS握手是整个协议里最值得花时间琢磨的一块。很多开发者把TLS当成“黑盒子”,配置完证书就完事了。但在做合规审计时,握手过程的每个细节都可能成为安全隐患的突破口。我亲手调过不下百次握手失败的案例,几乎每次都有新发现。

这一章我们就来拆解TLS握手的完整流程。说白了,就是从客户端说“你好”开始,到双方安全地交换数据为止。

4.1 握手到底在干什么?

先问一个问题:为什么需要握手?你想想看,两个人在公共场合第一次见面,想聊点私密话题。你们得先确认身份,约定一个只有你俩知道的暗号,然后才能放心交谈。TLS握手就是这个“确认身份”和“约定暗号”的过程。

具体要干三件事:

  • 身份验证 – 证明对方是它声称的那个人(通常由服务器出示证书)
  • 协商参数 – 决定用哪个TLS版本、哪套加密套件
  • 生成密钥 – 双方各自算出相同的会话密钥,后续通信全程加密
审计小贴士: 我建议你在审计时重点关注协商出来的TLS版本和加密套件。如果最终降级到了TLS 1.0,那握手过程一定有问题。

4.2 完整握手流程(一次握手)

先看最常见的“一次完整握手”。这里拿TLS 1.2举例,因为它最典型。TLS 1.3虽然简化了,但理解1.2能帮你打好基础。

我把这个过程分成四个阶段讲:

阶段一:Hello 交互

  1. ClientHello – 客户端发问候,带着支持的TLS版本列表、加密套件列表、一个随机数(Client Random)
  2. ServerHello – 服务端从中选定一个版本和一个套件,返回自己的随机数(Server Random)
  3. Certificate – 服务端把自己的数字证书链发过去(通常是X.509格式)
  4. ServerHelloDone – 服务端说“我这边发完了,轮到你了”
踩坑记录: 我曾经遇到一个案例,服务端发来的证书链不完整,缺了中间证书。客户端因为无法验证到根证书,直接拒绝了连接。很多运维同学只配了服务器证书,忘了配中间证书链。

阶段二:密钥协商

  1. ClientKeyExchange – 客户端生成一个“预主密钥”,用服务端的公钥加密后发过去
  2. 双方各自用相同的算法,结合Client Random、Server Random和预主密钥,计算出完全相同的主密钥
  3. 再从主密钥派生出后续用于对称加密的会话密钥、MAC密钥和IV

这一步是安全的核心。预主密钥只有服务端的私钥能解开。即使有人截获了加密后的数据包,没有私钥也白搭。

阶段三:变更密码规范

  1. ChangeCipherSpec – 客户端通知服务端:“接下来我要用刚才协商好的密钥加密了”
  2. Finished – 客户端用新密钥加密一条验证消息(包含之前所有握手消息的哈希值)
  3. 服务端做同样的事,解密并验证。如果对不上,握手立即失败

阶段四:握手完成

双方各自发送一次ChangeCipherSpec和Finished。全部校验通过后,握手正式结束。后面的HTTP数据全部用会话密钥加密传输。

下面这张图把这个过程完整串起来了:

TLS 1.2 完整握手流程(一次握手) 客户端 生成 Client Random 发起 ClientHello 验证服务器证书 提取公钥 生成预主密钥 用公钥加密并发送 ClientKeyExchange 发送 ChangeCipherSpec 发送 Finished 应用数据加密传输 服务器 选择TLS版本与套件 发送 ServerHello 发送服务器证书 发送 ServerHelloDone 用私钥解密预主密钥 双方算出相同主密钥 发送 ChangeCipherSpec 发送 Finished 应用数据加密传输 ClientHello 验证证书 ClientKeyExchange
(加密的预主密钥) ChangeCipherSpec + Finished ServerHello Certificate + ServerHelloDone ChangeCipherSpec + Finished

4.3 双向握手(双向认证)

上面提到的是“单向认证”——只有服务器出示证书。那“双向握手”呢?说白了就是客户端也得把自己的证书亮出来,服务器也去验证一遍。这在企业内部应用或金融场景中很常见。

流程上多了两步:

  • 服务端在ServerHello之后,会额外发一个CertificateRequest消息,要求客户端提供证书
  • 客户端收到后,发送自己的Certificate(以及对应的CertificateVerify消息,用私钥签名证明自己持有私钥)
为什么做双向认证? 我接触过一个银行的内部API网关,所有调用方必须是注册过的内部应用。单向认证只保证了“服务器是银行”,但服务器不知道“调用方是谁”。双向认证解决的就是这个问题——双方互相检查身份。

不过双向握手也有麻烦的地方。证书管理成本翻倍——你可能要管理几百个客户端的证书。而且每次客户端证书过期,就得重新走一遍申请流程。我在项目里见过因为客户端证书没及时更新,导致整个生产环境全部断连的事故。

4.4 会话恢复

每次新连接都来一次完整握手?性能上扛不住。尤其是移动端场景,网络差、延迟高,完整握手多出来的几个RTT(往返时延)体验极差。

会话恢复就是为了解决这个问题的。简单说就是“复用之前的握手结果”。

TLS提供了两种主流方式:

方式 原理 优点 缺点
Session ID 服务端保存会话ID和密钥,客户端连接时带上ID直接复用 实现简单,兼容性好 服务端要存状态,负载均衡场景下容易失效(请求落到不同节点就丢了)
Session Ticket 服务端将会话状态加密成Ticket发给客户端,客户端下次连接时带回来解密复用 服务端不需要存状态,水平扩展友好 需要服务端维护Ticket密钥,密钥泄露风险值得注意

我个人偏向用Session Ticket。那次做完一个百万并发接口的审计,发现服务端Session表里堆了几十万条记录,内存暴涨。后来切到Ticket方案后,服务端完全无状态,内存瞬间降下来了。但要注意——Ticket的加密密钥要定期更换。

避坑提醒: 我曾经遇到一个客户,他家的TLS实现用Session Ticket时,密钥一直没换过。跑了两年,所有用户的会话都能用同一把密钥解密。一旦密钥泄露,所有历史会话都被破了。记住:密钥轮换不是可选项,是必选项。

4.5 密钥交换:不只是RSA

早期的TLS主要用RSA做密钥交换。但RSA有个硬伤——它不提供前向安全性。什么意思?服务端的私钥如果泄露了,攻击者可以用私钥解密之前所有被截获的预主密钥,进而还原出所有历史会话的密钥。这很要命。

现在的TLS 1.2和1.3推荐用(EC)DHE——这就是所谓的“短暂密钥交换”。

基本原理:

  1. 客户端和服务端各自生成一对临时的DH密钥对
  2. 交换各自的DH公钥
  3. 双方独立计算出一个共享密钥(使用私钥和对方的公钥)
  4. 即使攻击者拿到长期私钥,也只能验证身份,无法还原出之前的临时DH私钥

简单说,RSA像在公共邮箱里放信,邮箱钥匙丢了,所有信都能被看。DHE像每次通信都换一个新邮箱,邮箱钥匙丢了,也只丢你丢钥匙那一刻的那一封信。

审计建议: 在TLS配置审计里,务必关闭所有基于RSA密钥交换的加密套件。应该只保留ECDHE——不仅支持前向安全,性能也比DHE好。我在一个金融项目中,把RSA套件强制关掉后,客户还有点担心兼容性。实测下来99%的客户端都支持,剩下的1%是设备太旧,该升级了。

4.6 审计检查清单

做完这一章,你应该清楚握手过程的每一个关键点了。我建议你把这些内容整理成一份检查清单:

检查项 审计标准 常见违规
TLS版本 ≥ TLS 1.2(推荐1.3) 服务端降级到TLS 1.0/1.1
证书链完整性 必须包含中间证书 缺中间证书,手机端、老旧浏览器无法验证
密钥交换算法 仅允许 ECDHE 启用 RSA 或 DHE 套件
会话恢复方案 Session Ticket + 密钥轮换 未启用恢复,或Ticket密钥长期不变
Finished校验 握手消息MAC校验必须开启 忽略校验,可能导致降级攻击

下回去审计一个TLS部署,你可以直接拿这张表去对照。少一项,就算不过。