TLS握手协议整体流程:C/S角色与握手阶段划分
说到TLS握手,很多初学者会觉得它是个黑盒子——双方先互相打招呼,然后掏出一堆证书,最后就能加密通信了。其实没那么玄乎。
本质上,握手就是两件事:确认身份,商定密钥。
我记得第一次抓包分析TLS握手时,看到那么多来回的消息,说实话有点懵。但当你把角色理清楚、阶段划分明白之后,这张图就清晰多了。
先记住一个总纲:TLS 1.2 握手,客户端先发起,服务器响应。双方通过数次交换,最后各自推导出相同的对称密钥。
4.1 角色分工:谁是发起方,谁是响应方
在网络通信里,C/S(客户端/服务器)角色其实很好区分:
- 客户端:主动发起握手的一方。通常是浏览器、App、或者你的程序里调用的网络库。
- 服务器:被动等待连接,响应客户端的请求。后端服务、负载均衡器、CDN节点都属于这端。
有人会问:那如果是双向认证的场景呢?双方都需要验对方的证书。但说白了,握手的第一条消息永远是客户端发出去的。服务器不会主动说「咱们开始握手吧」。这个角色定位从TCP的connect()开始就已经确定了。
| 角色 | 典型实例 | 核心职责 |
|---|---|---|
| 客户端(Client) | 浏览器、手机App、curl | 发起握手、发送ClientHello、验证服务器证书、生成最终密钥 |
| 服务器(Server) | Nginx、Tomcat、CDN节点 | 响应握手、选择密码套件、发送证书、完成密钥协商 |
你在写网络程序时,如果是用OpenSSL或者BoringSSL,Server端要加载证书私钥。这一点我在项目中遇到过——有人把客户端也配了证书和私钥,觉得反正也不影响通信。结果握手消息里多了个Certificate消息,对端如果没有配置验证请求,也没事。但如果你哪天开了双向认证,这个客户端证书就派上用场了。
4.2 握手阶段划分:四个核心步骤
TLS 1.2 完整的握手,我习惯把它拆成 四个阶段。不是RFC里那种按消息编号分段,而是从功能角度:
- 阶段一:Hello 阶段——双方打个照面,告诉对方自己能做什么、想做什么。
- 阶段二:证书与密钥交换阶段——服务器把身份证亮出来,同时开始交换密钥材料。
- 阶段三:密钥推导与验证阶段——两边各自算出一个对称密钥,然后验证对方确实也拿到了这个密钥。
- 阶段四:安全数据传输阶段——握手结束,握手记录变成加密记录,开始传真正的业务数据。
你想想看,这四个阶段是不是跟现实中两个人建立信任关系很像?先打招呼自我介绍,再交换凭证,然后各自验证凭证真伪,最后关起门来聊正事。
下面这张SVG图,把完整的握手流程画出来了。我把关键节点和消息流向都标在上面,理解了这个结构,后面所有细节你都能串联起来。
嗯,这张图里有个细节你可能注意到了:ServerKeyExchange 消息。不是所有握手都有这条消息的。如果密码套件用的是RSA密钥交换模式,服务器不需要发这条消息。只有DHE或者ECDHE这类需要临时DH参数的套件,才会带ServerKeyExchange。这一点我在项目中遇到过——有一次抓包发现握手老是中断,排查好久才发现服务器配置的密码套件强制要求发送ServerKeyExchange,但算法实现有bug,导致这条消息解析失败。
4.3 核心数据包详解
下面我们来看看刚才图中那些消息具体长什么样。
ClientHello
握手的第一声炮响。客户端把这个包发出去,告诉服务器:
- 我支持哪些TLS版本
- 我支持哪些密码套件列表
- 我生成了一个客户端随机数
- 我还支持哪些扩展功能(比如SNI、ALPN)
| 字段 | 作用 |
|---|---|
| 版本号 (Version) | 客户端能接受的最高TLS版本。TLS 1.2就是 0x0303 |
| 随机数 (Random) | 32字节,其中4字节是时间戳,28字节是随机数。用于后续密钥推导 |
| Session ID | 会话复用相关的标识,首次连接为空 |
| 密码套件列表 | 一个数组,按优先级排序。服务器会在里面挑一个 |
| 扩展 | SNI、ALPN、supported_groups、key_share 等,用来协商更高阶特性 |
ServerHello
服务器看完ClientHello后,选一个自己也能支持的密码套件,带上自己的服务器随机数,告诉客户端:
- 就按这个版本和套件来
- 这是我的随机数
注意,一旦ServerHello发出去,双方都知道了最终使用的密码套件。后面的证书交换、密钥交换都受这个套件的约束。
避坑指南: 我曾经排查过一个性能问题。客户发现每次握手都要好几秒,抓包后看到ServerHello和Certificate之间间隔了2秒。原来是服务器用HSM做签名,硬件签名太慢。这里建议:如果并发很高,优先用ECDSA证书而不是RSA证书,ECDSA签名效率更高。
4.4 密钥交换的实质
握手走到ClientKeyExchange之后,客户端和服务器就各自拥有了以下材料:
- 客户端随机数 (ClientHello里的)
- 服务器随机数 (ServerHello里的)
- 预主密钥 (Pre-Master Secret) —— 这是握手过程中真正要保护的核心秘密
预主密钥的生成方式取决于密码套件:
- 如果使用RSA模式,客户端生成一个48字节的预主密钥,用服务器公钥加密后发过去
- 如果使用DHE/ECDHE模式,预主密钥通过双方交换的DH参数,各自计算出来
然后两边用伪随机函数(PRF),把这三个材料揉在一起,推导出主密钥(Master Secret)。这个主密钥再进一步派生出加密密钥、MAC密钥、初始化向量(IV)等。
注意: 预主密钥绝不能泄露。一旦泄露,攻击者用同样的算法就能推导出所有通信密钥。我曾经审计过一个案例,某厂商的日志系统不小心把预主密钥明文打印到了日志里……嗯,后果很严重。
4.5 握手验证的妙用
最后一步Finished消息,很多人以为只是个收尾信号。其实它有个重要作用:验证握手没有被人篡改过。
具体做法是:双方把所有握手消息(从ClientHello到ClientKeyExchange)攒起来,算一个散列值,然后用主密钥加密后发过去。对方收到后自己也算一遍,如果对得上,说明握手过程没有被中间人动过手脚。
这也是为什么 TLS 1.3 更新后,握手机制变得更精简,但 Finished 消息依然保留着。它就是整个握手的「封条」。
我个人习惯把 Finished 消息叫做「握手完整性校验」。你在排查握手异常时,如果发现 Finished 验证失败,优先检查证书是否匹配、预主密钥计算是否有误。八成问题出在这儿。
4.6 阶段性小结
写到这,我把 TSL 1.2 握手流程串一遍:
- 客户端发ClientHello(随机数 + 套件列表)
- 服务器回ServerHello(选定套件 + 服务器随机数),接着发证书,可能需要发服务器DH参数
- 客户端验证证书,发自己的密钥交换参数
- 双方各自推导主密钥
- 客户端发ChangeCipherSpec + Finished
- 服务器回复ChangeCipherSpec + Finished
- 握手结束,开始加密数据传输