2. TLS 握手协议简介:四个子协议如何协同工作

说实话,很多搞安全的同学第一次接触 TLS 握手,容易被一堆术语绕晕。
什么 ClientHello、ServerHello、证书、密钥交换……其实你只要抓住一个核心就行:
TLS 握手不是单一协议,它是由四个子协议组成的“小团队”。

我个人习惯把这四个子协议比作一个工程项目:
有人负责设计方案(Handshake),有人负责发开工令(Change Cipher Spec),
有人负责处理意外(Alert),还有人负责搬运材料(Application Data)。
下面我挨个介绍它们的关系和分工。

2.1 四个子协议概览

子协议名称内容类型(字节值)核心职责一句话总结
Handshake22 (0x16)协商参数、交换密钥、验证身份谈判桌,所有核心参数都在这里定
Change Cipher Spec20 (0x14)通知对端:接下来的数据将用新密钥保护发令枪,一枪下去密码策略切换
Alert21 (0x15)传递错误、关闭通知、警告对讲机,出问题或要结束就喊一声
Application Data23 (0x17)传输真正的业务数据(HTTP、FTP 等)集装箱,里面装的是实际应用负载

我在做国产化 TLS 协议栈时,曾遇到一个坑:有些设备对内容类型字节的校验特别严格,
一旦 Alert 消息的类型字节写错,对端直接静默断连。嗯,从那以后我改代码时都拿 Wireshark 逐字节对一次。

2.2 握手阶段整体流程

一次典型的 TLS 1.2 完整握手,流程大概是这样的:

  1. 客户端发 ClientHello —— 告诉服务器自己支持的 TLS 版本、密码套件列表、随机数等。
  2. 服务器回 ServerHello —— 选好版本和密码套件,发回自己的随机数。
  3. 服务器发 Certificate —— 把证书链(通常是 X.509)发给客户端。
  4. 服务器发 ServerKeyExchange(可选) —— 比如 DHE 或 ECDHE 的密钥交换参数。
  5. 服务器发 CertificateRequest(可选) —— 要求客户端提供证书(双向认证)。
  6. 服务器发 ServerHelloDone —— 表示服务器部分结束。
  7. 客户端回 Certificate(可选) —— 如果上一步有请求,客户端发自己证书。
  8. 客户端发 ClientKeyExchange —— 携带密钥交换材料(如 Pre-master Secret 的密文或 DH 公钥)。
  9. 客户端发 CertificateVerify(可选) —— 用自己的私钥签名之前所有握手消息,证明证书所有权。
  10. 双方各自计算主密钥(Master Secret) —— 这是内部操作,不发送。
  11. 客户端发 ChangeCipherSpec —— 表示接下来我要用新密钥加密了。
  12. 客户端发 Finished —— 一个带 MAC 的校验消息,用于验证握手完整性。
  13. 服务器也发 ChangeCipherSpec + Finished —— 同样切换并验证。
  14. 握手完成,开始传输 Application Data。

注意:Change Cipher Spec 严格来说不是 Handshake 子协议的一部分,
它是独立的内容类型。但在实现时,它往往紧跟在 Finished 之前发送,
很多新手误以为它属于 Handshake——我在 Code Review 时就发现过有人把它放到 Handshake 消息队列里处理,
结果导致对端解析失败。千万别混了。

2.3 四个子协议在握手过程中的关系

我画了一张图,把这四者的关系和时间顺序说清楚。

TLS 握手阶段四子协议关系图(TLS 1.2 完整握手) 客户端 Handshake (类型22) ChangeCipherSpec (20) ApplicationData (23) 服务器 Handshake (类型22) ChangeCipherSpec (20) ApplicationData (23) Alert (类型21) — 任意时刻 CCS (先发) 加密隧道 顺序:Handshake(多次交换)→ CCS → Finished(仍在Handshake内)→ 加密的ApplicationData

2.4 子协议之间的典型交互顺序

从上图能看出来,真正干活的主力是 Handshake 子协议。它要来回跑好几趟。我总结一下关键规律:

  • Handshake 最复杂:携带所有密钥协商的必要信息。一次握手可能包含 4~10 条 Handshake 消息。
  • Change Cipher Spec 只有一条消息:它不参与业务逻辑,只是一个“扳手”消息。我见过有些程序员把这个消息塞进 Handshake 消息队列,结果解析到 CCS 时状态机乱了——记得单独处理它。
  • Alert 可以在握手期间或握手之后任何时间发送:包括错误、关闭通知、致命警告。比如服务器发现客户端证书过期,就会在握手阶段发一个 fatal bad_certificate 的 Alert,然后直接断开连接。
  • Application Data 必须等双方 Finished 验证通过后才能发送:否则对端收到加密数据也解不出来(因为还没生成会话密钥)。

曾经有个线上事故:客户端在收到 ServerHello 之后、Finished 之前就开始发 Application Data,
结果服务端早就按照 RFC 的规定丢弃了这些“过早”的数据包,造成业务重试风暴。后来我在所有的 TLS 实现里都加了一个状态机校验 —— 不到 ESTABLISHED 状态,禁止发送加密业务数据。

2.5 子协议如何被封装在 TLS 记录层里

每个子协议的消息都会被放在 TLS 记录层(Record Layer)中传输。记录层是一个统一的外壳:

+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 内容类型(1字节) | 版本(2字节) | 长度(2字节) | 负载 |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

内容类型就是上面表格里的数字:22 表示 Handshake,20 表示 CCS,21 表示 Alert,23 表示 Application Data。
你想想看,抓包时只要看第一个字节,就能知道这条记录属于哪个子协议。

我调试 TLS 栈时,最喜欢的工具就是 Wireshark 的过滤器:tls.record.content_type == 22,只看握手消息。

2.6 握手阶段的“变种”场景

上面讲的是完整的双向认证握手。实际场景中还有几种常见变体:

  • 会话恢复(Session Resumption):用之前协商好的 Session ID 或 Session Ticket,跳过证书交换和密钥协商,直接进入加密阶段。Handshake 消息会少很多。
  • PSK(Pre-Shared Key):不涉及证书,双方用预先共享的密钥直接做身份验证和密钥派生。
  • TLS 1.3 的简化握手:只用了 1-RTT 甚至 0-RTT,把 Change Cipher Spec 去掉了(因为密钥派生的时机变了)。不过本课程主要针对 TLS 1.2,1.3 我们后面章节会单独聊。

我在做金融支付网关时,发现不少业务为了提升性能,强制用 Session Ticket 复用握手。
但要注意:Session Ticket 的密钥要定期轮换,否则一旦泄露,历史会话都能被解密。
嗯,很多人只顾快,忘了安全——这个坑我踩过一次。

2.7 小结:记住这四者的关系

  • Handshake 是大脑,决定一切参数。
  • Change Cipher Spec 是开关,只拨一次。
  • Alert 是哨兵,随时报告异常。
  • Application Data 是货物,等一切就绪才运输。