Skip to content

TLS 1.3 协议深入

简介

TLS(Transport Layer Security)是互联网上最广泛使用的加密通信协议。HTTPS 中的"S"就来自 TLS。本文从零开始讲解 TLS 1.3 的完整握手流程、消息结构和关键机制,为理解 REALITY 如何在 TLS 握手中嵌入认证信息打下基础。

如果不理解 TLS 1.3 的握手,就不可能理解 REALITY 的巧妙之处。

本文回答

  • TLS 1.3 握手每一步在做什么,交换了什么信息
  • ClientHello 的二进制结构,每个字段的含义
  • 为什么 Session ID 可以被 REALITY 复用
  • TLS 扩展机制如何工作(SNI、ALPN、Key Share 等)
  • 握手过程中哪些是明文、哪些是加密的
  • 证书链验证的基本逻辑

本文在项目中的位置

本文是理解 REALITY 协议和 XTLS 流控的前置必修课。代码中的 hello.Raw[39:]HandshakeState.ServerHello 等操作直接对应 TLS 协议结构。

适合读者

需要理解 TLS 二进制协议层面的开发者。如果有 Wireshark 使用经验,配合抓包阅读效果最佳。

前置知识

建议先阅读 密码学基础,理解 ECDH、HMAC、HKDF、AEAD、数字签名的概念。

文章理解难度

较难。TLS 协议是多个子系统的精密组合,需要耐心逐层理解。

重点

  • ClientHello 的结构和 REALITY 修改的位置(Session ID 在 Raw 字节中的偏移 39)
  • TLS 1.3 握手的明文/加密分界线
  • 扩展(Extensions)是 TLS 可扩展性的基础
  • 密钥调度(Key Schedule)的两阶段设计

难点

  • TLS 记录的二进制编码(长度前缀、变长字段)
  • ClientHello 的 Raw 字节与解析后的结构体之间的关系
  • 密钥调度的多层派生逻辑

1. TLS 在协议栈中的位置

应用层 (L7)  : HTTP/2, WebSocket, gRPC ...

表示层 (L6)  : TLS Record Layer  ← 我们在这里

传输层 (L4)  : TCP

网络层 (L3)  : IP

TLS 位于 TCP 之上、应用协议之下。应用程序将数据交给 TLS 层,TLS 加密后通过 TCP 发送。

2. TLS 记录层(Record Layer)

TLS 的所有数据——包括握手消息和应用数据——都封装在**TLS 记录(TLS Record)**中传输。

2.1 记录格式

每个 TLS 记录由 5 字节头部 + 可变长度载荷组成:

[内容类型: 1 byte] [TLS版本: 2 bytes] [数据长度: 2 bytes] [数据载荷: 变长]

例子(十六进制):

17 03 03 01 02 [1202 字节的加密应用数据]
│  └────┘ └───┘
│    │      │
│    │      数据载荷长度 = 0x0102 = 258 字节
│    │
│    TLS 1.2 的版本号(Wire Version)——TLS 1.3 在记录层也使用 0x0303

内容类型:
  0x14 = ChangeCipherSpec(TLS 1.3 中已废弃,但为兼容性可能发送)
  0x15 = Alert(错误或关闭通知)
  0x16 = Handshake(握手消息)
  0x17 = Application Data(应用数据)

2.2 握手消息的嵌套

当内容类型为 0x16(Handshake)时,数据载荷内部是握手消息,有自己独立的头部:

TLS 记录层:
[0x16] [0x0303] [长度] 
   └── 握手消息层:
       [握手类型: 1 byte] [消息长度: 3 bytes] [消息内容: 变长]

握手类型(只列出常用):

  • 0x01:ClientHello
  • 0x02:ServerHello
  • 0x0B:Certificate
  • 0x0F:CertificateVerify
  • 0x14:Finished

2.3 对 REALITY 和 XTLS 的重要性

XTLS Vision 通过读取 TLS 记录头的前 5 个字节(内容类型 + 版本 + 长度)来判断内部流量是否为 TLS——这就是流量识别的基础。


3. TLS 1.3 完整握手流程

TLS 1.3 的一次完整握手(1-RTT)大约需要交换以下消息:

3.1 消息详解

(1) ClientHello

客户端发送的第一个消息。完全明文。这是 TLS 握手中审查方能观察到的所有内容。

核心字段:

ClientHello:
├── Protocol Version         # 客户端支持的最高 TLS 版本
├── Random                   # 32 字节客户端随机数
├── Session ID               # 会话 ID(TLS 1.3 中已废弃,但仍占位)
├── Cipher Suites            # 支持的密码套件列表
├── Compression Methods      # 压缩方法(TLS 1.3 中必须为空)
└── Extensions               # 扩展列表(TLS 的扩展机制)
    ├── server_name (0x0000)           # SNI:客户端要访问的域名
    ├── supported_groups (0x000A)      # 支持的密钥交换组
    ├── signature_algorithms (0x000D)  # 支持的签名算法
    ├── supported_versions (0x002B)    # 支持的 TLS 版本(TLS 1.3 必含)
    ├── key_share (0x0033)             # 客户端 ECDH 公钥
    ├── psk_key_exchange_modes (0x002D) # PSK 模式
    ├── ALPN (0x0010)                  # 应用层协议:h2, http/1.1
    └── ...

(2) ServerHello

服务端响应。明文。包含:

  • 选定的 TLS 版本
  • 服务端随机数(32 字节)
  • 选定的密码套件
  • 服务端的 Key Share(服务端 ECDH 公钥)

从这里开始,后续所有消息都是加密的。加密密钥由双方根据 ClientHello 和 ServerHello 中的 Key Share 计算出 SharedSecret 后派生。

也就是说,审查方最多只能看到 ClientHello 和 ServerHello。Certificate、CertificateVerify 等对审查方是不可见的。

这个分界线对理解 REALITY 非常重要:审查方看不到服务端返回的证书内容。他们只能通过 ClientHello 和 ServerHello 的特征来判断"这是不是一个代理"。

(3) EncryptedExtensions

加密的扩展信息。包含服务端选择的 ALPN 等。加密

(4) Certificate

服务端的证书链。加密

证书链通常包含:

  • 叶证书(Leaf Certificate):服务端自己的证书,包含服务端公钥和域名
  • 中间 CA 证书:签发叶证书的中间证书颁发机构
  • (根证书不在链中传输,客户端本地已预置)

(5) CertificateVerify

服务端用其证书私钥对握手记录进行签名,证明它拥有证书对应的私钥。加密

(6) Server Finished

服务端发送的 Finished 消息,包含整个握手过程的 MAC 值。加密

(7) Client Finished

客户端发送的 Finished 消息。加密


4. ClientHello 二进制结构与 REALITY 修改点

4.1 Raw 字节布局

ClientHello 序列化为 TLS 网络格式后的字节布局(简化版):

偏移  内容
0     [Handshake Type: 1 byte]  = 0x01 (ClientHello)
1-3   [Message Length: 3 bytes]
4-5   [Client Version: 2 bytes]
6-37  [Random: 32 bytes]        ← ClientHello.Random
38    [Session ID Length: 1 byte]
39-70 [Session ID: 32 bytes]    ← REALITY 修改这里!(hello.Raw[39:])
71-72 [Cipher Suites Length: 2 bytes]
73-.. [Cipher Suites: 变长]
..    [Compression Methods Length: 1 byte]
..    [Compression Methods: 变长]
..    [Extensions Length: 2 bytes]
..    [Extensions: 变长]

4.2 REALITY 为什么修改偏移 39

REALITY 修改 hello.Raw[39:] 是因为 Session ID 在 Raw 字节中的位置是固定的(偏移 39,假设 Session ID Length 为 32)。

源码位置:xray-core/transport/internet/reality/reality.go 行 141-175

go
// 初始化 32 字节的 Session ID
hello.SessionId = make([]byte, 32)

// 将 Session ID 指向 Raw 的内部切片
copy(hello.Raw[39:], hello.SessionId)

// 写入版本号和时间戳
hello.SessionId[0] = core.Version_x     // 偏移 39
hello.SessionId[1] = core.Version_y     // 偏移 40
hello.SessionId[2] = core.Version_z     // 偏移 41
hello.SessionId[3] = 0                  // 偏移 42(保留)
binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix())) // 偏移 43-46

// 复制 ShortId
copy(hello.SessionId[8:], config.ShortId) // 偏移 47+

// 用 AEAD 加密整个前 16 字节
aead.Seal(hello.SessionId[:0], hello.Random[20:], hello.SessionId[:16], hello.Raw)

// 将加密后的 Session ID 写回 Raw 字节
copy(hello.Raw[39:], hello.SessionId)

关键理解:hello.Raw 是 uTLS 构造的完整 ClientHello 字节切片。hello.SessionId 在被赋值为新切片之前,指向的是 hello.Raw 内部的子切片。REALITY 通过修改 hello.SessionId 的值来修改 hello.Raw 的内容——修改后写回偏移 39 的位置。


5. TLS 扩展机制

TLS 的扩展机制是其可扩展性的基础。每个扩展有一个 2 字节的类型 ID 和可变长度的数据:

[Extension Type: 2 bytes] [Extension Length: 2 bytes] [Extension Data: 变长]

5.1 关键扩展详解

SNI(Server Name Indication)— 类型 0x0000

[0x00 0x00] [长度] [Server Name List]
  └── Server Name:
      [Name Type: 0x00 = hostname] [Name Length: 2 bytes] [Name: "www.microsoft.com"]

SNI 是审查方进行域名级别封锁的主要依据。REALITY 通过让客户端发送真实存在的目标网站 SNI(如 www.microsoft.com),使这个字段无法被用于识别代理流量。

Key Share — 类型 0x0033

[0x00 0x33] [长度] [Client Key Share]
  └── [Group: 2 bytes] [Key Exchange Length: 2 bytes] [Key Exchange Data: 变长]

Group 的常见值:

  • 0x001D(29)= X25519
  • 0x11EC(4588)= X25519MLKEM768(混合后量子密钥交换)

客户端的 ECDH 公钥包含在 Key Share 扩展中。在 uTLS 的 HandshakeState.State13.KeyShareKeys 中,REALITY 会读取这个公钥(或后量子混合公钥)来执行 ECDH 计算。

ALPN(Application-Layer Protocol Negotiation)— 类型 0x0010

[0x00 0x10] [长度] [Protocol Name List]
  └── [Protocol 1: "h2"] [Protocol 2: "http/1.1"]

ALPN 告知对方自己支持的应用层协议。REALITY 配置中要求的目标网站需要支持 H2(HTTP/2),因为 HTTP/2 的二进制帧格式相比 HTTP/1.1 的文本格式更难以被审查方分析模式。


6. TLS 1.3 密钥调度

TLS 1.3 的密钥调度是一个多层次的 HKDF 派生过程:

每一层的 Secret 通过 HKDF-Expand 派生出具体的:

  • 客户端/服务端写入密钥(用于加密)
  • 客户端/服务端 IV(用于 AEAD Nonce)
  • Finished 密钥(用于 Finished 消息中的 MAC)

在标准 TLS 1.3 中,这些都是通过 TLS 内部的密钥调度自动完成的。REALITY 在 TLS 1.3 密钥调度之外,额外进行了自己的 ECDH + HKDF 派生(AuthKey)。


7. 证书和证书链验证

7.1 什么是 x509 证书

一个 x509 证书包含:

  • 主体(Subject):证书所属者的信息(如域名)
  • 公钥(Public Key):证书持有者的公钥
  • 签发者(Issuer):签发此证书的 CA 信息
  • 有效期(Not Before / Not After):证书的有效时间范围
  • 签名(Signature):签发者用其私钥对此证书内容的签名
  • 扩展(Extensions):额外信息(如 SAN 域名列表、密钥用途等)

7.2 证书链验证流程

客户端收到的证书链:
  [叶证书] → [中间 CA 证书] → [根 CA 证书(不在链中,预置在客户端)]

验证步骤:
  1. 叶证书的 Issuer 匹配中间 CA 证书的 Subject
  2. 用中间 CA 证书的公钥验证叶证书的签名 → 通过
  3. 中间 CA 证书的 Issuer 匹配根 CA 证书的 Subject
  4. 用根 CA 证书的公钥验证中间 CA 证书的签名 → 通过
  5. 根 CA 证书在客户端信任的根证书列表中 → 信任建立
  6. 叶证书的 Subject/SAN 匹配访问的域名(SNI)→ 域名验证通过

7.3 REALITY 如何改变了证书验证

标准 TLS 中,客户端验证证书链是否由受信任 CA 签发。

REALITY 中:

  • InsecureSkipVerify: true → 跳过标准 CA 链验证
  • VerifyPeerCertificate 回调 → 自定义验证逻辑

验证逻辑变成了:

  1. 证书公钥是否为 Ed25519?
  2. 如果是,证书签名是否等于 HMAC-SHA512(AuthKey, Ed25519公钥)
  3. 如果是 → Verified = true(这是 REALITY 临时证书)
  4. 如果不是 → 退回标准 x509 证书链验证 → 判断为"真证书"

8. TLS 1.3 中 Session ID 的状态

在 TLS 1.2 及之前,Session ID 用于会话恢复:客户端在后续连接中发送上次连接的 Session ID,服务端可以用缓存的会话参数跳过完整握手。

在 TLS 1.3 中,会话恢复改用 PSK(Pre-Shared Key)机制。Session ID 字段虽然保留在 ClientHello 格式中,但实际上不再用于任何 TLS 层面的功能

这意味着 REALITY 可以使用这 32 字节的 Session ID 作为认证信息载体,而不会影响 TLS 握手。审查方看到的是一个"有正常 Session ID 字段"的 ClientHello——完全合规。

服务端也可以选择忽略这个 Session ID(因为 TLS 1.3 不用它),但 REALITY 服务端会特意读取它。


9. 对理解 REALITY 至关重要的 TLS 事实总结

事实对 REALITY 的意义
ClientHello 是明文的审查方能看。所以必须"看起来正常"
ServerHello 也是明文的审查方能看。服务端随机数和选定的密码套件必须合规
ServerHello 之后全部加密审查方看不到。临时证书内容对审查方不可见
Session ID 在 TLS 1.3 中已废弃可以安全地复用作认证信息嵌入
SNI 是明文的必须指向一个真实存在的、未被封锁的域名
Key Share 是明文的客户端的 ECDH 公钥暴露在外——但 ECDH 的安全性保证即使公钥被看到,共享秘密也无法计算
密码套件列表是明文的uTLS 必须模拟浏览器的套件列表,不能用 Go 标准库的默认列表
TLS 扩展是明文的扩展的种类、顺序都会影响指纹

外部参考资料

资料:RFC 8446 - The Transport Layer Security (TLS) Protocol Version 1.3 类型:IETF 标准 链接:https://www.rfc-editor.org/rfc/rfc8446 可信度:A

资料:The Illustrated TLS 1.3 Connection 类型:技术博客(交互式 TLS 1.3 演示) 链接:https://tls13.xargs.org/ 可信度:A,每个字节都有标注的 TLS 握手可视化

资料:TLS Fingerprinting with JA3 and JA3S 类型:技术博客(Salesforce) 链接:https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967 可信度:A

读者自检

读完本文后应能回答:

  • TLS 1.3 握手中哪两个消息是明文的,之后的消息为什么是加密的
  • ClientHello 中 Session ID 在 Raw 字节中的偏移量为什么是 39
  • TLS 扩展机制如何让 SNI、ALPN、Key Share 共存于 ClientHello
  • 标准证书链验证的步骤
  • 为什么 TLS 1.3 的 Session ID 可以被 REALITY 安全复用

下一步阅读