mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
5961 字
17 分钟
TLS 1.3 握手详解
2026-03-23

每次你看到浏览器地址栏的锁头图标,背后就是 TLS 在工作。TLS 1.3 是 2018 年发布的安全协议,它把延续了 20 年的 SSL/TLS 握手从 2-RTT 简化到了 1-RTT,同时移除了所有已知不安全的算法。

数字签名 中理解了签名与验证。这一章来拆解 TLS 1.3 的完整握手流程。

一、TLS 概述#

1.1 TLS 的角色#

TLS(Transport Layer Security)在传输层提供加密、认证和完整性保护:

TLS 提供的安全属性实现方式
机密性对称加密(AES-GCM/ChaCha20)
认证数字证书(X.509)
完整性AEAD 认证标签
前向安全ECDHE 临时密钥交换

1.2 TLS 在网络协议栈中的位置#

TLS 工作在传输层(TCP)之上、应用层(HTTP)之下,这就是它叫”Transport Layer Security”的原因:

graph TB subgraph 应用层["应用层"] HTTP["HTTP / SMTP / IMAP"] end subgraph 安全层["安全层"] TLS["TLS 1.3"] end subgraph 传输层["传输层"] TCP["TCP"] end subgraph 网络层["网络层"] IP["IP"] end subgraph 链路层["链路层"] ETH["Ethernet / Wi-Fi"] end HTTP --> TLS TLS --> TCP TCP --> IP IP --> ETH style 安全层 fill:#e8f5e9,stroke:#2e7d32 style TLS fill:#c8e6c9,stroke:#1b5e20

TLS 保护的是应用层数据的机密性和完整性,但不保护 IP 头、TCP 头等元数据——这意味着攻击者仍然能看到通信双方的 IP 地址、数据包大小和时序模式。

1.3 TLS 保护什么,不保护什么#

维度TLS 保护TLS 不保护
数据内容加密传输
数据完整性AEAD 认证标签
服务器身份X.509 证书验证
通信方 IP 地址明文可见
数据包大小可用于流量分析
访问时间可用于时序分析
DNS 查询除非使用 DoH/DoT
SNI 信息明文可见(TLS 1.3 用 ESNI 缓解)
Note

TLS 不是万能的。它保护数据在传输中不被窃听和篡改,但无法防御流量分析、元数据泄露等侧信道攻击。对于这些威胁,需要结合 Tor、VPN、ESNI 等额外机制。

1.4 TLS 版本演进#

版本年份状态关键改进
SSL 3.01996已废弃
TLS 1.01999已废弃
TLS 1.12006已废弃
TLS 1.22008仍可用AEAD 支持
TLS 1.32018推荐简化握手、0-RTT、前向安全

1.5 TLS 1.2 的已知问题#

TLS 1.2 的问题不是”缺少功能”,而是”允许太多不安全的选项”。以下是针对 TLS 1.2 的著名攻击:

攻击名称年份利用漏洞影响
BEAST2011CBC 模式 IV 可预测解密 HTTPS 数据
CRIME2012TLS 压缩侧信道窃取会话 Cookie
POODLE2014SSL 3.0 CBC 填充降级攻击解密数据
ROBOT2017RSA PKCS#1 v1.5 填充预言机伪造签名解密数据
Logjam2015EXPORT 级 DH 参数降级破解密钥交换
Sweet32201664 位分组密码碰撞长连接数据泄露
DROWN2016SSLv2 协议交叉攻击解密 TLS 服务器数据

这些攻击的共同根源是:TLS 1.2 允许协商使用不安全的算法。TLS 1.3 的解决方式很直接——直接移除这些算法,而不是再打补丁。

二、TLS 1.3 握手流程#

2.1 完整握手(1-RTT)#

TLS 1.3 的核心改进是把握手从 2-RTT 压缩到 1-RTT。关键在于:客户端在第一条消息中就发送 DH 公钥,而不是等服务器先确认用哪个密钥交换算法。

sequenceDiagram participant C as Client participant S as Server Note over C,S: === TLS 1.3 完整握手(1-RTT)=== rect rgb(232, 245, 233) Note over C,S: 飞行 1:Client → Server C->>S: ClientHello<br/>supported_versions: TLS 1.3<br/>key_share: X25519 公钥<br/>signature_algorithms: rsa_pss_rsae_sha256<br/>supported_groups: x25519, secp256r1 end rect rgb(227, 242, 253) Note over C,S: 飞行 2:Server → Client S->>C: ServerHello<br/>selected_version: TLS 1.3<br/>key_share: X25519 公钥 S->>C: EncryptedExtensions<br/>ALPN, 服务器自定义扩展 S->>C: Certificate<br/>服务器证书链 S->>C: CertificateVerify<br/>用私钥签名握手上下文 S->>C: Finished<br/>验证握手完整性 end rect rgb(255, 243, 224) Note over C,S: 飞行 3:Client → Server C->>S: Finished<br/>验证握手完整性 end Note over C,S: 握手完成,开始加密通信 C->>S: 应用数据(加密) S->>C: 应用数据(加密)
步骤消息方向说明
1ClientHelloC→S客户端发送支持的套件、DH 公钥和签名算法
2ServerHelloS→C服务端选定套件,发送 DH 公钥
3EncryptedExtensionsS→C服务端扩展信息(ALPN 等)
4CertificateS→C服务端发送证书链
5CertificateVerifyS→C服务端签名证明拥有私钥
6FinishedS→C服务端确认握手完成
7FinishedC→S客户端确认握手完成

注意:从 ServerHello 之后的所有消息都是加密的——这是 TLS 1.3 相比 1.2 的重要改进,TLS 1.2 中 Certificate 和 CertificateVerify 是明文传输的。

2.2 ClientHello 包含什么#

ClientHello 是 TLS 握手的第一条消息,客户端用它告诉服务器”我支持什么”:

字段/扩展内容说明
client_version0x0303兼容字段,实际版本由 supported_versions 决定
random32 字节随机数防止重放,参与密钥派生
session_id空或旧会话 IDTLS 1.3 不依赖它,但保留兼容性
cipher_suites5 个 TLS 1.3 套件客户端支持的密码套件列表
compression_methodsnullTLS 1.3 禁止压缩(防 CRIME 攻击)
supported_versions[0x0304]表示支持 TLS 1.3
key_shareX25519 公钥(32 字节)关键:提前发送 DH 公钥
signature_algorithmsrsa_pss_rsae_sha256 等支持的签名算法
supported_groupsx25519, secp256r1 等支持的椭圆曲线
psk_key_exchange_modespsk_dhe_kePSK 配合 DHE 的模式
pre_shared_key可选,PSK 标识用于会话恢复或 0-RTT

2.3 ServerHello 包含什么#

ServerHello 是服务端的回应,告诉客户端”我选了什么”:

字段/扩展内容说明
server_version0x0303兼容字段
random32 字节随机数服务端随机数,参与密钥派生
session_id回显客户端的 ID兼容中间件
cipher_suite选定的套件如 TLS_AES_256_GCM_SHA384
compression_methodnull不压缩
supported_versions0x0304确认使用 TLS 1.3
key_shareX25519 公钥关键:服务端 DH 公钥

2.4 为什么 TLS 1.3 能做到 1-RTT#

TLS 1.2 需要 2-RTT 的根本原因是:客户端必须先问服务器”你支持什么密钥交换”,服务器回答后,客户端才能发送自己的 DH 公钥。这多了一个来回。

TLS 1.3 的解决方案是猜测

  1. 客户端在 ClientHello 中直接发送 key_share(DH 公钥),同时列出所有支持的曲线
  2. 如果服务器支持客户端选的曲线,直接回 ServerHello + 自己的 DH 公钥 → 1-RTT 完成
  3. 如果服务器不支持,回 HelloRetryRequest 让客户端换一条曲线 → 退化为 2-RTT

实际部署中,X25519 和 secp256r1 的覆盖率超过 99%,所以 HelloRetryRequest 极少发生。

key_shares = {
"x25519": generate_x25519_keypair(), # 优先级最高
"secp256r1": generate_ec_keypair("P-256"), # 备选
}

2.5 ECDHE 密钥交换过程#

TLS 1.3 强制使用 ECDHE(椭圆曲线 Diffie-Hellman 临时密钥交换),以 X25519 为首选曲线。以下是密钥交换的数学过程:

客户端 服务器
------ ------
生成私钥 a(随机标量) 生成私钥 b(随机标量)
计算公钥 A = a * G 计算公钥 B = b * G
(G 是曲线基点)
发送 A ──────────────────────────────────→
←────────────────────── 发送 B
计算共享密钥: 计算共享密钥:
shared = a * B shared = b * A
= a * (b * G) = b * (a * G)
= (a * b) * G = (a * b) * G
↑ 两者相等 ↑
前向安全保证:
- a 和 b 是临时生成的,用完即弃
- 即使长期私钥泄露,也无法反推 shared
- 每次连接的 shared 都不同

X25519 基于 Curve25519 椭圆曲线,密钥只有 32 字节,计算速度比 NIST P-256 快约 3 倍,且实现更简单、更抗侧信道攻击。

三、TLS 1.2 vs 1.3 对比#

3.1 握手流程对比#

graph LR subgraph TLS12["TLS 1.2(2-RTT)"] direction TB C1["① ClientHello"] --> S1["② ServerHello"] S1 --> S2["③ Certificate"] S2 --> S3["④ ServerKeyExchange"] S3 --> S4["⑤ ServerHelloDone"] S4 --> C2["⑥ ClientKeyExchange"] C2 --> C3["⑦ ChangeCipherSpec"] C3 --> C4["⑧ Finished"] C4 --> S5["⑨ ChangeCipherSpec"] S5 --> S6["⑩ Finished"] end subgraph TLS13["TLS 1.3(1-RTT)"] direction TB C5["① ClientHello + key_share"] --> S7["② ServerHello + key_share"] S7 --> S8["③ EncryptedExtensions"] S8 --> S9["④ Certificate"] S9 --> S10["⑤ CertificateVerify"] S10 --> S11["⑥ Finished"] S11 --> C6["⑦ Finished"] end style TLS12 fill:#ffebee,stroke:#c62828 style TLS13 fill:#e8f5e9,stroke:#2e7d32

3.2 核心差异对比#

维度TLS 1.2TLS 1.3
握手 RTT2-RTT1-RTT
密码套件37 个5 个
密钥交换RSA/ECDHE仅 ECDHE
前向安全可选强制
0-RTT不支持支持
已知弱点BEAST/CRIME/ROBOT无已知弱点
证书加密明文传输加密传输
协商方式逐项协商一次性确定
扩展机制有限丰富的扩展框架

3.3 TLS 1.3 移除了什么#

TLS 1.3 的设计哲学是”做减法”——移除一切不安全的选项:

移除项原因替代方案
RSA 密钥交换无前向安全,私钥泄露=历史全泄露ECDHE(强制前向安全)
CBC 模式BEAST/POODLE/Lucky13 攻击AEAD(GCM/ChaCha20)
RC4多种密码学攻击,已完全不安全AES-GCM/ChaCha20
SHA-1碰撞攻击已实际可行SHA-256/SHA-384
MD5碰撞攻击极其容易SHA-256/SHA-384
DES/3DES密钥太短/Sweet32 攻击AES-128/AES-256
EXPORT 密码套件Logjam 降级攻击不再支持弱密码
压缩CRIME/BREACH 攻击禁用 TLS 层压缩
非AEAD 密码套件无法同时保证加密+完整性仅保留 AEAD
renegotiation降级攻击风险不支持重新协商
DSA 签名随机数泄露=私钥泄露RSA-PSS/ECDSA/EdDSA
Note

TLS 1.3 移除了所有不安全的算法:RSA 密钥交换、CBC 模式、RC4、SHA-1、MD5。这是 TLS 1.3 比 TLS 1.2 更安全的根本原因——不是新增了什么,而是移除了不安全的东西。

3.4 迁移挑战#

从 TLS 1.2 迁移到 TLS 1.3 并非总是平滑的:

挑战说明解决方案
中间件兼容性部分防火墙/代理不识别 TLS 1.3逐步灰度,保留 1.2 兼容
证书兼容性某些旧证书签名算法不被 1.3 支持更新证书,使用 RSA-PSS 或 ECDSA
应用层依赖代码依赖 TLS 1.2 的 renegotiation重构应用逻辑,避免 renegotiation
调试困难1.3 加密了更多握手信息使用 SSLKEYLOGFILE 配合 Wireshark
合规要求某些行业标准尚未更新到 1.3保留 1.2 兼容,同时启用 1.3

四、0-RTT(早数据)#

4.1 0-RTT 原理#

0-RTT 允许客户端在重连时,在握手的第一条消息中就携带加密的应用数据,无需等待服务器响应:

sequenceDiagram participant C as Client participant S as Server Note over C,S: === 首次连接(1-RTT)=== C->>S: ClientHello + key_share S->>C: ServerHello + key_share + Certificate + Finished C->>S: Finished S-->>C: New Session Ticket(PSK) Note over C,S: === 重连(0-RTT)=== rect rgb(255, 243, 224) C->>S: ClientHello + key_share + PSK<br/>+ 早数据(用 PSK 加密) Note over C: 0-RTT:无需等待即可发送数据 end S->>C: ServerHello + 应用数据 Note over C,S: 0-RTT 数据已被服务器处理

0-RTT 的工作前提是客户端拥有上次连接获得的 PSK(Pre-Shared Key)。客户端用 PSK 派生出早数据密钥,在 ClientHello 中直接携带加密数据。

4.2 0-RTT 重放攻击#

0-RTT 最大的安全风险是重放攻击。因为 0-RTT 数据不包含服务端的随机数贡献,攻击者可以捕获并重放:

攻击场景:0-RTT 重放攻击
1. 客户端发送 0-RTT 请求:
PUT /api/transfer HTTP/1.1
Authorization: Bearer token_xxx
{ "to": "attacker", "amount": 1000 }
2. 攻击者截获该请求
3. 攻击者原封不动地重放(可能多次):
→ 服务器收到相同的 PUT 请求
→ 服务器处理了多次转账
→ 客户端被转走 N × 1000 元
关键问题:
- 0-RTT 数据没有 Server Random 参与
- 服务器无法区分"新请求"和"重放请求"
- TCP 层的重放防御对应用层无效

4.3 如何安全使用 0-RTT#

安全措施说明实现方式
仅用于幂等操作GET/HEAD 请求天然幂等应用层限制 0-RTT 请求类型
服务端反重放记录已处理的 0-RTT 请求使用时间窗口 + 去重缓存
限制 0-RTT 数据量减少重放的影响面max_early_data_size 控制大小
单次使用票据每个 PSK 只用一次服务器签发一次性 Session Ticket
时间窗口限制票据过期后不可用Session Ticket 设置短有效期

4.4 哪些场景不能用 0-RTT#

场景原因正确做法
支付/转账非幂等,重放=重复扣款使用 1-RTT
创建订单非幂等,重放=重复下单使用 1-RTT
修改密码非幂等,重放=安全隐患使用 1-RTT
删除资源非幂等,重放=误删使用 1-RTT
POST/PUT 请求通常非幂等默认不用 0-RTT
跨服务器负载均衡不同服务器可能无法共享反重放状态使用 1-RTT 或共享反重放缓存
Warning

0-RTT 数据容易受到重放攻击——攻击者可以捕获并重放 0-RTT 请求。只对幂等操作(GET 请求、查询)使用 0-RTT,不要用于支付、下单等非幂等操作。

五、密钥派生#

5.1 完整密钥调度#

TLS 1.3 使用 HKDF 从共享密钥逐层派生出所有需要的密钥。整个过程形成一个严格的层次结构:

flowchart TB ECDHE["ECDHE 共享密钥<br/>(a * b * G)"] ECDHE --> EXTRACT0["HKDF-Extract<br/>salt = 0(零值)<br/>IKM = shared_secret"] EXTRACT0 --> HS_SECRET["handshake_secret"] HS_SECRET --> EXTRACT_C_HS["Derive-Secret<br/>label = c hs traffic"] HS_SECRET --> EXTRACT_S_HS["Derive-Secret<br/>label = s hs traffic"] EXTRACT_C_HS --> C_HS_KEY["client_handshake_key<br/>+ client_handshake_iv"] EXTRACT_S_HS --> S_HS_KEY["server_handshake_key<br/>+ server_handshake_iv"] HS_SECRET --> EXTRACT1["HKDF-Extract<br/>IKM = handshake_secret"] EXTRACT1 --> MS_SECRET["master_secret"] MS_SECRET --> EXTRACT_C_APP["Derive-Secret<br/>label = c ap traffic"] MS_SECRET --> EXTRACT_S_APP["Derive-Secret<br/>label = s ap traffic"] EXTRACT_C_APP --> C_APP_KEY["client_application_key<br/>+ client_application_iv"] EXTRACT_S_APP --> S_APP_KEY["server_application_key<br/>+ server_application_iv"] MS_SECRET --> EXPORTER["Derive-Secret<br/>label = exp master"] EXPORTER --> EXPORTER_KEY["exporter_master_secret"] style ECDHE fill:#fff9c4,stroke:#f9a825 style HS_SECRET fill:#e1bee7,stroke:#6a1b9a style MS_SECRET fill:#bbdefb,stroke:#1565c0 style C_APP_KEY fill:#c8e6c9,stroke:#2e7d32 style S_APP_KEY fill:#c8e6c9,stroke:#2e7d32

5.2 HKDF-Extract 和 HKDF-Expand-Label#

TLS 1.3 的密钥派生基于两个核心操作:

HKDF-Extract:从输入密钥材料中提取固定长度的伪随机密钥

HKDF-Expand-Label:从伪随机密钥派生出指定长度的密钥

HKDF-Expand-Label(Secret, Label, Context, Length) =
HKDF-Expand(Secret, HkdfLabel, Length)
其中 HkdfLabel 的结构:
struct {
uint16 length = Length;
opaque label<7..255> = "tls13 " + Label;
opaque context<0..255> = Context;
} HkdfLabel;
示例:派生客户端握手密钥
HKDF-Expand-Label(
handshake_secret,
"c hs traffic", // label
Transcript-Hash(CH), // context = ClientHello 的哈希
32 // length
) → client_handshake_traffic_secret

5.3 从 ECDHE 共享密钥到应用数据密钥#

完整的密钥派生链路如下:

import hmac
import hashlib
def hkdf_extract(salt: bytes, ikm: bytes) -> bytes:
"""HKDF-Extract: HMAC(salt, IKM)"""
return hmac.new(salt, ikm, hashlib.sha256).digest()
def hkdf_expand_label(secret: bytes, label: str, context: bytes, length: int) -> bytes:
"""HKDF-Expand-Label(简化版)"""
hkdf_label = length.to_bytes(2, 'big')
label_bytes = b"tls13 " + label.encode()
hkdf_label += len(label_bytes).to_bytes(1, 'big') + label_bytes
hkdf_label += len(context).to_bytes(1, 'big') + context
return hkdf_expand(secret, hkdf_label, length)
def hkdf_expand(prk: bytes, info: bytes, length: int) -> bytes:
"""HKDF-Expand"""
n = (length + 31) // 32
okm = b""
t = b""
for i in range(1, n + 1):
t = hmac.new(prk, t + info + i.to_bytes(1, 'big'), hashlib.sha256).digest()
okm += t
return okm[:length]
shared_secret = ecdhe_compute_key(client_private, server_public)
early_secret = hkdf_extract(b'\x00' * 32, b'\x00' * 32)
handshake_secret = hkdf_extract(early_secret, shared_secret)
client_hs_traffic = hkdf_expand_label(handshake_secret, "c hs traffic", transcript_hash_ch, 32)
server_hs_traffic = hkdf_expand_label(handshake_secret, "s hs traffic", transcript_hash_ch, 32)
client_hs_key = hkdf_expand_label(client_hs_traffic, "key", b"", 16) # AES-128
client_hs_iv = hkdf_expand_label(client_hs_traffic, "iv", b"", 12) # GCM IV
server_hs_key = hkdf_expand_label(server_hs_traffic, "key", b"", 16)
server_hs_iv = hkdf_expand_label(server_hs_traffic, "iv", b"", 12)
master_secret = hkdf_extract(handshake_secret, b'\x00' * 32)
client_app_traffic = hkdf_expand_label(master_secret, "c ap traffic", transcript_hash_all, 32)
server_app_traffic = hkdf_expand_label(master_secret, "s ap traffic", transcript_hash_all, 32)
client_app_key = hkdf_expand_label(client_app_traffic, "key", b"", 16)
client_app_iv = hkdf_expand_label(client_app_traffic, "iv", b"", 12)
server_app_key = hkdf_expand_label(server_app_traffic, "key", b"", 16)
server_app_iv = hkdf_expand_label(server_app_traffic, "iv", b"", 12)

5.4 派生密钥一览#

派生密钥用途何时使用
client_handshake_key/iv加密客户端握手消息Finished 消息
server_handshake_key/iv加密服务端握手消息EncryptedExtensions 开始
client_application_key/iv加密客户端应用数据握手完成后
server_application_key/iv加密服务端应用数据握手完成后
exporter_master_secret密钥导出(RFC 5705)外部协议绑定
resumption_master_secret派生 PSK 用于会话恢复会话恢复

六、TLS 握手抓包分析#

6.1 Wireshark 抓取 TLS 1.3 握手#

用 Wireshark 分析真实的 TLS 1.3 握手,是理解协议最直观的方式:

export SSLKEYLOGFILE=/tmp/sslkeys.log
sudo tshark -i eth0 -f "tcp port 443" -w /tmp/tls13-capture.pcap &
curl -v --tls13-ciphers TLS_AES_256_GCM_SHA384 https://example.com
tshark -r /tmp/tls13-capture.pcap -Y "tls.handshake" -T fields \
-e frame.number \
-e ip.src \
-e ip.dst \
-e tls.handshake.type \
-e tls.handshake.version

6.2 TLS 1.3 握手报文解读#

以下是典型的 TLS 1.3 握手报文序列:

帧号源 → 目的TLS 消息类型说明
1Client → ServerClientHello1包含 key_share、supported_versions
2Server → ClientServerHello2选定套件,发送 key_share
3Server → ClientChangeCipherSpec兼容中间件,无实际意义
4Server → ClientEncryptedExtensions8加密扩展(ALPN 等)
5Server → ClientCertificate11服务器证书链
6Server → ClientCertificateVerify15签名验证
7Server → ClientFinished20握手完成
8Client → ServerChangeCipherSpec兼容中间件
9Client → ServerFinished20客户端确认
Note

帧号 3 和 8 中的 ChangeCipherSpec 是 TLS 1.3 为了兼容中间件(middlebox)而保留的”假”消息,在 TLS 1.3 中没有任何实际安全作用。如果中间件看不到这个消息,可能会误判连接异常。

6.3 如何用 Wireshark 解密 TLS 流量#

Wireshark 配合 SSLKEYLOGFILE 可以解密 TLS 1.3 的加密握手消息:

  1. 设置环境变量export SSLKEYLOGFILE=/tmp/sslkeys.log
  2. 启动浏览器或应用:Chrome/Firefox/curl 都支持此环境变量
  3. 配置 Wireshark:Edit → Preferences → Protocols → TLS → “(Pre)-Master-Secret log filename” → 选择 /tmp/sslkeys.log
  4. 抓包并分析:现在 Wireshark 能解密所有 TLS 消息
head -3 /tmp/sslkeys.log
Warning

SSLKEYLOGFILE 仅用于开发调试!生产环境绝对不能启用,否则等于把所有 TLS 密钥明文写在磁盘上。密钥日志文件包含所有连接的会话密钥,任何能读取该文件的人都能解密你的 TLS 流量。

七、TLS 会话恢复#

7.1 Session Ticket 机制#

TLS 1.3 不再使用 TLS 1.2 的 Session ID 恢复机制,而是采用 Session Ticket:

sequenceDiagram participant C as Client participant S as Server Note over C,S: === 首次连接 === C->>S: ClientHello + key_share S->>C: ServerHello + key_share + Certificate + Finished C->>S: Finished S-->>C: New Session Ticket<br/>ticket = Enc(PSK, server_key)<br/>lifetime = 7200s<br/>max_early_data_size = 16384 Note over C,S: === 会话恢复(PSK 模式)=== C->>S: ClientHello + pre_shared_key + key_share<br/>(携带 Session Ticket) S->>C: ServerHello + pre_shared_key + key_share<br/>+ Finished C->>S: Finished Note over C,S: 恢复完成(1-RTT,但无需证书验证)

Session Ticket 的核心思路是:服务器把 PSK 用只有自己知道的密钥加密后发给客户端,客户端下次连接时带上这个”票据”,服务器解密后即可恢复会话,无需再次进行证书验证。

特性Session ID(TLS 1.2)Session Ticket(TLS 1.3)
状态存储服务端保存会话状态客户端保存加密票据
服务端开销需要维护会话缓存无状态,解密即可
负载均衡友好需要共享会话缓存天然支持,票据自包含
安全性会话 ID 明文传输PSK 加密传输
票据生命周期服务端控制票据中包含 lifetime

7.2 Pre-Shared Key(PSK)模式#

TLS 1.3 支持两种 PSK 建立方式:

PSK 来源说明使用场景
Session Ticket上次连接后服务器签发最常见,自动会话恢复
外部配置预共享密钥(如 IoT 设备)受限环境,无证书基础设施

PSK 模式的安全属性:

属性PSK-onlyPSK + DHE(推荐)
前向安全
密钥交换仅 PSKPSK + ECDHE
0-RTT 支持
安全等级较低
PSK_KE_ONLY = 0 # 仅 PSK,无前向安全(不推荐)
PSK_DHE_KE = 1 # PSK + DHE,有前向安全(推荐)
psk_key_exchange_modes = [PSK_DHE_KE] # 只接受 PSK + DHE

八、TLS 1.3 密码套件#

套件加密密钥交换签名
TLS_AES_128_GCM_SHA256AES-128-GCMECDHERSA/ECDSA
TLS_AES_256_GCM_SHA384AES-256-GCMECDHERSA/ECDSA
TLS_CHACHA20_POLY1305_SHA256ChaCha20-Poly1305ECDHERSA/ECDSA
TLS_AES_128_CCM_SHA256AES-128-CCMECDHERSA/ECDSA
TLS_AES_128_CCM_8_SHA256AES-128-CCM-8ECDHERSA/ECDSA

密码套件命名规则:TLS_{加密算法}_{哈希算法}。注意 TLS 1.3 的密码套件不再包含密钥交换和签名算法——因为密钥交换固定为 ECDHE,签名算法在签名扩展中单独协商。

九、配置最佳实践#

9.1 Nginx 配置#

server {
listen 443 ssl http2;
server_name example.com;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256;
ssl_prefer_server_ciphers on;
ssl_certificate /etc/ssl/certs/server.crt;
ssl_certificate_key /etc/ssl/private/server.key;
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/ssl/certs/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets on;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
}

9.2 Apache 配置#

<VirtualHost *:443>
ServerName example.com
SSLEngine on
SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLCipherSuite TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256
SSLCipherSuite SSL ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
SSLCertificateFile /etc/ssl/certs/server.crt
SSLCertificateKeyFile /etc/ssl/private/server.key
SSLCertificateChainFile /etc/ssl/certs/chain.pem
# HSTS
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
# OCSP Stapling
SSLUseStapling on
SSLStaplingCache shmcb:/var/run/ocsp(128000)
</VirtualHost>

9.3 Go 配置#

package main
import (
"crypto/tls"
"net/http"
)
func main() {
// TLS 1.3 配置
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{
tls.X25519, // 优先 X25519
tls.CurveP256, // 备选 P-256
},
CipherSuites: []uint16{
tls.TLS_CHACHA20_POLY1305_SHA256,
tls.TLS_AES_256_GCM_SHA384,
tls.TLS_AES_128_GCM_SHA256,
},
}
server := &http.Server{
Addr: ":443",
TLSConfig: tlsConfig,
}
// Go 1.21+ 默认启用 TLS 1.3
server.ListenAndServeTLS("server.crt", "server.key")
}

9.4 Java 配置#

// Java: TLS 1.3 配置(JDK 11+)
import javax.net.ssl.SSLContext;
import java.net.ServerSocket;
import java.net.Socket;
public class TLSServer {
public static void main(String[] args) throws Exception {
// 启用 TLS 1.3
SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
sslContext.init(null, null, null);
// 创建 SSL ServerSocket
ServerSocket serverSocket = sslContext
.getServerSocketFactory()
.createServerSocket(443);
// JDK 11+ 默认支持 TLS 1.3
// 可通过系统属性控制:
// jdk.tls.client.protocols=TLSv1.3
// jdk.tls.server.protocols=TLSv1.3
while (true) {
Socket client = serverSocket.accept();
// 处理连接...
}
}
}

9.5 SSL Labs A+ 评级清单#

要获得 SSL Labs A+ 评级,需要满足以下所有条件:

检查项要求验证方式
TLS 版本≥ 1.2,推荐 1.3ssl_protocols TLSv1.2 TLSv1.3
密码套件仅 AEAD(GCM/ChaCha20)ssl_ciphers 不含 CBC/RC4
证书RSA ≥ 2048 或 ECC ≥ 256openssl x509 -text
HSTS启用,max-age ≥ 1 年add_header Strict-Transport-Security
证书透明度启用 CT 日志证书包含 SCT 扩展
OCSP Stapling启用ssl_stapling on
前向安全强制 ECDHE密码套件含 ECDHE
无已知漏洞无 BEAST/ROBOT/Heartbleedsslyze 扫描

9.6 常见配置错误#

错误后果正确做法
ssl_protocols TLSv1 TLSv1.1 TLSv1.2支持 POODLE/BEAST 攻击只保留 TLSv1.2 TLSv1.3
ssl_ciphers 包含 RC4/DES数据可被破解只用 AEAD 套件
HSTS max-age 太短浏览器很快忘记≥ 31536000(1 年)
证书私钥权限 644任何用户可读私钥权限设为 600
未启用 OCSP Stapling客户端需单独查询 OCSPssl_stapling on
SSL Session Timeout 太长会话劫持风险增大1 天左右
同时支持 RSA 密钥交换无前向安全只用 ECDHE 套件

九·附、实践:用 s_client 分析 TLS 握手#

SSL/TLS 协议的历史几乎与万维网本身一样长。1995 年,Netscape 发布了 SSL 2.0——这是第一个被广泛部署的安全传输协议,但它在设计上存在严重缺陷:没有消息认证、密钥交换可被中间人攻击、截断攻击无法检测。SSL 3.0(1996)修复了大部分问题,却引入了 CBC 填充预言机漏洞——这一隐患在 18 年后被 POODLE 攻击彻底引爆。

1999 年,IETF 接管了 SSL 的标准化工作,将其更名为 TLS 1.0。但 TLS 1.0 本质上只是 SSL 3.0 的微调版本,CBC 模式的 IV 可预测性问题依然存在,这直接导致了 2011 年的 BEAST 攻击。TLS 1.1(2006)修复了 CBC 的 IV 问题,却未能阻止更广泛的攻击浪潮——CRIME(2012)利用 TLS 压缩侧信道窃取 Cookie,ROBOT(2017)利用 RSA PKCS#1 v1.5 填充预言机伪造签名,Logjam(2015)通过 EXPORT 级 DH 参数降级攻击破解密钥交换。这些攻击的共同根源是:协议允许协商使用不安全的算法选项。

TLS 1.3(RFC 8446,2018)的解决方式不再是打补丁,而是做减法——直接移除 RSA 密钥交换、CBC 模式、RC4、SHA-1、MD5、压缩、 renegotiation 等所有已知不安全的选项,将密码套件从 37 个精简到 5 个,同时把握手从 2-RTT 压缩到 1-RTT。这一章,用 OpenSSL 的 s_client 工具和 Python ssl 模块,亲手验证这些理论改进。

附.1 前置知识#

  • OpenSSL 3.0+ 命令行工具(本文使用 OpenSSL 3.0.13)
  • Python 3.8+ssl 模块(本文使用 Python 3.13.0)
  • 理解本章前九节的理论内容,特别是握手流程、密码套件和密钥交换

注:如果你的网络环境需要代理才能访问外部网站,s_client 可以使用 -proxy 参数指定 HTTP 代理,例如 -proxy 127.0.0.1:7890

附.2 TLS 1.3 握手分析#

openssl s_client 连接 GitHub,强制使用 TLS 1.3:

openssl s_client -connect github.com:443 -servername github.com -tls1_3 </dev/null

关键输出如下:

CONNECTED(00000003)
---
Certificate chain
0 s:CN = github.com
i:C = GB, O = Sectigo Limited, CN = Sectigo Public Server Authentication CA DV E36
a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA256
v:NotBefore: May 5 00:00:00 2026 GMT; NotAfter: Aug 2 23:59:59 2026 GMT
1 s:C = GB, O = Sectigo Limited, CN = Sectigo Public Server Authentication CA DV E36
i:C = GB, O = Sectigo Limited, CN = Sectigo Public Server Authentication Root E46
a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA384
v:NotBefore: Mar 22 00:00:00 2021 GMT; NotAfter: Mar 21 23:59:59 2036 GMT
2 s:C = GB, O = Sectigo Limited, CN = Sectigo Public Server Authentication Root E46
i:C = US, ST = New Jersey, L = Jersey City, O = The USERTRUST Network, CN = USERTrust ECC Certification Authority
a:PKEY: id-ecPublicKey, 384 (bit); sigalg: ecdsa-with-SHA384
v:NotBefore: Mar 22 00:00:00 2021 GMT; NotAfter: Jan 18 23:59:59 2038 GMT
---
...
Peer signing digest: SHA256
Peer signature type: ECDSA
Server Temp Key: X25519, 253 bits
---
SSL handshake has read 3126 bytes and written 373 bytes
Verification: OK
---
New, TLSv1.3, Cipher is TLS_AES_128_GCM_SHA256
Server public key is 256 bit
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
---
DONE

逐行解读:

输出字段含义
New, TLSv1.3TLSv1.3协议版本确认为 TLS 1.3
Cipher is TLS_AES_128_GCM_SHA256AES-128-GCM + SHA-256TLS 1.3 密码套件,命名规则为 TLS_{加密}_{哈希}
Server Temp Key: X25519X25519临时密钥交换使用 Curve25519,保证前向安全
Peer signature type: ECDSAECDSA证书签名算法为 ECDSA(非 RSA)
Secure Renegotiation IS NOT supported不支持TLS 1.3 移除了 renegotiation 机制
Early data was not sent未发送非 0-RTT 连接,没有早数据
Certificate chain 深度 33 级叶子证书 → 中间 CA → 根 CA

注:TLS 1.3 密码套件的命名不再包含密钥交换和签名算法——因为密钥交换固定为 ECDHE,签名算法在 signature_algorithms 扩展中单独协商。这与 TLS 1.2 的 ECDHE-ECDSA-AES128-GCM-SHA256 命名形成鲜明对比。

附.3 TLS 1.2 vs 1.3 对比#

-tls1_2 参数强制降级到 TLS 1.2:

openssl s_client -connect github.com:443 -servername github.com -tls1_2 </dev/null

关键输出:

...
Peer signing digest: SHA256
Peer signature type: ECDSA
Server Temp Key: X25519, 253 bits
---
SSL handshake has read 3030 bytes and written 365 bytes
Verification: OK
---
New, TLSv1.2, Cipher is ECDHE-ECDSA-AES128-GCM-SHA256
Server public key is 256 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
Protocol : TLSv1.2
Cipher : ECDHE-ECDSA-AES128-GCM-SHA256
Session-ID:
Session-ID-ctx:
Master-Key: F23AA9C594D9E89CDDCE5EC174C31A9F...
PSK identity: None
PSK identity hint: None
SRP username: None
Start Time: 1778164040
Timeout : 7200 (sec)
Verify return code: 0 (ok)
Extended master secret: yes
---
DONE

对比两张握手的差异:

对比维度TLS 1.3TLS 1.2
协议版本TLSv1.3TLSv1.2
密码套件TLS_AES_128_GCM_SHA256ECDHE-ECDSA-AES128-GCM-SHA256
套件命名TLS_{加密}_{哈希}{密钥交换}-{签名}-{加密}_{模式}-{哈希}
Secure RenegotiationIS NOT supportedIS supported
Master-Key 字段不暴露明文暴露 Master-Key
Session-ID不使用保留(为空)
Early datawas not sent无此字段
握手字节数(读/写)3126 / 3733030 / 365

关键发现:

  1. 密码套件命名变化:TLS 1.3 的 TLS_AES_128_GCM_SHA256 不再包含密钥交换和签名算法,因为它们已经固定/单独协商
  2. Master-Key 不再暴露:TLS 1.2 的输出中 Master-Key 明文可见,而 TLS 1.3 的输出中完全没有这个字段——这本身就是安全改进
  3. Renegotiation 被移除:TLS 1.3 不支持安全重新协商,因为该机制本身是攻击面
  4. 握手数据量相近:虽然 TLS 1.3 减少了一个 RTT,但由于加密了更多握手消息,总字节数反而略多

注意:TLS 1.2 输出中的 Master-Key 仅在调试时可见,不会在网络上传输。但这也意味着任何能访问 s_client 输出的人都能获取会话密钥——生产环境中应避免记录此类信息。

附.4 证书链与 OCSP Stapling#

3.1 查看完整证书链#

openssl s_client -connect github.com:443 -servername github.com -showcerts </dev/null

输出中的证书链结构:

Certificate chain
0 s:CN = github.com
i:C = GB, O = Sectigo Limited, CN = Sectigo Public Server Authentication CA DV E36
a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA256
v:NotBefore: May 5 00:00:00 2026 GMT; NotAfter: Aug 2 23:59:59 2026 GMT
-----BEGIN CERTIFICATE-----
MIID7jCCA5SgAwIBAgIRAOfOzDsT+zt7ikbqjNCutxwwCgYIKoZIzj0EAwIw...
-----END CERTIFICATE-----
1 s:C = GB, O = Sectigo Limited, CN = Sectigo Public Server Authentication CA DV E36
i:C = GB, O = Sectigo Limited, CN = Sectigo Public Server Authentication Root E46
a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA384
v:NotBefore: Mar 22 00:00:00 2021 GMT; NotAfter: Mar 21 23:59:59 2036 GMT
-----BEGIN CERTIFICATE-----
MIIDXzCCAuagAwIBAgIQNuBZ7YiN1Xrt1XC2cn+b2jAKBggqhkjOPQQDAzBf...
-----END CERTIFICATE-----
2 s:C = GB, O = Sectigo Limited, CN = Sectigo Public Server Authentication Root E46
i:C = US, ST = New Jersey, L = Jersey City, O = The USERTRUST Network, CN = USERTrust ECC Certification Authority
a:PKEY: id-ecPublicKey, 384 (bit); sigalg: ecdsa-with-SHA384
v:NotBefore: Mar 22 00:00:00 2021 GMT; NotAfter: Jan 18 23:59:59 2038 GMT
-----BEGIN CERTIFICATE-----
MIIDRjCCAsugAwIBAgIQGp6v7G3o4ZtcGTFBto2Q3TAKBggqhkjOPQQDAzCBi...
-----END CERTIFICATE-----

证书链解读:

层级角色主题 (Subject)签名算法公钥类型
0叶子证书CN=github.comecdsa-with-SHA256EC 256 bit
1中间 CACN=Sectigo Public Server Authentication CA DV E36ecdsa-with-SHA384EC 256 bit
2根 CACN=Sectigo Public Server Authentication Root E46ecdsa-with-SHA384EC 384 bit

注:GitHub 的证书链全部使用 ECDSA 签名,没有 RSA 证书。这是现代 TLS 部署的趋势——ECDSA 证书更短(256 bit vs 2048 bit)、签名更快、握手数据量更小。

3.2 检查 OCSP Stapling#

openssl s_client -connect github.com:443 -servername github.com -status </dev/null

输出:

CONNECTED(00000003)
OCSP response: no response sent
---
Certificate chain
0 s:CN = github.com
i:C = GB, O = Sectigo Limited, CN = Sectigo Public Server Authentication CA DV E36
...

OCSP response: no response sent 表示服务器没有启用 OCSP Stapling。这意味着客户端需要自行向 OCSP 响应器查询证书吊销状态,会增加一个额外的网络请求。

注:OCSP Stapling 是服务器在 TLS 握手时主动附带 OCSP 响应的机制。启用后,客户端无需单独查询 OCSP 服务器,既减少延迟又保护隐私。在生产环境中,建议始终启用 OCSP Stapling(参考第九章的 Nginx/Apache 配置)。

虽然 GitHub 未启用 OCSP Stapling,但证书中包含了 OCSP 响应器的 URL:

# 提取证书中的 OCSP URI
openssl s_client -connect github.com:443 -servername github.com </dev/null 2>/dev/null | \
openssl x509 -noout -ocsp_uri
http://ocsp.sectigo.com

附.5 Python ssl 模块实践#

OpenSSL 命令行适合快速诊断,但编程接口更灵活。Python 的 ssl 模块封装了 OpenSSL,可以直接获取 TLS 连接的结构化信息。

4.1 TLS 1.3 连接#

import ssl, socket
# 创建 TLS 1.3 上下文
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.check_hostname = True
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.load_default_certs()
# 连接 github.com:443
with socket.create_connection(("github.com", 443)) as sock:
with ctx.wrap_socket(sock, server_hostname="github.com") as ssock:
print(f"协议版本: {ssock.version()}")
print(f"密码套件: {ssock.cipher()[0]}")
print(f"密钥位数: {ssock.cipher()[2]}")
cert = ssock.getpeercert()
print(f"主题: {dict(x[0] for x in cert['subject'])}")
print(f"签发者: {dict(x[0] for x in cert['issuer'])}")
print(f"SAN: {[x[1] for x in cert['subjectAltName']]}")
print(f"有效期: {cert['notBefore']} ~ {cert['notAfter']}")
print(f"压缩: {ssock.compression()}")

运行结果:

协议版本: TLSv1.3
密码套件: TLS_AES_128_GCM_SHA256
密钥位数: 128
主题: {'commonName': 'github.com'}
签发者: {'countryName': 'GB', 'organizationName': 'Sectigo Limited', 'commonName': 'Sectigo Public Server Authentication CA DV E36'}
SAN: ['github.com', 'www.github.com']
有效期: May 5 00:00:00 2026 GMT ~ Aug 2 23:59:59 2026 GMT
压缩: None

4.2 对比 TLS 1.2 连接#

# 创建 TLS 1.2 上下文
ctx12 = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx12.maximum_version = ssl.TLSVersion.TLSv1_2
ctx12.check_hostname = True
ctx12.verify_mode = ssl.CERT_REQUIRED
ctx12.load_default_certs()
with socket.create_connection(("github.com", 443)) as sock:
with ctx12.wrap_socket(sock, server_hostname="github.com") as ssock:
print(f"协议版本: {ssock.version()}")
print(f"密码套件: {ssock.cipher()[0]}")
print(f"压缩: {ssock.compression()}")

运行结果:

协议版本: TLSv1.2
密码套件: ECDHE-ECDSA-AES128-GCM-SHA256
压缩: None

注:Python ssl 模块通过 ctx.maximum_version 控制协议版本上限,通过 ctx.minimum_version 控制下限。ssl.TLSVersion.TLSv1_3ssl.TLSVersion.TLSv1_2 是 Python 3.7+ 引入的枚举值。

附.6 实践小结#

对比维度TLS 1.3TLS 1.2
握手 RTT1-RTT2-RTT
密码套件数量5 个(仅 AEAD)37 个(含不安全选项)
密码套件命名TLS_AES_128_GCM_SHA256ECDHE-ECDSA-AES128-GCM-SHA256
密钥交换仅 ECDHE(X25519)RSA 或 ECDHE
前向安全强制可选
Renegotiation不支持支持
Master-Key 暴露不暴露s_client 输出中可见
证书传输加密明文
OCSP Stapling支持(status_request 扩展)支持
0-RTT支持不支持
压缩禁止允许(但有 CRIME 风险)

通过 s_client 和 Python ssl 模块的实际操作,验证了 TLS 1.3 的核心改进:更少的密码套件、更简洁的命名、强制前向安全、移除 renegotiation、不暴露 Master-Key。这些不是纸面上的规范差异,而是每一次 s_client 连接都能观察到的真实变化。

十、总结#

上一章深入探讨了数字签名与证书链。

10.1 核心要点#

维度关键要点
握手1-RTT,ECDHE 强制前向安全
0-RTT重连时零延迟,但有重放风险
密码套件仅 5 个,全部 AEAD
密钥派生HKDF,从共享密钥逐层派生所有密钥
安全改进移除 RSA 密钥交换、CBC、SHA-1
配置TLS 1.3 + HSTS + OCSP Stapling
会话恢复Session Ticket + PSK + DHE
抓包分析SSLKEYLOGFILE + Wireshark

10.2 TLS 版本迁移清单#

从 TLS 1.2 迁移到 TLS 1.3 的步骤:

步骤操作验证方式
1审计现有 TLS 配置sslyzetestssl.sh 扫描
2更新服务器软件到支持 TLS 1.3 的版本Nginx ≥ 1.13.0, OpenSSL ≥ 1.1.1
3配置 ssl_protocols TLSv1.2 TLSv1.3保留 1.2 兼容
4配置 TLS 1.3 密码套件仅 AEAD 套件
5更新证书签名算法使用 RSA-PSS 或 ECDSA
6启用 HSTS 和 OCSP Stapling添加安全头
7灰度测试先在测试环境验证
8SSL Labs 扫描确认 A+ 评级
9监控错误率确认客户端兼容性
10逐步禁用 TLS 1.2根据业务需求决定时间

参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

TLS 1.3 握手详解
https://blog.souloss.com/posts/cryptography/tls1-3/
作者
Souloss
发布于
2026-03-23
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时