TLS 握手只干三件事:确认对面是谁、商量用什么算法、换出一把会话密钥。 它跑在 TCP 之上,所以只保护「这一段」,出了这一段就没了。
上一节的 IPsec 保护到网关为止。 TLS 保护到「应用进程」为止,所以它是唯一能做到端到端的那一层。 Demo 真连一次 example.com,把协议版本、加密套件、证书签发者都打印出来。
一、握手的三件事
握手这个词听着复杂,其实只解决三个问题: 对面到底是谁、我们用哪套算法、接下来那把会话密钥是什么。 第一件事靠证书:服务器把证书发过来,客户端顺着证书链验到根, 验通了才知道「这把公钥真的属于这个名字」。 如果只拿到一把公钥而不验身份,中间人换一把自己的公钥就全盘接管了—— 这正是所有中间人攻击的共同原理。 第二件事容易被忽略:客户端和服务器各自列出「我支持哪些套件」,然后挑一个共同的。 这件事之所以危险,是因为「支持」不等于「安全」—— 如果两边都还留着某个老套件,攻击者就可以把它降下来。 TLS 1.3 的应对很干脆:把不安全的全部删掉,没得谈。 第三件事是核心:双方各自算出一个只有两个人知道的密钥,而且这个密钥从不在链路上出现。 具体做法是「各出一半材料,公开交换,然后各自算出同一个结果」,这就是 Diffie-Hellman 交换。 它妙在「公开的东西凑不出私有的结果」,所以哪怕全程被录音,也推不出那把钥匙。
二、证书链:为什么不只发一张
浏览器和系统里只预装了「根证书」,而根证书对应的私钥在离线机房里,几年才用一次。 所以真实的签发是三层:根签中间、中间签站点。 服务器必须把「自己那张」和「中间那张」一起发过来,因为客户端手里只有根,缺了中间就接不上。 这就是那个经典的故障:「浏览器能开、手机打不开」——有些客户端会自己去补中间证书,有些不会。 排查这类问题的第一个动作,就是看服务器有没有发全整条链。 证书还有两个常被忘的字段:有效期和吊销状态。 前者靠系统时间判断,所以「设备时间错乱」会导致所有 HTTPS 报错; 后者要靠 OCSP 或 CRL 去查,而查询本身又可能拖慢握手, 所以有了 OCSP stapling——服务器替客户端去查,把结果一起发过来。
三、记录层与前向保密
握手之后是记录层,它干的事很朴素:把上层数据切成块、加密、附上完整性校验、编号。 编号是为了防重放和防乱序, 加密和校验就是前面讲过的对称加密加 HMAC(GCM 把两者合在一起算)。 这里有一个值得单独记住的性质:前向保密。 如果密钥交换用的是「服务器的私钥去解密客户端送来的密钥」, 那么服务器私钥一旦泄露,攻击者可以把以前录下的流量全部解开。 而用 ECDHE 这种「临时交换」的方式,每次会话都换一对临时密钥,用完就扔, 所以私钥泄露也解不开历史流量。 这也解释了为什么现在的配置都强制用 ECDHE:不是为了更快,是为了「今天泄露不连累昨天」。 同理,TLS 1.3 干脆只保留了带前向保密的密钥交换方式。 最后说 0-RTT 的代价:会话复用能让下一次连接零往返就发数据, 但「零往返」意味着「服务器还没确认你的身份,就已经收下你的请求」。 所以 0-RTT 只允许发「重复也无害」的请求——查一个页面可以,转账绝对不行。
四、动手 Demo:真连一次 HTTPS,看握手都谈了什么
用标准库真连 example.com 的 443,打印协议版本、加密套件、证书上的名字和签发者, 再算一遍不同 RTT 下的握手代价。
import socket, ssl
ctx = ssl.create_default_context()
with socket.create_connection(("www.example.com", 443), timeout=15) as raw:
with ctx.wrap_socket(raw, server_hostname="www.example.com") as ss:
print(ss.version(), ss.cipher()[0])
cert = ss.getpeercert()
print(cert["subject"][-1][0][1])
print(cert["issuer"][-1][0][1])
print(cert["notBefore"], "->", cert["notAfter"])
实跑输出:
--------------------------------------------------------------------
真连一次 HTTPS,看握手都谈了什么
--------------------------------------------------------------------
**TLS 的握手可以概括成三句话**:**确认对面是谁、商量用什么算法、换出一把只有双方知道的会话密钥。**
**下面真连一次,把谈出来的结果打印出来。**
项目 谈出来的结果
------------------------------------------------------------------------------
**协议版本** **TLSv1.3**
**加密套件** **TLS_AES_256_GCM_SHA384**
**证书上的名字** **example.com**
**谁签的** **Cloudflare TLS Issuing ECC CA 3**
**有效期** **Sep 26 22:49:11 2026 GMT 至 Dec 25 22:56:35 2026 GMT**
**备用名字条数** **2**
**第二行是重点**:`TLS_AES_256_GCM_SHA384` 这个名字拆开看是「协议套件」四个字,
**它一次定下三件事**:**密钥交换用什么、加密用什么、完整性用什么。**
**TLS 1.3 把可选的套件砍到只剩几个**,
**原因就是老版本里那些「看起来能配」的组合,有一半其实不安全。**
**第三到第五行是证书的三件事**:**它是谁、谁给它背书、什么时候过期。**
**注意「谁签的」不是根证书**,**而是一个中间机构**——**这就是证书链:根签中间、中间签站点。**
**根证书预装在系统里,所以整条链能被验通。**
**握手的代价是往返次数**:
单程 RTT TLS 1.2(两趟) TLS 1.3(一趟) 会话复用(零趟)
------------------------------------------------------------------------
**20 ms** **40 ms** **20 ms** **0 ms**
**50 ms** **100 ms** **50 ms** **0 ms**
**150 ms** **300 ms** **150 ms** **0 ms**
**TLS 1.2 那一列比 TLS 1.3 那一列多整整一趟**:
**跨洋链路 RTT 150 毫秒时,TLS 1.2 光握手就多花 150 毫秒。**
**TLS 1.3 把它压到一趟**,**办法是「客户端先猜一组参数」**,
**把密钥材料随第一个包一起发过去**——**猜错就退回重来,猜对就省一整趟。**
**这个思路和 TCP 的快速打开是同一个:先发再说,省下的时间比返工的概率值钱。**
**第四列是「零趟」**:**上次会话的密钥材料被记住,下一次直接拿着用**。
**代价是「重放」风险**——**所以 0-RTT 只允许发「重复也无害」的请求,转账这种绝不许走 0-RTT。**
第一行是谈出来的协议版本和套件:TLSv1.3 加 TLS_AES_256_GCM_SHA384。
这个名字拆开是三段:
AES-256 是加密、GCM 是工作模式(顺带做完整性)、SHA384 是握手时用的哈希。
TLS 1.3 里套件的命名规则变了,因为密钥交换不再由套件决定,
所以老版本那种「一长串、看不懂」的名字消失了。
第三行是证书上的名字,第四行是签发者。
注意签发者不是根证书,而是一个中间机构——这就是证书链:根签中间、中间签站点。
服务器把中间证书一起发过来,客户端用手里的根去验,整条链才通。
第五行是有效期,只有三个月。
这个长度不是随便定的:短有效期能限制「私钥泄露之后还能被冒用多久」,
代价是必须自动化续签——这就是 Let's Encrypt 和 ACME 协议存在的理由。
最后那张 RTT 表要看清一件事:TLS 1.2 比 1.3 多一整趟,跨洋链路上就是多 150 毫秒。
而 0-RTT 那一列全是零,代价是重放风险,所以只用于「重复也无害」的请求。
30 秒实验
- 算一下:一个页面要连 30 个域名,每个多一趟 50 毫秒,TLS 1.2 比 1.3 总共多花多久?(1.5 秒)
- 想一下:为什么服务器必须把中间证书一起发过来?
- 想一下:前向保密防的是哪一种「以后」?
常见误区
- 以为「有锁就是安全网站」。 锁只说明这一段被加密了,不说明这个站是好人。
- 以为「TLS 能防住一切」。 它保护到服务器为止,服务器后面的数据库它管不了。
- 以为「会话复用越激进越好」。 0-RTT 有重放风险,不能用在有副作用的请求上。
自测三问(点开对答案)
1. TLS 握手要解决的三件事? —— **确认对方身份、商量算法、换出会话密钥**。 2. 为什么服务器要发中间证书? —— **客户端只预装了根**,缺了中间那一环就接不上链。 3. 前向保密是什么意思? —— **服务器私钥将来泄露,也解不开以前录下的流量**,靠的是每次换临时密钥。下一节讲有线网络的门禁:插上网线,凭什么就能上网?
- ☐ (本节完,下一节 9.4.3 IEEE802.1X)