2. TLS 握手协议简介:四个子协议如何协同工作
说实话,很多搞安全的同学第一次接触 TLS 握手,容易被一堆术语绕晕。
什么 ClientHello、ServerHello、证书、密钥交换……其实你只要抓住一个核心就行:
TLS 握手不是单一协议,它是由四个子协议组成的“小团队”。
我个人习惯把这四个子协议比作一个工程项目:
有人负责设计方案(Handshake),有人负责发开工令(Change Cipher Spec),
有人负责处理意外(Alert),还有人负责搬运材料(Application Data)。
下面我挨个介绍它们的关系和分工。
2.1 四个子协议概览
| 子协议名称 | 内容类型(字节值) | 核心职责 | 一句话总结 |
|---|---|---|---|
| Handshake | 22 (0x16) | 协商参数、交换密钥、验证身份 | 谈判桌,所有核心参数都在这里定 |
| Change Cipher Spec | 20 (0x14) | 通知对端:接下来的数据将用新密钥保护 | 发令枪,一枪下去密码策略切换 |
| Alert | 21 (0x15) | 传递错误、关闭通知、警告 | 对讲机,出问题或要结束就喊一声 |
| Application Data | 23 (0x17) | 传输真正的业务数据(HTTP、FTP 等) | 集装箱,里面装的是实际应用负载 |
我在做国产化 TLS 协议栈时,曾遇到一个坑:有些设备对内容类型字节的校验特别严格,
一旦 Alert 消息的类型字节写错,对端直接静默断连。嗯,从那以后我改代码时都拿 Wireshark 逐字节对一次。
2.2 握手阶段整体流程
一次典型的 TLS 1.2 完整握手,流程大概是这样的:
- 客户端发 ClientHello —— 告诉服务器自己支持的 TLS 版本、密码套件列表、随机数等。
- 服务器回 ServerHello —— 选好版本和密码套件,发回自己的随机数。
- 服务器发 Certificate —— 把证书链(通常是 X.509)发给客户端。
- 服务器发 ServerKeyExchange(可选) —— 比如 DHE 或 ECDHE 的密钥交换参数。
- 服务器发 CertificateRequest(可选) —— 要求客户端提供证书(双向认证)。
- 服务器发 ServerHelloDone —— 表示服务器部分结束。
- 客户端回 Certificate(可选) —— 如果上一步有请求,客户端发自己证书。
- 客户端发 ClientKeyExchange —— 携带密钥交换材料(如 Pre-master Secret 的密文或 DH 公钥)。
- 客户端发 CertificateVerify(可选) —— 用自己的私钥签名之前所有握手消息,证明证书所有权。
- 双方各自计算主密钥(Master Secret) —— 这是内部操作,不发送。
- 客户端发 ChangeCipherSpec —— 表示接下来我要用新密钥加密了。
- 客户端发 Finished —— 一个带 MAC 的校验消息,用于验证握手完整性。
- 服务器也发 ChangeCipherSpec + Finished —— 同样切换并验证。
- 握手完成,开始传输 Application Data。
注意:Change Cipher Spec 严格来说不是 Handshake 子协议的一部分,
它是独立的内容类型。但在实现时,它往往紧跟在 Finished 之前发送,
很多新手误以为它属于 Handshake——我在 Code Review 时就发现过有人把它放到 Handshake 消息队列里处理,
结果导致对端解析失败。千万别混了。
2.3 四个子协议在握手过程中的关系
我画了一张图,把这四者的关系和时间顺序说清楚。
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 是货物,等一切就绪才运输。