🌈个人主页:一条泥憨鱼(欢迎各位大佬莅临)

🎬精选专栏传送门:

❄️《数据结构》   ❄️《AI与Agent那些事》

                                         ❄️《从0开始学计算机网络》 ❄️《后端开发》

前言:

"我用 Wireshark 抓 HTTPS 的包,怎么还能看到 ClientHello、ServerHello 这些东西?不是加密了吗?"

这个问题问得挺典型的。很多人对 HTTPS 的理解停留在"加密了所以看不到内容",但抓包工具恰恰能清清楚楚地看到 TLS 握手的每一个步骤——因为握手阶段本身就是明文设计的。那问题就来了:既然要加密,为什么握手过程不一起加密?这个握手到底在握什么?

今天就把 TLS 1.3 的四次握手从头到尾拆一遍。

先搞清楚:TLS 握手要解决哪三个问题

很多人容易把 TLS 握手和 TCP 的三次握手搞混。TCP 三次握手是建立连接——确认"你能听到我,我也能听到你"。TLS 握手是在这条已经建立的信道上,再解决三个更上层的问题:

第一,你是谁?

浏览器地址栏的小锁头,本质上就是 TLS 握手中服务器向客户端证明"我是这个域名的合法主人"。这个证明靠的是数字证书,而证书的核心是一对公私钥。服务器把证书甩给客户端,客户端验证证书的签名链,确认这个证书确实是某个可信 CA 签发的。

第二,咱们用什么密钥说话?

就算确认了身份,也不能直接用服务器的公钥加密所有数据——公钥加密(非对称加密)太慢了,一个 2048 位的 RSA 运算比对称加密慢好几个数量级。所以 TLS 握手的核心目标,是让双方协商出一个只有彼此知道的对称密钥,后续所有数据都用这个密钥加密传输。

第三,咱们怎么聊?

双方得确认用什么加密套件、什么 TLS 版本、支持哪种签名算法。好比两个人打电话,得先确认说中文还是英文,不然聊到一半鸡同鸭讲。

这三个问题,就是 TLS 握手要解决的。理解了这个,后面看四次握手的具体步骤就顺了。

四次握手,一次一次拆开看

TLS 1.3 的握手只有两个往返(Round-Trip Time),四个消息。逐个拆。

第一步:ClientHello

客户端先开口。这个包里带了什么?客户端支持的 TLS 版本列表、支持的密码套件列表、一个随机数(client_random),以及——这是 1.3 的新特性——客户端猜的一组密钥交换参数(key_share)。

为什么要带 key_share?这是 1.3 为了省一次 RTT 做的优化,后面细说。总之,客户端在这个包里把自己的"能力清单"和"一个随机数"都亮出来了。

第二步:ServerHello

服务器收到 ClientHello 后,从客户端的列表里挑一个双方都支持的版本和密码套件,然后回一个 ServerHello,包含:选定的版本和套件、服务器自己的随机数(server_random)、以及服务器选的密钥交换参数。

到这里,双方已经各自持有 client_random、server_random 和对方的密钥交换参数。理论上,双方已经可以各自独立算出同一个对称密钥了。

但先别急,服务器还要做一件事——证明自己确实是证书的主人。所以 ServerHello 之后,服务器还会发 Certificate 消息(带着证书)和 CertificateVerify 消息(用私钥对握手上下文签名)。这两个消息在 1.3 里合并到了第二个 flight 里,但逻辑上是独立的。

第三步:客户端密钥交换

客户端收到服务器的证书后,验证证书链、检查域名匹配、确认证书没过期。然后用自己的密钥交换参数和服务器的参数算出对称密钥。

这个"算"的过程,在 1.3 里用的就是 ECDHE(椭圆曲线 Diffie-Hellman 密钥交换)。客户端拿到服务器的公钥参数,结合自己的私钥参数,算出一个共享密钥——这个密钥只有客户端和服务器能算出来,中间人就算截获了所有报文也算不出来,因为私钥参数从没在网络上传输过。

第四步:完成消息

客户端算好密钥后,发一个 Finished 消息——用协商好的密钥加密一段握手上下文的哈希值。服务器收到后,用自己算出的密钥解密并验证,确认"客户端确实算出了和我一样的密钥"。然后服务器也回一个 Finished 消息。

到这里,双方确认密钥一致,握手完成,后面传输的所有 HTTP 数据都用这个密钥加密。



 

一个容易忽略的细节:TLS 1.3 其实只剩两次往返

"四次握手"这个名字容易让人误会,以为要四个 RTT。实际上,TLS 1.3 的握手只需要两个 RTT——一次 ClientHello 出去,一次 ServerHello 回来,再一个来回确认。

那"四次"指的是四条消息,不是四个往返。

对比一下 TLS 1.2 就清楚了。1.2 的握手是这样的:

1. ClientHello

2. ServerHello + Certificate + ServerKeyExchange + ServerHelloDone

3. ClientKeyExchange + ChangeCipherSpec + Finished

4. ChangeCipherSpec + Finished

看起来也是四条,但 1.2 的密钥交换参数是在 ServerKeyExchange 里才传的,客户端收到之后才能算出密钥,所以第 3 步和第 4 步之间必须等一个完整 RTT。算下来是两个 RTT 起步,加上某些场景还要额外的往返。

1.3 的核心优化是把密钥交换参数提前到了 ClientHello 里——客户端反正要猜服务器支持什么曲线,那就把自己支持的曲线参数都放进去,服务器直接选一个用就行。这样服务器在第一个 RTT 的回复里就能把密钥材料给齐,双方同时算出密钥,省掉了整整一个 RTT。

另外 1.3 还砍掉了 1.2 里那些过时的加密套件和算法——RSA 密钥交换、CBC 模式的套件、各种老古董,全删了。这不仅仅是安全考虑,也是为了让握手路径更短、更可预测。

实际抓包看一眼

光看理论不过瘾,自己抓一次包就全明白了。用 tcpdump 抓 HTTPS 流量:

# 抓 443 端口的流量,不解析 DNS

sudo tcpdump -i any -nn -s 0 port 443 -w tls_handshake.pcap



# 然后另开一个终端,发起 HTTPS 请求

curl -s https://www.example.com/ > /dev/null



# 抓完 Ctrl+C 停止,用 Wireshark 打开 pcap 文件

打开 pcap 后,在 Wireshark 的过滤栏输入 `tls.handshake.type`,就能看到所有 TLS 握手消息。按顺序看:

- `ClientHello`(type 1):展开可以看到支持的密码套件列表,以及 `key_share` 扩展——里面就是客户端预置的密钥交换参数。

- `ServerHello`(type 2):看 `Cipher Suite` 字段,服务器选的是哪个套件;再看 `key_share`,服务器选的曲线和参数。

- `Certificate`(type 11):服务器的证书链,双击可以查看证书详情。

- `CertificateVerify`(type 15):服务器的签名,证明它持有证书对应的私钥。

- `Finished`(type 20):握手完成,后续的 Application Data 就是加密后的 HTTP 流量了。

有个小技巧:Wireshark 里右键 ClientHello,选择 `Protocol Preferences` 可以配置 TLS 解密。如果你有服务器的私钥,甚至可以解密后续的加密流量——这也是排查 HTTPS 问题时的常用手段。

回到开头的问题

现在可以回答同事那个问题了。HTTPS 的加密是分层的:握手阶段用明文协商密钥,协商完成后再用对称密钥加密数据。抓包能看到握手过程,恰恰是设计如此——因为握手阶段的信息(证书、随机数、密钥交换参数)本身不泄露敏感数据,真正的机密是协商出的那个对称密钥。

TLS 握手的本质,是用两个 RTT 的明文开销,换取后续所有流量的机密性和完整性。这个成本是值得的,但如果你的业务对首包延迟极度敏感,TLS 1.3 还提供了 0-RTT 模式——客户端在 ClientHello 里直接带上上次会话的密钥材料,服务器可以直接回复应用数据,省掉整个握手往返。

代价是什么?重放攻击的风险。0-RTT 的数据没法保证幂等性,所以只适合 GET 请求这种安全操作。

更多推荐