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里那种按消息编号分段,而是从功能角度:

  1. 阶段一:Hello 阶段——双方打个照面,告诉对方自己能做什么、想做什么。
  2. 阶段二:证书与密钥交换阶段——服务器把身份证亮出来,同时开始交换密钥材料。
  3. 阶段三:密钥推导与验证阶段——两边各自算出一个对称密钥,然后验证对方确实也拿到了这个密钥。
  4. 阶段四:安全数据传输阶段——握手结束,握手记录变成加密记录,开始传真正的业务数据。

你想想看,这四个阶段是不是跟现实中两个人建立信任关系很像?先打招呼自我介绍,再交换凭证,然后各自验证凭证真伪,最后关起门来聊正事。

下面这张SVG图,把完整的握手流程画出来了。我把关键节点和消息流向都标在上面,理解了这个结构,后面所有细节你都能串联起来。

TLS 1.2 完整握手流程(四次握手) 客户端 (Client) 服务器 (Server) 阶段一 阶段二 阶段三 阶段四 ① ClientHello(支持的套件、随机数) ② ServerHello(选定套件、服务器随机数) ③ Certificate(服务器证书链) ④ ServerKeyExchange(DH参数/签名) ⑤ ClientKeyExchange(客户端DH参数) ⑥ ChangeCipherSpec ⑦ Finished(客户端验证消息) ⑧ ChangeCipherSpec + Finished 安全数据传输,应用数据加密

嗯,这张图里有个细节你可能注意到了: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 握手流程串一遍:

  1. 客户端发ClientHello(随机数 + 套件列表)
  2. 服务器回ServerHello(选定套件 + 服务器随机数),接着发证书,可能需要发服务器DH参数
  3. 客户端验证证书,发自己的密钥交换参数
  4. 双方各自推导主密钥
  5. 客户端发ChangeCipherSpec + Finished
  6. 服务器回复ChangeCipherSpec + Finished
  7. 握手结束,开始加密数据传输