TLS 握手协议基础(TLS 1.2)
聊到TLS改造,咱们得先把老本行吃透。TLS 1.2 虽然已经是「上一代」协议,但绝大多数生产环境还在跑它。我接手过的几个大项目,核心链路全是 1.2,升级到 1.3 得排到下个财年。所以,搞懂 1.2 的握手逻辑,是后续一切改造的基石。
握手的目标
说白了,TLS 握手就干三件事:
- 身份认证 —— 确认你连的服务器是真的,不是中间人。
- 密钥协商 —— 双方商量出一个只有你俩知道的对称密钥。
- 参数确认 —— 选好加密套件、协议版本、压缩算法等。
你想想看,没有握手,后面的加密传输全是空中楼阁。我刚开始做安全开发时,总觉得握手过程太啰嗦,想自己省几个来回,结果被安全审计怼了回来——嗯,有些轮子真不能自己造。
一次完整的握手流程(TLS 1.2 全握手)
全握手是最常见的场景,尤其是客户端第一次访问服务器。下面这张图把整个流程画清楚了。
小经验:如果你用 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
客户端收到服务器证书后,先验证证书有效性,然后:
- ClientKeyExchange —— 生成预主密钥(Pre-Master Secret),用服务器公钥加密后发送;如果是 ECDHE 密钥交换,则发送客户端的 ECDHE 公钥。
- ChangeCipherSpec —— 通知服务器:「接下来我发的消息要用商定好的密钥加密了」。
- 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:
- ClientHello(带上 Session ID 或 Session Ticket)
- ServerHello + ChangeCipherSpec + Finished(如果允许恢复)
- 客户端验证后直接进入加密通信
我在做性能优化时发现,约 60% 的 TLS 连接都可以走缩写握手,尤其是移动端频繁断连重连的场景。如果你的服务端还没开启 Session Ticket 支持,赶紧加上——能省一次证书传输,延迟降低 30% 以上。
常见避坑
| 坑点 | 原因 | 解决方案 |
|---|---|---|
| 握手超时 | 证书链太长或 DNS 解析慢 | 控制证书链 ≤ 3 级,使用 OCSP Stapling |
| 加密套件不匹配 | 客户端和服务端支持列表无交集 | 服务端至少开放 ECDHE-RSA-AES128-GCM-SHA256 |
| Finished 校验失败 | 中间代理篡改或双方时间偏差过大 | 检查代理配置,校准 NTP |
| 会话恢复无效 | Session Ticket 加密密钥轮换导致 | 设置合理的 Ticket 密钥有效期(一般 24 小时) |