TLS握手过程详解:ClientHello、ServerHello、证书交换、密钥协商
TLS握手,说白了就是客户端和服务器在正式传输数据之前,先互相打个招呼、亮明身份、商量好怎么加密。很多连接超时的问题,就卡在这一步。我排查过不少案例,发现大部分工程师对握手的理解停留在“好像有四个阶段”,但真正遇到丢包、超时、证书错误时,往往不知道从哪下手。
这一章咱们把四个核心阶段掰开揉碎讲清楚。我会把每个阶段在网络上发的是什么、每一帧长什么样、哪些地方容易出问题,都给你说明白。你想想看,搞懂了握手,你再遇到“连接超时”的告警,脑子里就能立刻浮现出是哪个阶段挂了。
TLS握手的四个核心阶段:
- ClientHello – 客户端发起问候
- ServerHello + 证书交换 – 服务器回应并出示身份
- 密钥协商 – 双方交换参数,生成会话密钥
- Finished – 握手完成确认
阶段一:ClientHello,客户端先亮牌
握手的第一步,客户端主动发一个 ClientHello 消息。别小看这几十个字节,里面装的信息直接决定了后面怎么谈。我个人习惯在抓包时先看这个报文,因为它把客户端的能力全交代了。
- 协议版本:比如 TLS 1.2、TLS 1.3,客户端声明的最高支持版本
- 随机数(Client Random):32字节,前4字节是时间戳,后面是随机数据。这是后续密钥派生的重要种子
- 密码套件列表(Cipher Suites):客户端支持的加密算法组合,比如 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- 扩展字段:SNI(服务器名称指示)、ALPN(应用层协议协商)、支持的椭圆曲线等
避坑指南: 我曾经遇到一个超时故障,客户端发完 ClientHello 后服务器迟迟不回复。抓包发现客户端的 SNI 字段里写的域名服务器不认识,导致 ServerHello 里返回了一个 alert。你想想看,如果 SNI 写错了,证书验证根本过不去。
阶段二:ServerHello + 证书交换
服务器收到 ClientHello 后,会挑选一个双方都支持的密码套件,然后回复 ServerHello。嗯,这里要注意:服务器发的随机数(Server Random)和客户端随机数一样重要,后面生成预主密钥全靠它们。
紧接着服务器会发送自己的证书。证书是用 X.509 格式编码的,里面包含公钥、签名算法、颁发机构、有效期等信息。我见过有人配置错了证书链,服务器只发了叶子证书,没发中间 CA 证书,结果客户端验证链断了,直接握手失败。
| 证书字段 | 作用 | 常见问题 |
|---|---|---|
| Subject | 证书持有者(域名或组织) | 域名不匹配,导致浏览器警告 |
| Issuer | 签发机构(CA) | 缺少中间 CA,客户端无法递归验证 |
| Validity | 有效期 | 证书过期,或服务器时间不同步 |
| Public Key | 用于密钥交换或数字签名 | 密钥长度过短(如1024位) |
| Signature | CA 对证书的签名 | 签名算法不支持(如 SHA-1 已被弃用) |
注意: 在 TLS 1.3 中,ServerHello 之后不需要单独发 Certificate 消息的情况只出现在 PSK 模式下。绝大多数生产环境还是走标准证书交换。我之前调过一个金融客户的问题,他们服务器配了自签名证书,测试环境能通,上生产后客户端验证不过——因为生产环境强制要求证书链可追溯到受信任的根 CA。
阶段三:证书验证(客户端视角)
客户端收到服务器证书后,要做三件事:验证签名、检查有效期、确认证书链。说白了就是拆开证书看看是不是真的。
- 验证签名:用 CA 的公钥解密签名,跟证书摘要对比
- 检查有效期:当前时间必须在 notBefore 和 notAfter 之间
- 检查证书链:从叶子证书逐级向上,直到找到系统信任的根证书
- 检查吊销状态:通过 CRL 或 OCSP 确认证书没有被吊销
这一步如果超时,通常是因为 OCSP 服务器响应慢,或者 CRL 下载过大。我帮一个电商平台排查过,他们镜像跑在海外,每次握手都要去欧洲的 OCSP 服务器查询,网络延迟直接导致握手耗时超过 5 秒,于是前端报了连接超时。
实战技巧: 可以在服务器端启用 OCSP Stapling,让服务器自己带着 OCSP 响应发给客户端,省去客户端主动查询的 RTT。TLS 1.3 对此做了标准化支持,非常值得开启。
阶段四:密钥协商
证书验证通过后,双方进入密钥协商阶段。TLS 1.2 有两种主流方式:RSA 密钥交换和 Diffie-Hellman(DH)密钥交换。我个人更推荐 DH 类,因为 RSA 密钥交换不具备前向安全性——一旦私钥泄漏,历史会话全被破解。
协商过程大致是这样:
- 客户端生成一个预主密钥(Pre-Master Secret),如果是 RSA 方式就用服务器公钥加密发过去;如果是 ECDHE 方式,客户端会发送自己的 DH 公钥参数
- 服务器收到后用自己的私钥解密(RSA)或用 DH 参数计算共享密钥
- 双方用 客户端随机数 + 服务器随机数 + 预主密钥 通过 PRF(伪随机函数)派生出主密钥(Master Secret)
- 主密钥再被切分为多个会话密钥:写密钥、初始化向量、MAC 密钥等
我曾犯过的错: 早期调一个 ECDHE 的握手问题,客户端发完 ClientKeyExchange 后服务器就断开连接了。抓包对比才发现,客户端用的椭圆曲线是 secp384r1,但服务器只支持 secp256r1 —— 套件名字看起来都是 ECDHE,但曲线没对齐。所以你看,不仅要看密码套件,还得看支持的曲线列表。
最后的确认:ChangeCipherSpec 与 Finished
密钥协商完成后,双方各自发送 ChangeCipherSpec 消息,告诉对方“我从下一条消息开始要用协商好的加密”。紧接着发送 Finished 消息,这个消息是用会话密钥加密并携带所有握手消息的 MAC 校验值。如果双方都能正确解密和验证,握手就正式完成。
如果在这个阶段超时,大概率是密钥派生过程出了问题——比如客户端算出来的密钥跟服务器不一致。我遇到过一种情况:服务器时钟偏差过大,生成的随机数中含有错误的时间戳,导致 PRF 输入不同。虽然 RFC 说时间戳不一定需要精确,但某些实现会严格校验。
# 抓包观察示例(tcpdump + wireshark 过滤)
# 只看 TLS 握手数据包
tcpdump -i eth0 -w handshake.pcap port 443
# 在 Wireshark 中使用过滤器
tls.handshake.type == 1 # ClientHello
tls.handshake.type == 2 # ServerHello
tls.handshake.type == 11 # Certificate
tls.handshake.type == 16 # ClientKeyExchange
tls.handshake.type == 20 # Finished
以上就是 TLS 握手的四个核心阶段。搞清楚了每个阶段发什么、做什么、可能错在哪,你再排查超时问题的时候,心里就有底了。说白了,超时就对应着某一个阶段的交互卡住了——要么是 ClientHello 丢包,要么是证书过大导致分片超时,要么是密钥协商失败中断。后面我们结合实际场景,一个个拆解。