TLS 握手协议基础(TLS 1.2)

聊到TLS改造,咱们得先把老本行吃透。TLS 1.2 虽然已经是「上一代」协议,但绝大多数生产环境还在跑它。我接手过的几个大项目,核心链路全是 1.2,升级到 1.3 得排到下个财年。所以,搞懂 1.2 的握手逻辑,是后续一切改造的基石。

握手的目标

说白了,TLS 握手就干三件事:

  • 身份认证 —— 确认你连的服务器是真的,不是中间人。
  • 密钥协商 —— 双方商量出一个只有你俩知道的对称密钥。
  • 参数确认 —— 选好加密套件、协议版本、压缩算法等。

你想想看,没有握手,后面的加密传输全是空中楼阁。我刚开始做安全开发时,总觉得握手过程太啰嗦,想自己省几个来回,结果被安全审计怼了回来——嗯,有些轮子真不能自己造。

一次完整的握手流程(TLS 1.2 全握手)

全握手是最常见的场景,尤其是客户端第一次访问服务器。下面这张图把整个流程画清楚了。

TLS 1.2 全握手流程 客户端 服务器 ① ClientHello ② ServerHello + 证书 + ServerHelloDone ③ ClientKeyExchange + ChangeCipherSpec + Finished ④ ChangeCipherSpec + Finished 加密应用数据 加密应用数据 ⑤ 加密数据双向传输 注意:全握手共需要 2 个 RTT 才能完成身份认证和密钥协商 步骤③和步骤④之间客户端已完成预主密钥传递,双方独立计算主密钥

小经验:如果你用 Wireshark 抓包,看到好几个 TLS 握手包来回跑,别慌。对照这张图一步步看,很快就能定位问题。我曾经在生产环境排查一次握手超时,就是靠抓包对比每一步的时间戳,发现服务器证书链太长导致传输变慢。

关键握手消息详解

① ClientHello

客户端发起的第一个消息,里面主要带:

  • 客户端支持的 TLS 版本列表(比如 {1.2, 1.1, 1.0})
  • 一个随机数 random_C(32 字节,其中 4 字节是时间戳)
  • 可用的加密套件列表,按优先级排序
  • 扩展字段(SNI、ALPN、supported_groups 等)

我见过有些客户端只带一个加密套件,结果服务器不支持,握手直接失败。建议最少放 5-8 个,优先级最高的放前面。

② ServerHello + 证书 + ServerHelloDone

服务器一口气回复三个消息(通常合并在同一个 TCP 段里):

消息内容作用
ServerHello选定版本、加密套件、服务器随机数 random_S双方版本/套件对齐
Certificate服务器证书链(叶子证书 + 中间 CA)客户端验证服务器身份
ServerHelloDone无载荷,仅表示「我发完了」通知客户端开始下一步

坑点:许多开发者以为 Certificate 消息只发叶子证书就行。错了!必须发送完整的证书链(到根证书之前),否则客户端可能无法验证。我曾经遇到一个客户,只发了叶子证书,结果 iOS 设备全都连接失败,因为 iOS 不缓存中间 CA。

③ ClientKeyExchange + ChangeCipherSpec + Finished

客户端收到服务器证书后,先验证证书有效性,然后:

  1. ClientKeyExchange —— 生成预主密钥(Pre-Master Secret),用服务器公钥加密后发送;如果是 ECDHE 密钥交换,则发送客户端的 ECDHE 公钥。
  2. ChangeCipherSpec —— 通知服务器:「接下来我发的消息要用商定好的密钥加密了」。
  3. Finished —— 加密发送整个握手消息的摘要(verify_data),用来做完整性校验。

注意,ChangeCipherSpec 本身不属于握手消息,它是一个独立的记录层消息。很多协议实现上容易混淆。

④ ChangeCipherSpec + Finished(服务器->客户端)

服务器收到客户端的预主密钥后,用它和双方随机数计算出主密钥(Master Secret),然后:

  • 切换加密状态
  • 发送 ChangeCipherSpec
  • 发送 Finished 消息(同样包含握手摘要验证)

客户端验证服务器的 Finished 通过后,握手完成。之后双方使用主密钥派生的会话密钥进行对称加密通信。

密钥派生流程

整个握手过程中最核心的计算就是密钥派生。我习惯把它画成一个三段流水线:

Pre-Master Secret (48 字节)
       ↓
PRF(Pre-Master Secret, "master secret", random_C + random_S)
       ↓
Master Secret (48 字节)
       ↓
PRF(Master Secret, "key expansion", random_C + random_S)
       ↓
六个密钥块:client_write_MAC_key, server_write_MAC_key,
             client_write_key, server_write_key,
             client_write_IV,  server_write_IV

PRF 伪随机函数在 TLS 1.2 里使用 HMAC-SHA256(默认),将来改造时可以换成更强的哈希。

关键点:主密钥由三部分输入决定——预主密钥、双方随机数。这意味着即使预主密钥泄露,只要随机数没有同时泄露,就无法还原主密钥。这是握手的核心安全设计之一。

缩写握手(Abbreviated Handshake)与会话恢复

如果客户端之前和服务器成功握手过,可以省略证书交换,直接使用之前协商的会话 ID 或会话票证(Session Ticket)恢复会话。流程简化为 1 个 RTT:

  1. ClientHello(带上 Session ID 或 Session Ticket)
  2. ServerHello + ChangeCipherSpec + Finished(如果允许恢复)
  3. 客户端验证后直接进入加密通信

我在做性能优化时发现,约 60% 的 TLS 连接都可以走缩写握手,尤其是移动端频繁断连重连的场景。如果你的服务端还没开启 Session Ticket 支持,赶紧加上——能省一次证书传输,延迟降低 30% 以上。

常见避坑

坑点原因解决方案
握手超时证书链太长或 DNS 解析慢控制证书链 ≤ 3 级,使用 OCSP Stapling
加密套件不匹配客户端和服务端支持列表无交集服务端至少开放 ECDHE-RSA-AES128-GCM-SHA256
Finished 校验失败中间代理篡改或双方时间偏差过大检查代理配置,校准 NTP
会话恢复无效Session Ticket 加密密钥轮换导致设置合理的 Ticket 密钥有效期(一般 24 小时)