一次 HTTPS 请求的完整旅程:从 TLS 握手到加密传输

HTTP 你已经看透了每一个字节。HTTPS 在 TCP 之上加了一层 TLS——数据还是那些数据,但全程加密,抓包只能看到密文。
这篇文章把 HTTPS 的每个阶段拆开来看:TLS 握手怎么协商密钥、证书怎么验证、数据怎么加密,以及如何用 Wireshark 把这一切看清楚。
4c370ca8-ce50-433e-aeae-21b3ebac79bc


目录

  1. 全局视角:HTTPS 比 HTTP 多了什么
  2. TLS 在协议栈里的位置
  3. TLS 握手:完整的七步流程
  4. 证书验证:怎么确认服务器是真的
  5. 密钥协商:ECDHE 的工作原理
  6. 加密数据传输:TLS Record 结构
  7. TLS 1.2 vs TLS 1.3:握手包数量减半
  8. 真实抓包:逐包解读一次完整 HTTPS 会话
  9. 解密 HTTPS 流量:SSLKEYLOGFILE 方法
  10. 关键概念速查表

1. 全局视角:HTTPS 比 HTTP 多了什么

HTTP 的问题很简单:所有数据明文传输,路由器、运营商、同一 WiFi 下的任何人都能看到你发了什么、收到了什么。

HTTPS = HTTP + TLS。TLS 在 TCP 连接建立之后、HTTP 数据发送之前,插入了一个"握手"阶段,协商出一个只有两端知道的对称密钥,之后所有 HTTP 数据都用这个密钥加密传输。

你的机器 (192.168.5.221)                   服务器 (54.198.84.224:443)
         │                                          │
         │  ① DNS 查询(和 HTTP 一样)               │
         │─────────────────────────────────────────►│
         │◄─────────────────────────────────────────│
         │                                          │
         │  ② TCP 三次握手(和 HTTP 一样)            │
         │──── [SYN]  ────────────────────────────►│
         │◄─── [SYN, ACK] ─────────────────────────│
         │──── [ACK]  ────────────────────────────►│
         │                                          │
         │  ③ TLS 握手(HTTPS 新增,约 2 个 RTT)    │
         │──── ClientHello ───────────────────────►│
         │◄─── ServerHello ────────────────────────│
         │◄─── Certificate(1400字节,分片传输)─────│
         │◄─── ServerKeyExchange, ServerHelloDone ──│
         │──── ClientKeyExchange + ChangeCipherSpec►│
         │◄─── NewSessionTicket + ChangeCipherSpec ─│
         │◄─── Finished ───────────────────────────│
         │                                          │
         │  ④ 加密的 HTTP 请求                       │
         │──── [Application Data] ────────────────►│  ← 看不到内容
         │                                          │
         │  ⑤ 加密的 HTTP 响应(多个包)              │
         │◄─── [Application Data × N] ─────────────│  ← 看不到内容
         │                                          │
         │  ⑥ TLS 关闭 + TCP 四次挥手               │
         │◄─── [Encrypted Alert] ──────────────────│
         │◄─── [FIN, ACK] ─────────────────────────│
         │──── [ACK] + [FIN, ACK] ────────────────►│
         │◄─── [ACK]  ─────────────────────────────│

和 HTTP 相比,多了第 ③ 步 TLS 握手,端口从 80 变成 443,其余完全一样。


2. TLS 在协议栈里的位置

TLS 不是独立的协议层,它夹在 TCP 和 HTTP 之间:

┌──────────────────────────────────────────────────────┐
│  应用层 (HTTP)                                        │
│  GET / HTTP/1.1\r\nHost: httpbin.org\r\n\r\n         │
│  ← 这层的内容对 TLS 完全透明,TLS 只管加密           │
├──────────────────────────────────────────────────────┤
│  TLS 层                                               │
│  ContentType=23  Version=TLS1.2  Length=...          │
│  [加密后的 HTTP 数据,外部看不到原文]                  │
├──────────────────────────────────────────────────────┤
│  传输层 (TCP)                                         │
│  src_port=58458  dst_port=443  seq=...  flags=[PSH,ACK]│
├──────────────────────────────────────────────────────┤
│  网络层 (IP)                                          │
│  src=192.168.5.221  dst=54.198.84.224  TTL=128       │
├──────────────────────────────────────────────────────┤
│  链路层 (Ethernet)                                    │
│  src_mac=aa:bb:cc  dst_mac=dd:ee:ff                  │
└──────────────────────────────────────────────────────┘

Wireshark 抓到的 HTTPS 包,IP 和 TCP 层完全可读,TLS 层的 ContentType 和 Length 可读,但 Payload 是密文。


3. TLS 握手:完整的七步流程

以 TLS 1.2 为例(最常见,TLS 1.3 在第 7 节讲)。

客户端 (192.168.5.221)                    服务器 (54.198.84.224)
   │                                              │
   │  1. ClientHello                              │
   │  "我支持这些加密套件,这是我的随机数 C"       │
   │  SNI=httpbin.org(明文!)                   │
   │─────────────────────────────────────────────►│
   │                                              │
   │  2. ServerHello                              │
   │  "我选 ECDHE-RSA-AES128-GCM-SHA256"          │
   │  "这是我的随机数 S"                           │
   │◄─────────────────────────────────────────────│
   │                                              │
   │  3. Certificate(可能分多个 TCP 包)           │
   │  "这是我的证书链(服务器证书+中间CA)"         │
   │◄─────────────────────────────────────────────│
   │                                              │
   │  4. ServerKeyExchange                        │
   │  "这是我的 ECDHE 临时公钥"                   │
   │◄─────────────────────────────────────────────│
   │                                              │
   │  5. ServerHelloDone                          │
   │  "我说完了,轮到你"                           │
   │◄─────────────────────────────────────────────│
   │                                              │
   │  ← 客户端验证证书,计算 Pre-Master Secret →  │
   │                                              │
   │  6. ClientKeyExchange + ChangeCipherSpec      │
   │     + Encrypted Handshake Message            │
   │  "这是我的 ECDHE 公钥,切换加密,握手完成"    │
   │─────────────────────────────────────────────►│
   │                                              │
   │  7. NewSessionTicket + ChangeCipherSpec       │
   │     + Encrypted Handshake Message            │
   │  "给你一个 Session Ticket(下次可以快速恢复)" │
   │  "切换加密,握手完成"                         │
   │◄─────────────────────────────────────────────│
   │                                              │
   │  ══════ TLS 握手完成,开始加密通信 ══════     │

Session Ticket 是什么?

第 7 步里服务器发的 NewSessionTicket 是一个优化机制:

服务器把本次会话的密钥材料加密后打包成 Ticket,发给客户端保存
下次连接同一服务器时,客户端在 ClientHello 里带上这个 Ticket
服务器解密 Ticket,直接恢复上次的会话密钥,跳过完整握手

好处:节省 1 个 RTT(从 2 RTT 降到 1 RTT)
代价:如果 Ticket 加密密钥泄露,历史会话可能被解密(影响前向保密)

密钥是怎么来的

握手过程中产生了三个原材料:

C(Client Random)  ← ClientHello 里,客户端生成的 32 字节随机数
S(Server Random)  ← ServerHello 里,服务器生成的 32 字节随机数
PMS(Pre-Master Secret)← 通过 ECDHE 密钥交换,双方各自计算出相同的值

Master Secret = PRF(PMS, "master secret", C + S)
                ↑ 伪随机函数,把三个原材料混合成 48 字节主密钥

Session Key = 从 Master Secret 派生出 4 个密钥:
  client_write_key  ← 客户端加密用
  server_write_key  ← 服务器加密用
  client_write_MAC  ← 客户端消息认证码用
  server_write_MAC  ← 服务器消息认证码用

为什么需要两个随机数加上 PMS?防止任何一方单独控制最终密钥。即使服务器是恶意的,它也无法预测客户端的随机数,反之亦然。


4. 证书验证:怎么确认服务器是真的

TLS 握手里最关键的一步:客户端收到服务器证书后,怎么确认这个证书是真的?

证书里有什么

X.509 证书结构:

┌─────────────────────────────────────────────────────┐
 Subject: CN=httpbin.org                                这张证书是给谁的
 Issuer:  CN=Amazon RSA 2048 M03                        谁签发的
 Valid:   Jul 20 2025 ~ Aug 17 2026                     有效期
 Public Key: (RSA 2048-bit)                             服务器公钥
 SAN: httpbin.org, *.httpbin.org                        覆盖的域名
 Signature: [CA 用私钥对上面内容的签名]                  防篡改
└─────────────────────────────────────────────────────┘

信任链验证

你的操作系统/浏览器内置了约 150 个"根证书"(Root CA)
这些根 CA 是被全球信任的机构(DigiCert、Let's Encrypt、Amazon 等)

验证过程(以 httpbin.org 为例):

httpbin.org 的服务器证书
    │ 由谁签发?
    ▼
Amazon RSA 2048 M03(中间 CA)
    │ 由谁签发?
    ▼
Amazon Root CA 1(根 CA)
    │ 在系统信任列表里吗?
    ▼
✓ 在 → 证书链验证通过,连接继续
✗ 不在 → 浏览器报"证书不受信任",连接中断

类比:
  证书 = 身份证
  中间 CA = 省级公安局
  根 CA = 国家公安部(最终权威)
  你信任公安部,所以信任省公安局,所以信任这张身份证

验证的四个检查项

① 签名验证
  用 CA 的公钥解密证书签名,得到证书内容的哈希值
  自己计算证书内容的哈希值
  两者相等 → 证书没被篡改

② 域名匹配
  证书的 CN 或 SAN 字段必须包含你访问的域名
  *.httpbin.org 可以匹配 api.httpbin.org
  但不能匹配 a.b.httpbin.org(通配符只匹配一级)

③ 有效期
  当前时间必须在 notBefore ~ notAfter 之间
  证书过期 → 浏览器报"证书已过期"

④ 吊销检查(OCSP / CRL)
  证书可能被提前吊销(私钥泄露、公司倒闭等)
  OCSP:实时向 CA 查询证书状态(Online Certificate Status Protocol)
  CRL:下载 CA 发布的吊销列表(Certificate Revocation List)

5. 密钥协商:ECDHE 的工作原理

现代 HTTPS 几乎都用 ECDHE(椭圆曲线 Diffie-Hellman 临时密钥交换)。它解决了一个核心问题:如何在不安全的信道上,让双方协商出一个只有自己知道的共享密钥?

直觉理解:颜色混合比喻

公开信息(所有人都能看到):
  底色 = 黄色

客户端的秘密:红色(只有自己知道)
服务器的秘密:蓝色(只有自己知道)

客户端发给服务器:黄色 + 红色 = 橙色(公开传输,ServerKeyExchange 里)
服务器发给客户端:黄色 + 蓝色 = 绿色(公开传输,ClientKeyExchange 里)

客户端拿到绿色,加上自己的红色 → 橙绿混合色(Pre-Master Secret)
服务器拿到橙色,加上自己的蓝色 → 橙绿混合色(Pre-Master Secret)

两边得到相同的颜色!
攻击者只看到橙色和绿色,无法还原出红色或蓝色。

实际的数学过程(ECDH)

公开参数:椭圆曲线(如 P-256 或 x25519),基点 G

客户端:
  生成随机私钥 a(只存在内存里,不发出去)
  计算公钥 A = a × G(椭圆曲线点乘)
  把 A 发给服务器(在 ClientKeyExchange 里)

服务器:
  生成随机私钥 b(只存在内存里,不发出去)
  计算公钥 B = b × G
  把 B 发给客户端(在 ServerKeyExchange 里)

双方各自计算共享密钥:
  客户端:K = a × B = a × (b × G) = ab × G
  服务器:K = b × A = b × (a × G) = ab × G
  → 结果相同!这就是 Pre-Master Secret

攻击者知道 A、B、G,但无法从 A=a×G 反推出 a
(椭圆曲线离散对数问题,目前无法破解)

ECDHE 里的 E(Ephemeral,临时的):每次连接都生成新的临时密钥对 a 和 b,握手结束后立刻丢弃。即使服务器的长期私钥泄露,攻击者也无法解密历史会话,因为临时密钥已经不存在了。这叫前向保密(Forward Secrecy)


6. 加密数据传输:TLS Record 结构

握手完成后,所有数据都封装在 TLS Record 里传输。

TLS Record 的结构

TLS Record(在 TCP Payload 里):

┌──────────────────────────────────────────────────────┐
│ Content Type  (1 byte)                               │
│   14 = ChangeCipherSpec  (0x14)                      │
│   15 = Alert             (0x15)  错误/关闭通知        │
│   16 = Handshake         (0x16)  握手消息             │
│   17 = Application Data  (0x17)  加密的 HTTP 数据     │
├──────────────────────────────────────────────────────┤
│ Version  (2 bytes)                                   │
│   03 01 = TLS 1.0                                    │
│   03 03 = TLS 1.2(也用于 TLS 1.3,历史原因)         │
├──────────────────────────────────────────────────────┤
│ Length  (2 bytes)                                    │
│   后面 Payload 的字节数(最大 16384 字节)             │
├──────────────────────────────────────────────────────┤
│ Payload  (Length bytes)                              │
│   握手阶段:明文的握手消息(ClientHello 等)           │
│   数据阶段:加密后的内容(AES-GCM 等)                 │
│   ┌──────────────────────────────────────────────┐   │
│   │ Encrypted HTTP Data                          │   │
│   │ + Auth Tag(16字节,防篡改)                  │   │
│   └──────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────┘

加密套件(Cipher Suite)决定了用什么算法

握手时协商的加密套件,例如本次实验用的:

ECDHE-RSA-AES128-GCM-SHA256

完整名称:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256

拆解:
  ECDHE        → 密钥交换算法(椭圆曲线 DH,有前向保密)
  RSA          → 证书签名算法(用来验证服务器身份)
  AES_128_GCM  → 对称加密算法(加密实际数据,128位密钥)
  SHA256       → PRF/MAC 哈希算法(消息完整性)

AES-GCM 加密的实际过程

输入:
  明文 = HTTP 请求/响应(如 "GET / HTTP/1.1\r\n...")
  密钥 = session key(握手时协商的,128 位)
  Nonce = 每条消息唯一的 12 字节随机数(防重放攻击)

输出:
  密文 = AES-GCM(明文, 密钥, Nonce)  ← 和明文等长
  Tag  = 16 字节认证标签(接收方用来验证数据没被篡改)

接收方:
  用相同密钥和 Nonce 解密
  验证 Tag,不匹配则立刻丢弃(可能被篡改或传输错误)

GCM 的优势:加密和认证一步完成,比 CBC+HMAC 快,且没有 padding oracle 漏洞

7. TLS 1.2 vs TLS 1.3:握手包数量减半

TLS 1.3 是 2018 年发布的新版本,主要改进是把握手从 2 个 RTT 压缩到 1 个 RTT。

TLS 1.2 握手(2 RTT)

时间轴(每个 → 代表一次网络传输):

t=0ms   客户端 ──ClientHello──────────────────────────────► 服务器
t=20ms  客户端 ◄─ServerHello + Certificate + ServerHelloDone─ 服务器
        ↑ 第一个 RTT 结束,客户端开始验证证书

t=20ms  客户端 ──ClientKeyExchange + ChangeCipherSpec + Finished► 服务器
t=40ms  客户端 ◄─ChangeCipherSpec + Finished──────────────── 服务器
        ↑ 第二个 RTT 结束,握手完成

t=40ms  客户端 ──HTTP GET(加密)────────────────────────────► 服务器
        ↑ 从 TCP 连接建立到第一个 HTTP 请求,共花了约 2 RTT

TLS 1.3 握手(1 RTT)

t=0ms   客户端 ──ClientHello(含 KeyShare)──────────────► 服务器
                ↑ 直接把 ECDHE 公钥带上,不等服务器确认算法

t=20ms  客户端 ◄─ServerHello(含 KeyShare)+ Certificate
                 + CertificateVerify + Finished──────────── 服务器
                ↑ 服务器一次性把所有东西发完,此时密钥已协商好

t=20ms  客户端 ──Finished + HTTP GET(加密)────────────► 服务器
                ↑ 确认握手的同时直接发数据!

        从 TCP 连接建立到第一个 HTTP 请求,只花了 1 RTT

TLS 1.3 的其他改进

① 删除了不安全的算法
  不再支持 RSA 密钥交换(没有前向保密)
  不再支持 RC4、DES、3DES、MD5、SHA-1
  只保留 AES-GCM、ChaCha20-Poly1305 等现代算法

② 0-RTT 恢复(Session Resumption)
  之前连接过的服务器,可以在第一个包里直接发数据
  延迟降到 0 RTT(有重放攻击风险,仅用于幂等请求如 GET)

③ 加密更早
  TLS 1.2:Certificate 是明文传输的(抓包可以看到证书内容)
  TLS 1.3:Certificate 加密传输(更好的隐私保护)

④ 握手消息更少
  TLS 1.2:ServerHello + Certificate + ServerKeyExchange + ServerHelloDone(4条)
  TLS 1.3:ServerHello + EncryptedExtensions + Certificate + CertVerify + Finished(合并发送)

Wireshark 里怎么区分 TLS 版本

看 ClientHello 里的 Extensions:

TLS 1.2(没有 supported_versions extension):
  Handshake Protocol: Client Hello
    Version: TLS 1.2 (0x0303)
    ← 没有 supported_versions,就是 TLS 1.2

TLS 1.3(有 supported_versions extension):
  Handshake Protocol: Client Hello
    Version: TLS 1.2 (0x0303)  ← 外层版本字段,兼容性保留,忽略它
    Extensions:
      supported_versions: TLS 1.3 (0x0304)  ← 这里才是真实版本

→ 规则:看 Extensions 里的 supported_versions,不要看外层 Version 字段

8. 真实抓包:逐包解读一次完整 HTTPS 会话

下面用真实抓包数据(python s06_the_tls_layer.py get httpbin.org)逐包解读。

环境信息:
  客户端:192.168.5.221(Windows,TTL=128)
  服务器:54.198.84.224(httpbin.org,Linux,TTL=233)
  协议:TLSv1.2
  加密套件:ECDHE-RSA-AES128-GCM-SHA256
  Wireshark 过滤器:ip.addr == 54.198.84.224 && tcp.port == 443

8.1 完整包序列总览

包号   方向        协议      长度    Info
────────────────────────────────────────────────────────────────────
316    CS       TCP       52      58458443 [SYN] MSS=1360 WS=256 SACK_PERM
322    SC       TCP       52      44358458 [SYN,ACK] MSS=1418 WS=256
323    CS       TCP       40      [ACK] Seq=1 Ack=1
                             ↑ ─── TCP 三次握手完成 ───

324    CS       TLSv1.2   557     Client Hello (SNI=httpbin.org)
333    SC       TCP       40      [ACK] Seq=1 Ack=518
334    SC       TLSv1.2   1400    Server Hello
335    SC       TCP       40      [ACK]
336    SC       TCP       1400    [TCP PDU reassembled in 337]  ← 证书分片1
337    SC       TLSv1.2   1400    Certificate1400字节,TCP重组)
338    SC       TLSv1.2   211     Server Key Exchange, Server Hello Done
339    CS       TCP       40      [ACK]
340    CS       TLSv1.2   166     Client Key Exchange, Change Cipher Spec,
                                     Encrypted Handshake Message
348    SC       TLSv1.2   244     New Session Ticket, Change Cipher Spec,
                                     Encrypted Handshake Message
                             ↑ ─── TLS 握手完成 ───

351    CS       TLSv1.2   159     Application Data  ← 加密的 HTTP GET
359    SC       TCP       40      [ACK]
364    SC       TLSv1.2   303     Application Data  ← 响应第1片(小包)
365    SC       TCP       1400    [TCP PDU reassembled in 376]  ← 响应分片
366    SC       TCP       1400    [TCP PDU reassembled in 376]
...(多个 1400 字节的 TCP 分片)
376    SC       TLSv1.2   142     Application Data  ← 响应最后一片
                             ↑ ─── HTTP 响应传输完成(共约 9827 字节)───

377    SC       TLSv1.2   71      Encrypted AlertTLS Close Notify
379    SC       TCP       40      [FIN, ACK]
380    CS       TCP       40      [ACK]
381    CS       TCP       40      [FIN, ACK]
382    SC       TCP       40      [ACK]
                             ↑ ─── 连接完全关闭 ───

8.2 TCP 三次握手(包 316-323)

继续输出,从 8.2 TCP 三次握手接着往下:


包 316:C → S  [SYN]
  52 字节(IP头20 + TCP头32含Options)
  Seq=0  Win=65535  MSS=1360  WS=256  SACK_PERM
  ↑ MSS=1360:客户端告诉服务器"每个 TCP 数据段最多 1360 字节"
  ↑ WS=256:窗口缩放因子,实际接收窗口 = 65535 × 256 = 16,776,960 字节
  ↑ SACK_PERM:支持选择性确认,丢包时只重传丢失的包

包 322:S → C  [SYN, ACK]
  52 字节
  Seq=0  Ack=1  Win=26883  MSS=1418  WS=256
  ↑ 服务器 MSS=1418,双方取较小值 1360 作为实际 MSS
  ↑ TTL=233(Linux 服务器,初始值 255,255-233=22,经过约 22 跳路由器)

包 323:C → S  [ACK]
  40 字节(最小 TCP 包,无 Options,无 Payload)
  Seq=1  Ack=1
  ↑ 握手完成,连接建立

8.3 TLS ClientHello(包 324)

包 324:C → S  TLSv1.2  557 字节
  Info: Client Hello (SNI=httpbin.org)

展开 TLS 层:
  Content Type: Handshake (22)       ← 0x16
  Version: TLS 1.0 (0x0301)         ← 兼容性写法,实际版本看 Extensions
  Length: 512

  Handshake Protocol: Client Hello
    Version: TLS 1.2 (0x0303)
    Random: [32字节]                 ← Client Random,密钥材料之一
    Session ID: (空)                 ← 新连接,无缓存
    Cipher Suites: (多个)
      TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256  ← 最终协商用这个
      TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
      TLS_RSA_WITH_AES_128_GCM_SHA256
      ...
    Extensions:
      server_name: httpbin.org       ← SNI!明文可见,这是唯一暴露域名的地方
      supported_groups: x25519, secp256r1, ...
      signature_algorithms: rsa_pss_rsae_sha256, ...

重点:SNI 是明文的
  即使 HTTPS 加密了所有 HTTP 内容,
  ClientHello 里的 server_name 字段仍然明文可见。
  这意味着网络上的任何人(运营商、路由器)都能看到你访问了哪个域名,
  但看不到你访问了什么路径、发了什么内容。

8.4 TLS ServerHello(包 334)

334:S → C  TLSv1.2  1400 字节
  Info: Server Hello

  Handshake Protocol: Server Hello
    Version: TLS 1.2 (0x0303)
    Random: [32字节]                 ← Server Random,密钥材料之一
    Session ID: [32字节]             ← 服务器分配的 Session ID
    Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
                  ↑ 从客户端列表里选了这个,双方都支持且安全
    Compression Method: null         ← 不压缩(压缩+加密有安全漏洞 CRIME)

8.5 Certificate(包 336-337,TCP 分片重组)

 336:S  C  TCP  1400 字节
  Info: [TCP PDU reassembled in 337]
   这不是一个完整的 TLS 消息,只是证书数据的第一片
   Wireshark 提示"这个包的内容会在包 337 里重组显示"

 337:S  C  TLSv1.2  1400 字节
  Info: Certificate(Wireshark 在这里显示重组后的完整证书)

  Handshake Protocol: Certificate
    Certificates (2 certs)

    证书 1:服务器证书(httpbin.org)
      subject: CN=httpbin.org
      issuer:  CN=Amazon RSA 2048 M03
      validity:
        notBefore: Jul 20 00:00:00 2025 GMT
        notAfter:  Aug 17 23:59:59 2026 GMT
      subjectAltName: httpbin.org, *.httpbin.org
      publicKey: RSA 2048-bit

    证书 2:中间 CA 证书
      subject: CN=Amazon RSA 2048 M03
      issuer:  CN=Amazon Root CA 1
       这条链告诉客户端:httpbin.org  Amazon M03  Amazon Root CA 1

为什么证书要分两个 TCP 包传?
  证书数据约 2000+ 字节,超过了 MSS=1360
  TCP 自动把它拆成两片:
    片1(包336):1360 字节
    片2(包337):剩余部分
  Wireshark  "TCP PDU reassembled in 337" 就是在告诉你这件事

8.6 ServerKeyExchange + ServerHelloDone(包 338)

338:S → C  TLSv1.2  211 字节
  Info: Server Key Exchange, Server Hello Done

  Handshake Protocol: Server Key Exchange
    EC Diffie-Hellman Server Params
      Curve Type: named_curve
      Named Curve: x25519              ← 使用 x25519 椭圆曲线
      Pubkey: [32字节]                 ← 服务器的 ECDHE 临时公钥 B
      Signature: [RSA 签名]            ← 用服务器私钥签名,证明公钥是真的

  Handshake Protocol: Server Hello Done
    ← "我说完了,轮到你"(空消息,只是一个信号)

为什么要对 ECDHE 公钥签名?
  ECDHE 公钥是临时生成的,不在证书里。
  如果不签名,中间人可以替换这个公钥,实施中间人攻击。
  用服务器私钥签名后,客户端用证书里的公钥验证签名,
  确认这个 ECDHE 公钥确实来自真正的服务器。

8.7 ClientKeyExchange + ChangeCipherSpec + Finished(包 340)

340:C → S  TLSv1.2  166 字节
  Info: Client Key Exchange, Change Cipher Spec, Encrypted Handshake Message
  ↑ 三条消息打包在一个 TCP 包里发出

  1. Handshake Protocol: Client Key Exchange
     EC Diffie-Hellman Client Params
       Pubkey: [32字节]               ← 客户端的 ECDHE 临时公钥 A

     此时双方都有了对方的公钥:
       客户端计算:PMS = a × B(a是自己的私钥,B是服务器公钥)
       服务器计算:PMS = b × A(b是自己的私钥,A是客户端公钥)
       两边结果相同 → Pre-Master Secret 协商完成

  2. Change Cipher Spec
     ← "从下一条消息开始,我切换到加密模式"
     ← 这是一个独立的 TLS Record,Content Type=20

  3. Encrypted Handshake Message(即 Finished)
     ← 第一条加密消息!
     ← 内容是所有握手消息的哈希值,用来验证握手没被篡改
     ← 如果中间人修改了任何握手消息,这里的哈希就对不上,连接中断

8.8 NewSessionTicket + ChangeCipherSpec + Finished(包 348)

348:S → C  TLSv1.2  244 字节
  Info: New Session Ticket, Change Cipher Spec, Encrypted Handshake Message

  1. New Session Ticket
     ← 服务器把本次会话的密钥材料加密打包,发给客户端保存
     ← 下次连接时客户端带上这个 Ticket,可以跳过完整握手(节省 1 RTT)
     ← Ticket 有效期通常 24 小时

  2. Change Cipher Spec
     ← "从下一条消息开始,我也切换到加密模式"

  3. Encrypted Handshake Message(即服务器的 Finished)
     ← 服务器的握手完成确认,同样是所有握手消息的哈希

  ══════ 至此 TLS 握手完成 ══════
  双方都持有相同的 Session Key,后续所有数据加密传输

8.9 Application Data:加密的 HTTP 请求(包 351)

351:C → S  TLSv1.2  159 字节
  Info: Application Data

  TLS Record:
    Content Type: Application Data (23)   ← 0x17
    Version: TLS 1.2 (0x0303)
    Length: 119
    Encrypted Data: [119字节密文]          ← 看不到原文

  这 119 字节密文解密后是:
    GET / HTTP/1.1\r\n
    Host: httpbin.org\r\n
    User-Agent: learn-networking/1.0\r\n
    Connection: close\r\n
    \r\n
  (配置 SSLKEYLOGFILE 后 Wireshark 可以直接显示明文)

注意:TLS Record 的 Length=119,但 TCP 包总长 159 字节
  差值 = 159 - 20(IP) - 20(TCP) - 5(TLS header) = 114... 
  实际上 TLS Record 头部 5 字节 + 密文 119 字节 + TCP/IP 头 40 字节 = 164
  Wireshark 显示的 Length 是 TLS Payload 长度,不含 TLS 头本身

8.10 Application Data:加密的 HTTP 响应(包 364-376)

响应总大小约 9827 字节,被拆成多个包传输:

包 364SC  TLSv1.2  303 字节   ← 第一个响应包(较小,可能是 HTTP Header)
包 365SC  TCP      1400 字节  ← [TCP PDU reassembled in 376]366SC  TCP      1400 字节  ← [TCP PDU reassembled in 376]367SC  TCP      1400 字节  ← [TCP PDU reassembled in 376]368SC  TCP      1400 字节  ← [TCP PDU reassembled in 376]369SC  TCP      1400 字节  ← [TCP PDU reassembled in 376]370SC  TCP      1400 字节  ← [TCP PDU reassembled in 376]371SC  TCP      1400 字节  ← [TCP PDU reassembled in 376]372SC  TCP      1400 字节  ← [TCP PDU reassembled in 376]373SC  TCP      1400 字节  ← [TCP PDU reassembled in 376]374SC  TCP      1400 字节  ← [TCP PDU reassembled in 376]375SC  TLSv1.2  142 字节   ← 最后一片

中间穿插着客户端的 TCP ACK 包(包 366368370...ACK)

为什么有这么多 "TCP PDU reassembled in 376"?
  一个 TLS Application Data Record 可以最大 16384 字节
  但 TCP 每次最多传 MSS=1360 字节
  所以一个大的 TLS RecordTCP 拆成多片传输
  Wireshark 把所有片段重组后,在最后一片(376)上显示完整的 TLS 消息
  中间那些片段只显示 "TCP PDU reassembled in 376",不单独解析 TLS

8.11 TLS 关闭 + TCP 四次挥手(包 377-382)

377:S → C  TLSv1.2  71 字节
  Info: Encrypted Alert
  ← TLS Alert,Content Type=21,加密的 Close Notify
  ← 意思是"我的数据发完了,TLS 层关闭"
  ← 类比 HTTP 里的 Connection: close379:S → C  TCP  40 字节
  Info: [FIN, ACK]  Seq=14372  Ack=763
  ← TCP 层关闭,服务器主动发起

包 380:C → S  TCP  40 字节
  Info: [ACK]  Seq=763  Ack=14373
  ← 客户端确认

包 381:C → S  TCP  40 字节
  Info: [FIN, ACK]  Seq=763  Ack=14373
  ← 客户端也关闭

包 382:S → C  TCP  40 字节
  Info: [ACK]  Seq=14373  Ack=764
  ← 服务器确认,连接彻底关闭

TLS Close Notify vs TCP FIN 的区别:
  TLS Close Notify(包377):应用层的优雅关闭,告诉对方"数据发完了"
  TCP FIN(包379):传输层的关闭,释放 TCP 连接资源
  两者都需要,缺一不可:
    只有 TCP FIN 没有 TLS Close Notify → 对方不知道是正常结束还是连接被截断
    只有 TLS Close Notify 没有 TCP FIN → TCP 连接还占着资源

8.12 Wireshark 操作指南

过滤器设置(先查 IP):

# 查目标 IP
python -c "import socket; print(socket.gethostbyname('httpbin.org'))"
# Wireshark 过滤器
ip.addr == 54.198.84.224 && tcp.port == 443

# 只看 TLS 握手
tls.handshake

# 只看 ClientHello(查看 SNI)
tls.handshake.type == 1

# 只看 Certificate
tls.handshake.type == 11

# 只看加密数据
tls.record.content_type == 23

Follow TLS Stream:
右键任意 TLS 包 → FollowTLS Stream

  • 未配置密钥:看到乱码
  • 配置 SSLKEYLOGFILE 后:看到完整 HTTP 明文

时序图:
StatisticsFlow Graph → 勾选 Limit to display filter → 选 TCP Flows
可以看到每个包的方向和时间,握手的 RTT 一目了然。


9. 解密 HTTPS 流量:SSLKEYLOGFILE 方法

9.1 为什么 Wireshark 默认看不到明文

Wireshark 是一个旁观者,它只能看到网线上传输的字节。HTTPS 的密钥从来不经过网线,只存在于两端的内存里,所以 Wireshark 单独无法解密。

普通抓包看到的:                    配置密钥文件后看到的:
─────────────────────────────────────────────────────
No.  Protocol  Info                 No.  Protocol  Info
351  TLSv1.2   Application Data     351  HTTP      GET / HTTP/1.1
364  TLSv1.2   Application Data     364  HTTP      HTTP/1.1 200 OK (text/html)
376  TLSv1.2   Application Data     376  HTTP      [TCP segment of reassembled PDU]

解密的唯一方式:从持有密钥的程序(浏览器、Python)把密钥导出来,交给 Wireshark。

9.2 解密原理:三步走

第一步:Python/浏览器把 Master Secret 写入文件
─────────────────────────────────────────────────────
tls-keys.log 内容:
  CLIENT_RANDOM  a43f6c49...(32字节)  dea372c9...(48字节)
                 ↑                      ↑
                 Client Random          Master Secret
                 (ClientHello 里的随机数,每次握手唯一)

第二步:Wireshark 用 Client Random 匹配抓包
─────────────────────────────────────────────────────
  抓到一个 Application Data 包
      │
      ▼
  找到这条流的 ClientHello,取出 Client Random
      │
      ▼
  拿 Client Random 去 tls-keys.log 里逐行匹配
      │
      ▼
  找到对应的 Master Secret

第三步:从 Master Secret 推导出 Session Key,解密
─────────────────────────────────────────────────────
  Master Secret + Client Random + Server Random
      │
      ▼ PRF(伪随机函数)
      ▼
  client_write_key(客户端加密用)
  server_write_key(服务器加密用)
      │
      ▼ AES-GCM 解密
      ▼
  明文 HTTP 内容

一句话总结:Client Random 是索引,Master Secret 是钥匙,Wireshark 用索引找到钥匙,再用钥匙开锁。

9.3 每次请求的密钥都不一样

是的,每次 TLS 握手都产生全新的密钥:

1次连接:
  Client Random = a43f6c49...   ← 随机生成
  ECDHE 临时私钥 a₁              ← 随机生成,握手后销毁
  → Master Secret = X₁
  → Session Key  = K₁

第2次连接(哪怕1秒后,同一个服务器):
  Client Random = d27883c0...   ← 全新随机数
  ECDHE 临时私钥 a₂              ← 全新随机数,握手后销毁
  → Master Secret = X₂          ← 完全不同
  → Session Key  = K₂            ← 完全不同

tls-keys.log 里有多少行 = 发生了多少次 TLS 握手
每行对应一个独立连接,互不影响

这就是前向保密的意义:即使攻击者录下了你所有的加密流量,拿到了服务器私钥,也无法解密——临时密钥早已销毁,服务器私钥推不出 Session Key。

9.4 实验:用 Python + Wireshark 解密

前提:抓包和密钥导出必须同时进行。

第一步:设置密钥导出路径

# 会话级设置(当前窗口有效,关窗口自动消失)
$env:SSLKEYLOGFILE = "$env:USERPROFILE\Desktop\tls-keys.log"

# 验证设置生效
echo $env:SSLKEYLOGFILE
# 输出:C:\Users\xxx\Desktop\tls-keys.log

第二步:Wireshark 开始抓包

1. 打开 Wireshark,选择网卡(有线选"以太网",WiFi 选"WLAN"2. 先查目标 IP:
   python -c "import socket; print(socket.gethostbyname('httpbin.org'))"
3. 过滤器填入:ip.addr == <查到的IP> && tcp.port == 443
4. 点击蓝色鲨鱼鳍开始抓包

第三步:发起 HTTPS 请求

# 在同一个 PowerShell 窗口运行(必须继承环境变量)
python s06_the_tls_layer.py get httpbin.org

看到输出 响应状态: HTTP/1.1 200 OK 后,停止 Wireshark 抓包。

第四步:配置 Wireshark 读取密钥文件

Edit → Preferences → Protocols → TLS
(Pre)-Master-Secret log filename:
  C:\Users\<你的用户名>\Desktop\tls-keys.log
点 OK

配置完成后,包列表立刻重新解析,Application Data 变成 HTTP。

第五步:查看解密结果

方法一:点击 GET 请求包,下方展开
  ▼ Transport Layer Security
    ▼ TLSv1.2 Record Layer: Application Data
      ▼ Decrypted TLS (90 bytes)    ← 新出现!
        ▼ Hypertext Transfer Protocol
            GET / HTTP/1.1\r\n
            Host: httpbin.org\r\n
            User-Agent: learn-networking/1.0\r\n
            Connection: close\r\n
            \r\n

方法二:右键任意 HTTP 包 → Follow → HTTP Stream
  看到完整的一问一答:
    GET / HTTP/1.1
    Host: httpbin.org
    ...

    HTTP/1.1 200 OK
    Content-Type: text/html; charset=utf-8
    Content-Length: 9593
    ...
    <!DOCTYPE html>...

9.5 用浏览器代替 Python(不需要写任何代码)

设置系统环境变量后,从命令行启动浏览器,所有 HTTPS 流量都能解密:

# 永久设置(只做一次,重启后仍有效)
[System.Environment]::SetEnvironmentVariable(
    "SSLKEYLOGFILE",
    "$env:USERPROFILE\Desktop\tls-keys.log",
    "User"
)
# 重启 PowerShell 后生效

# 从命令行启动 Chrome(必须从命令行启动才能继承环境变量)
& "C:\Program Files\Google\Chrome\Application\chrome.exe"

之后在浏览器里正常访问任何 HTTPS 网站,Wireshark 就能解密所有流量。

9.6 清除环境变量

# 清除会话级变量(当前窗口)
Remove-Item Env:SSLKEYLOGFILE

# 清除系统级变量(如果之前用 SetEnvironmentVariable 设置过)
[System.Environment]::RemoveEnvironmentVariable("SSLKEYLOGFILE", "User")

# 验证已清除
echo $env:SSLKEYLOGFILE   # 输出空白 = 已清除

9.7 安全提示

tls-keys.log 的安全性和私钥一样重要:
  ✗ 不要把这个文件发给别人
  ✗ 不要在生产环境设置 SSLKEYLOGFILE
  ✓ 分析完后可以删除:Remove-Item "$env:USERPROFILE\Desktop\tls-keys.log"

SSLKEYLOGFILE 打破了 HTTPS 的安全前提:
  正常情况:密钥只存在于内存,不落盘,不传输
  开启后:密钥写入磁盘文件,任何能读这个文件的人都能解密你的流量

10. 关键概念速查表

本次实验的完整包序列(真实数据)

包号   阶段        方向   包类型                        关键信息
────────────────────────────────────────────────────────────────────────
316    TCP握手     CS    [SYN]                         MSS=1360, WS=256
322    TCP握手     SC    [SYN, ACK]                    MSS=1418, WS=256
323    TCP握手     CS    [ACK]                         握手完成

324    TLS握手     CS    ClientHello                   SNI=httpbin.org(明文)
334    TLS握手     SC    ServerHello                   选定 ECDHE-RSA-AES128-GCM-SHA256
336    TLS握手     SC    [TCP分片]                     证书数据第1337    TLS握手     SC    Certificate                   httpbin.org证书 + Amazon中间CA
338    TLS握手     SC    ServerKeyExchange+HelloDone   ECDHE临时公钥(x25519339    TLS握手     CS    [ACK]
340    TLS握手     CS    ClientKeyExchange+            客户端ECDHE公钥
                          ChangeCipherSpec+Finished     切换加密,握手完成确认
348    TLS握手     SC    NewSessionTicket+             Session Ticket(下次快速恢复)
                          ChangeCipherSpec+Finished     服务器确认握手完成

351    数据传输    CS    Application Data              加密的 HTTP GET159字节)
364    数据传输    SC    Application Data              响应第1片(303字节)
365-375 数据传输  SC    [TCP分片 × 11]                响应数据(每片1400字节)
376    数据传输    SC    Application Data              响应最后一片(142字节)

377    关闭        SC    Encrypted Alert               TLS Close Notify
379    关闭        SC    [FIN, ACK]                    TCP关闭
380    关闭        CS    [ACK]
381    关闭        CS    [FIN, ACK]
382    关闭        SC    [ACK]                         连接彻底关闭

核心概念一句话总结

概念 一句话
TLS TCP 之上的加密层,HTTP 数据在进入 TCP 之前先被 TLS 加密
TLS 握手 双方协商加密算法、交换密钥材料、验证服务器身份的过程,约 2 RTT
SNI ClientHello 里明文的域名字段,是 HTTPS 里唯一暴露的访问目标信息
证书 服务器的"身份证",由 CA 签发,包含公钥、域名、有效期
信任链 服务器证书 → 中间 CA → 根 CA,根 CA 在系统信任列表里
ECDHE 椭圆曲线 DH 密钥交换,每次连接生成临时密钥,保证前向保密
前向保密 即使服务器私钥泄露,历史会话也无法被解密(因为临时密钥已销毁)
Pre-Master Secret ECDHE 协商出的共享值,是派生 Session Key 的原材料之一
Session Key 握手后派生的对称密钥,实际加密数据用它,比非对称加密快 1000 倍
AES-GCM 对称加密算法,同时提供加密和消息认证(防篡改),现代 HTTPS 标配
ChangeCipherSpec 通知对方"从这条消息之后切换到加密模式"
Session Ticket 服务器发给客户端的加密会话凭证,下次连接可跳过完整握手
TCP PDU reassembled Wireshark 提示:这个包是大消息的分片,在另一个包里重组显示
Encrypted Alert TLS 层的优雅关闭通知,类似 TCP 的 FIN
SSLKEYLOGFILE 浏览器/Python 导出会话密钥的文件,Wireshark 用它解密 HTTPS
TLS 1.3 握手从 2 RTT 降到 1 RTT,删除不安全算法,Certificate 加密传输

Wireshark HTTPS 常用过滤器

# 基础过滤
ip.addr == 54.198.84.224 && tcp.port == 443   只看这次实验的流量
tls                                            只看 TLS 包
tls.handshake                                  只看握手包

# 按握手消息类型
tls.handshake.type == 1     ClientHello(查 SNI、支持的套件)
tls.handshake.type == 2     ServerHello(查选定的套件)
tls.handshake.type == 11    Certificate(查证书内容)
tls.handshake.type == 12    ServerKeyExchange(查 ECDHE 公钥)
tls.handshake.type == 14    ServerHelloDone
tls.handshake.type == 16    ClientKeyExchange(查客户端 ECDHE 公钥)
tls.handshake.type == 4     NewSessionTicket

# 按 TLS Record 类型
tls.record.content_type == 20    ChangeCipherSpec
tls.record.content_type == 21    Alert(包括 Close Notify)
tls.record.content_type == 22    Handshake
tls.record.content_type == 23    Application Data(加密数据)

# 解密后使用
http                              解密后的 HTTP 流量
http.request.method == "GET"      只看 GET 请求
http.response.code == 200         只看 200 响应

# 排查问题
tls.alert_message                 TLS 错误(握手失败、证书错误等)
tcp.analysis.retransmission       TCP 重传包(网络质量差时出现)

HTTP vs HTTPS 对比

维度              HTTP                    HTTPS
─────────────────────────────────────────────────────────────────
端口              80                      443
加密              无,明文传输             TLS 加密,密文传输
握手开销          TCP 三次握手(1.5 RTT)  TCP + TLS 1.23.5 RTT)
                                          TCP + TLS 1.32.5 RTT)
证书              不需要                  需要(Let's Encrypt 免费)
Wireshark 可读    完全可读                IP/TCP/TLS头可读,Payload密文
                                          配置 SSLKEYLOGFILE 后可解密
SNI               无                      ClientHello 里明文可见域名
中间人攻击        容易                    需要伪造受信任 CA 签发的证书
性能              略快(无加密开销)       现代硬件有 AES-NI 指令,差距可忽略
TCP 分片          HTTP 数据直接分片        TLS Record 先封装,再被 TCP 分片
关闭方式          TCP FIN                 TLS Close Notify + TCP FIN