4. TLS握手协议整体流程:客户端Hello到Finished
TLS握手是整个安全通信的起点。说白了,就是客户端和服务端在正式加密传输数据之前,先互相认识一下,商量好用什么密码套件,再交换几个密钥参数,最后确认大家都准备好了。整个过程就像两个人在开黑前先语音对一下“你进的哪个房间?密码是啥?装备带齐了吗?”——不过TLS的流程更严谨、更安全。
我在好几个项目中排查过握手失败的问题,最常见的原因就是密码套件不匹配或者证书链有问题。所以理解握手流程,对于排查网络问题和优化性能都特别关键。下面我们一步步拆解。
4.1 握手概览:消息交换的时间线
一个完整的TLS 1.2握手(不考虑会话复用)通常需要交换以下消息(从客户端到服务端再到客户端):
- 1. ClientHello —— 客户端发起,携带支持的版本、密码套件列表、随机数等
- 2. ServerHello —— 服务端回应,选择版本、密码套件、随机数
- 3. Certificate —— 服务端发送自己的证书链(可选客户端证书,但极少见)
- 4. ServerKeyExchange —— 根据密码套件,可能需要发送额外的密钥参数(例如DH公钥)
- 5. CertificateRequest —— 如果服务端要求客户端证书,会发这个(通常不用)
- 6. ServerHelloDone —— 服务端说:“我的消息发完了,等你的回应”
- 7. ClientCertificate —— 如果服务端要求且客户端有证书,就发(可选)
- 8. ClientKeyExchange —— 客户端发送密钥交换信息(如预主密钥的密文或DH公钥)
- 9. CertificateVerify —— 如果发了客户端证书,需要签名验证
- 10. ChangeCipherSpec —— 客户端通知服务端:我要切换成加密通信了
- 11. Finished —— 客户端发送第一个加密消息,包含之前所有握手消息的校验
- 12. ChangeCipherSpec —— 服务端同样切换加密
- 13. Finished —— 服务端发送自己的Finished消息,握手完成
你想想看,上面列了13步,但实际中有些步骤(如CertificateRequest、ClientCertificate)经常被省略。真正核心的流程其实可以浓缩为“3个回合”的来回。我习惯把第1~6步看作“服务端配置下发”,第7~11步“客户端密钥提交”,最后12~13步“双方确认握手”。
4.2 第一阶段:客户端Hello(ClientHello)
所有握手的起点。客户端一上来就扔出一个ClientHello消息,里面装了一大堆自己支持的选项。我当年第一次抓包看TLS握手时,还以为是什么黑魔法,其实结构特别简单:
- 版本号 —— 客户端能支持的最高TLS版本(比如TLS 1.2)
- 随机数(32字节) —— 客户端生成的随机值,用于后面计算主密钥
- 会话ID —— 如果客户端之前有过连接,想复用会话就带上之前的ID
- 密码套件列表 —— 按优先级排列的一堆套件,服务端从中选一个
- 扩展字段 —— 比如SNI(服务器名称指示)、ALPN(应用层协议协商)等
4.3 第二阶段:服务端回复(ServerHello 到 ServerHelloDone)
服务端收到ClientHello后,会从客户端列出的密码套件中挑选一个最适合的(根据服务端自己的配置)。然后发回ServerHello,包含:
- 选中的版本号(通常就是客户端建议的最高版本,除非服务端只支持更低的)
- 服务端自己的随机数(也是32字节)
- 选中的密码套件(例如
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256) - 还有服务端生成的会话ID(如果支持会话复用)
随后,服务端会发送自己的证书(Certificate消息),里面一般包含一个由CA签发的X.509证书链。我建议你检查一下证书链是否完整——很多握手失败是因为服务端只发了叶子证书,没有发中间CA证书,导致客户端无法验证。
如果密码套件使用的是基于DH或ECDH的密钥交换,服务端还需要发送ServerKeyExchange消息,里面包含DH参数(p、g、公钥)或者椭圆曲线参数。最后发送一个ServerHelloDone表示“我这边说完了”。
4.4 第三阶段:客户端密钥交换(ClientKeyExchange 到 Finished)
客户端收到服务端的所有信息后,会做两件事:
- 验证证书 —— 检查证书是否有效、是否被吊销、是否与域名匹配
- 生成预主密钥 —— 根据密码套件的密钥交换算法生成一个秘密值
如果是RSA密钥交换,客户端会用服务端的公钥加密一个48字节的预主密钥,然后通过ClientKeyExchange发过去。如果是ECDHE,客户端会生成自己的临时ECDH私钥/公钥对,把公钥发过去,然后双方用ECDH算法算出共享的预主密钥。
嗯,这里要注意:预主密钥 + 两个随机数 = 主密钥(通过PRF伪随机函数)。主密钥再进一步派生出会话密钥(加密密钥、MAC密钥、IV等)。这个过程我在调试时经常需要抓包出来,通过工具解密看中间值——不过Wireshark只支持有限的解密场景,生产环境不建议这么做。
之后客户端发送ChangeCipherSpec(这是一个单独的消息,不是握手消息,属于记录层协议),表示后续的消息都要用刚刚协商好的密钥加密了。紧接着发送Finished消息,这是一个经过加密和MAC保护的消息,里面包含之前所有握手消息的哈希值。服务端收到后,会验证这个Finished,如果匹配,说明握手没有被篡改。
最后服务端也发送自己的ChangeCipherSpec和Finished。客户端验证通过后,握手正式结束,双方开始用对称密钥加密应用数据。
4.5 各阶段关键消息速查表
| 阶段 | 发送方 | 消息类型 | 主要内容 |
|---|---|---|---|
| 第一阶段 | 客户端 | ClientHello | 版本、密码套件列表、随机数、扩展 |
| 第二阶段 | 服务端 | ServerHello | 选中的版本、套件、随机数 |
| 第二阶段 | 服务端 | Certificate | X.509证书链 |
| 第二阶段 | 服务端 | ServerKeyExchange | DH/ECDH参数(如果需要) |
| 第二阶段 | 服务端 | ServerHelloDone | 结束标记 |
| 第三阶段 | 客户端 | ClientKeyExchange | 预主密钥(RSA加密)或DH公钥 |
| 第三阶段 | 客户端 | ChangeCipherSpec | 切换加密 |
| 第三阶段 | 客户端 | Finished | 握手消息的MAC校验 |
| 第三阶段 | 服务端 | ChangeCipherSpec | 切换加密 |
| 第三阶段 | 服务端 | Finished | 握手消息的MAC校验 |
4.6 避坑:握手失败的常见原因
- 证书链不完整: 服务器只发了叶子证书,中间CA缺失。客户端无法构建信任链,直接断开连接。修复方法:把中间CA证书也配置到服务器上。
- 密码套件不匹配: 客户端和服务端支持的套件列表没有交集。特别是当客户端只支持TLS 1.2但服务端只开启TLS 1.3时,版本协商也可能失败。
- SNI缺失: 现在很多服务器托管多个域名,如果客户端没有发SNI扩展,服务端可能找不到对应的证书,返回默认证书或直接拒绝。
- 时间问题: 客户端或服务端系统时间不准确,会导致证书有效期验证失败。我遇到过一台设备时间拔了两年,所有HTTPS访问都报证书过期——折腾了半天才找到原因。
4.7 关于握手性能的一点建议
全握手需要2-RTT(两次往返)才能完成,如果算上TCP的三次握手,总共需要3-RTT才能开始发送应用数据。对于高延迟的网络(比如跨国连接),这个开销很可观。我曾经优化过一个海外业务的项目,就是把握手延迟从200ms降到了80ms——方法是启用了TLS会话复用(session ticket)和False Start。
会话复用允许客户端在后续连接中直接发送加密数据,不需要重新握手。False Start则是让客户端在收到服务端Finished之前就提前发送应用数据,减少一次RTT。不过这些高级特性对协议实现有要求,不是所有浏览器和服务器都完美支持。我的建议是:先确保基本握手流程正确,再考虑优化。