土法炼钢兴趣小组的算法知识备份

【网络工程】TLS 1.3 工程实践:1-RTT 与 0-RTT 的安全权衡

文章导航

分类入口
network
标签入口
#tls#tls13#0-rtt#psk#handshake#forward-secrecy#engineering

目录

2018 年 8 月,RFC 8446 正式发布了 TLS 1.3。这不是一次小修小补——它重新设计了握手流程,移除了大量历史包袱,将完整握手从 2 RTT 压缩到 1 RTT,并引入了 0-RTT 恢复模式。Cloudflare 在部署后报告,TLS 1.3 使 HTTPS 连接建立时间减少了约 30%。

但 TLS 1.3 不是”更快的 TLS 1.2”。它的设计哲学发生了根本变化——不再提供”选择不安全配置的自由”,而是强制安全。理解这些变化,对于工程师部署和升级 TLS 至关重要。

一、TLS 1.3 移除了什么

TLS 1.3 做的第一件事不是添加新功能,而是删除旧功能。RFC 8446 的作者 Eric Rescorla 明确表示:“TLS 1.3 的设计原则之一是减少攻击面。”

1.1 被移除的特性清单

被移除的特性 移除原因 对应的已知攻击
RSA 密钥交换 无前向保密 被动解密历史流量
CBC 模式加密 Padding Oracle BEAST、Lucky 13、POODLE
RC4 流加密 已被攻破 RC4 偏差攻击
3DES 太慢 + Sweet32 Sweet32 生日攻击
压缩 侧信道泄露 CRIME、BREACH
重新协商(Renegotiation) 协议复杂性 三重握手攻击
静态 RSA/DH 无前向保密
MD5/SHA-1 签名 哈希碰撞 SHAttered
DSA 签名 实现质量差
自定义 DHE 参数 弱参数风险 Logjam

1.2 仅保留的密码套件

TLS 1.3 只保留了 5 个密码套件,全部使用 AEAD(Authenticated Encryption with Associated Data)模式:

TLS 1.3 密码套件:

TLS_AES_128_GCM_SHA256           (0x1301)  ← 推荐默认
TLS_AES_256_GCM_SHA384           (0x1302)  ← 高安全场景
TLS_CHACHA20_POLY1305_SHA256     (0x1303)  ← 移动设备友好
TLS_AES_128_CCM_SHA256           (0x1304)  ← IoT 场景
TLS_AES_128_CCM_8_SHA256         (0x1305)  ← IoT 极低开销

注意命名变化:
  TLS 1.2: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
  TLS 1.3: TLS_AES_128_GCM_SHA256

  密钥交换和签名算法不再出现在密码套件名称中
  → 它们通过 supported_groups 和 signature_algorithms 扩展独立协商
# 查看 OpenSSL 支持的 TLS 1.3 密码套件
openssl ciphers -v -s -tls1_3
# TLS_AES_256_GCM_SHA384     TLSv1.3 Kx=any Au=any Enc=AESGCM(256) Mac=AEAD
# TLS_CHACHA20_POLY1305_SHA256 TLSv1.3 Kx=any Au=any Enc=CHACHA20/POLY1305(256) Mac=AEAD
# TLS_AES_128_GCM_SHA256     TLSv1.3 Kx=any Au=any Enc=AESGCM(128) Mac=AEAD

# 注意 Kx=any Au=any
# → 密钥交换和认证独立于密码套件

为什么 ChaCha20-Poly1305 对移动设备更友好?因为它不依赖 AES-NI 硬件指令。在没有 AES 硬件加速的 ARM 处理器上,ChaCha20 比 AES-GCM 快 3 倍以上。Google 在 Android Chrome 中优先选择 ChaCha20-Poly1305 就是这个原因。

1.3 密码套件性能对比

# 在不同硬件上测试加密算法性能
openssl speed -evp aes-128-gcm
openssl speed -evp aes-256-gcm
openssl speed -evp chacha20-poly1305

# 典型结果 (有 AES-NI 的 x86_64 服务器):
#                     16 bytes    64 bytes    256 bytes   1024 bytes  8192 bytes
# aes-128-gcm         5.8 GB/s    7.2 GB/s    7.8 GB/s    8.0 GB/s    8.1 GB/s
# aes-256-gcm         4.9 GB/s    6.1 GB/s    6.6 GB/s    6.8 GB/s    6.9 GB/s
# chacha20-poly1305   1.8 GB/s    2.3 GB/s    2.6 GB/s    2.7 GB/s    2.8 GB/s
# → 有 AES-NI 时,AES-GCM 远快于 ChaCha20

# 典型结果 (无 AES-NI 的 ARM Cortex-A53):
#                     16 bytes    64 bytes    256 bytes   1024 bytes  8192 bytes
# aes-128-gcm         85 MB/s     110 MB/s    130 MB/s    140 MB/s    145 MB/s
# chacha20-poly1305   280 MB/s    350 MB/s    390 MB/s    410 MB/s    420 MB/s
# → 无 AES-NI 时,ChaCha20 约 3 倍快于 AES-GCM

# 检查当前 CPU 是否支持 AES-NI
grep -o aes /proc/cpuinfo | head -1
# 有输出 → 支持 AES-NI → 优先 AES-GCM
# 无输出 → 不支持     → 优先 ChaCha20-Poly1305

选择建议:服务器端有 AES-NI 时,优先 AES-128-GCM(性能最优);客户端是移动设备或嵌入式设备时,服务器应允许客户端优先选择 ChaCha20-Poly1305(设置 ssl_prefer_server_ciphers off)。

二、TLS 1.3 完整握手:1-RTT

2.1 握手消息序列

客户端                                       服务器
  │                                            │
  │  ClientHello                               │
  │  + key_share (客户端 ECDHE 公钥)           │
  │  + supported_versions (0x0304)             │
  │  + signature_algorithms                    │
  │  + psk_key_exchange_modes (可选)           │
  ├────────────────────────────────────────────→│
  │                                            │
  │  ServerHello                               │
  │  + key_share (服务器 ECDHE 公钥)           │
  │  + supported_versions (0x0304)             │
  │←────────────────────────────────────────────┤
  │  {EncryptedExtensions}                     │
  │  {CertificateRequest} (可选, mTLS 时)      │
  │  {Certificate}                             │
  │  {CertificateVerify}                       │
  │  {Finished}                                │
  │←════════════════════════════════════════════┤
  │                                            │
  │  {Certificate} (可选, mTLS 时)             │
  │  {CertificateVerify} (可选, mTLS 时)       │
  │  {Finished}                                │
  ├════════════════════════════════════════════→│
  │                                            │
  │  [Application Data]                        │
  ╞════════════════════════════════════════════╡
  │                                            │

  {} = 加密的握手消息
  [] = 加密的应用数据

  总计: 1 RTT 即可开始发送应用数据

2.2 关键变化分析

密钥交换前移:在 TLS 1.2 中,客户端先发 ClientHello,等收到 ServerHello 后才知道服务器选择了哪条曲线,然后才能发送自己的公钥。TLS 1.3 的做法是客户端在 ClientHello 中就”猜测”服务器会选择的曲线(通过 key_share 扩展直接发送公钥)。

TLS 1.2 的问题:
  ClientHello → 等待 → ServerHello → 等待 → ClientKeyExchange
  (客户端不知道服务器选什么曲线,无法提前计算)

TLS 1.3 的解法:
  ClientHello + key_share(X25519) → ServerHello + key_share(X25519)
  (客户端猜 X25519,猜对了 → 1 RTT)
  (客户端猜错了 → HelloRetryRequest → 2 RTT)

证书加密:TLS 1.2 中服务器的 Certificate 消息是明文传输的——任何网络观察者都能看到服务器发送了什么证书。TLS 1.3 在 ServerHello 之后就开始加密,Certificate 消息被加密传输。

CertificateVerify 消息:这是 TLS 1.3 新增的消息。服务器用自己的私钥对之前所有握手消息的哈希进行签名,证明它确实持有证书对应的私钥。TLS 1.2 中 RSA 密钥交换模式下没有这一步——服务器通过解密 Pre-Master Secret 隐式证明身份。

2.3 HelloRetryRequest

如果客户端的 key_share 中没有服务器支持的曲线,服务器会发送 HelloRetryRequest(HRR),要求客户端重新发送 ClientHello:

客户端                                       服务器
  │                                            │
  │  ClientHello                               │
  │  + key_share (P-384)                       │
  ├────────────────────────────────────────────→│
  │                                            │
  │                        服务器只支持 X25519  │
  │                                            │
  │  HelloRetryRequest                         │
  │  + key_share (X25519)                      │
  │←────────────────────────────────────────────┤
  │                                            │
  │  ClientHello (重试)                        │
  │  + key_share (X25519)                      │
  ├────────────────────────────────────────────→│
  │                                            │
  │  正常 ServerHello + 后续消息               │
  │←════════════════════════════════════════════┤

  此时退化为 2 RTT

实际上 HRR 很少发生,因为客户端通常会在 key_share 中同时提供 X25519 和 P-256 的公钥。

# 用 tshark 观察 TLS 1.3 握手
tshark -i eth0 -f 'tcp port 443' \
  -Y 'tls.handshake.type' \
  -T fields \
  -e frame.time_relative \
  -e ip.src \
  -e tls.handshake.type \
  -e tls.handshake.extensions.supported_version

# 同时发起 TLS 1.3 连接
curl --tlsv1.3 -v https://example.com 2>/dev/null

# 输出示例 (type 字段):
# 0.000  192.168.1.100  1       0x0304  (ClientHello, TLS 1.3)
# 0.031  93.184.216.34  2       0x0304  (ServerHello, TLS 1.3)
# 0.031  93.184.216.34  8,11,15,20      (加密的 Extensions+Cert+CertVerify+Finished)
# 0.033  192.168.1.100  20              (客户端 Finished)

三、TLS 1.3 密钥推导:HKDF

TLS 1.2 使用自定义的 PRF(伪随机函数)推导密钥。TLS 1.3 替换为标准化的 HKDF(HMAC-based Key Derivation Function,RFC 5869),分为 Extract 和 Expand 两步。

3.1 密钥推导调度

                0
                │
                v
  PSK ──→ HKDF-Extract = Early Secret
                │
                v
           Derive-Secret(., "derived", "")
                │
                v
 (EC)DHE ──→ HKDF-Extract = Handshake Secret ──→ 握手流量密钥
                │                                  (加密 Certificate/Finished)
                v
           Derive-Secret(., "derived", "")
                │
                v
   0 ───→ HKDF-Extract = Master Secret ──→ 应用流量密钥
                                             (加密 Application Data)

每一级 Secret 通过 Derive-Secret 派生出:
  - client_handshake_traffic_secret
  - server_handshake_traffic_secret
  - client_application_traffic_secret
  - server_application_traffic_secret

3.2 与 TLS 1.2 密钥推导的对比

特性 TLS 1.2 TLS 1.3
推导函数 自定义 PRF HKDF(标准化)
输入材料 Pre-Master Secret + Randoms PSK + (EC)DHE
密钥层级 单层(Master Secret → Key Block) 三层(Early/Handshake/Master)
握手加密 无(握手消息明文) 有(ServerHello 后加密)
密钥更新 需要重新协商 KeyUpdate 消息
# 使用 OpenSSL 导出 TLS 1.3 的密钥日志
# 设置 SSLKEYLOGFILE 环境变量
export SSLKEYLOGFILE=/tmp/tls13-keys.log
curl https://example.com >/dev/null 2>&1

# 查看密钥日志
cat /tmp/tls13-keys.log
# CLIENT_HANDSHAKE_TRAFFIC_SECRET <client_random> <secret>
# SERVER_HANDSHAKE_TRAFFIC_SECRET <client_random> <secret>
# CLIENT_TRAFFIC_SECRET_0 <client_random> <secret>
# SERVER_TRAFFIC_SECRET_0 <client_random> <secret>
# EXPORTER_SECRET <client_random> <secret>

# 在 Wireshark 中使用密钥日志解密 TLS 1.3 流量:
# Edit → Preferences → Protocols → TLS
# → (Pre)-Master-Secret log filename: /tmp/tls13-keys.log

3.3 KeyUpdate:无中断密钥轮换

TLS 1.3 引入了 KeyUpdate 消息,允许在不重新握手的情况下更新应用数据的加密密钥。这在长连接场景中特别有用——例如 HTTP/2 的长时间复用连接。

KeyUpdate 流程:

  发送方                          接收方
    │                               │
    ├── KeyUpdate(update_requested) ─→│
    │   (更新自己的发送密钥)          │ (更新对方的接收密钥)
    │                               │
    │  如果 update_requested:        │
    │←── KeyUpdate(update_not_requested) ──┤
    │   (更新对方的接收密钥)          │ (更新自己的发送密钥)
    │                               │

新密钥 = HKDF-Expand-Label(旧密钥, "traffic upd", "", Hash.length)

四、0-RTT 恢复模式

4.1 0-RTT 的工作原理

0-RTT(Zero Round Trip Time)允许客户端在发送 ClientHello 的同时就附带加密的应用数据,不需要等待服务器响应。这是 TLS 1.3 最引人注目的性能优化。

客户端                                       服务器
  │                                            │
  │  ClientHello                               │
  │  + early_data                              │
  │  + key_share                               │
  │  + psk_identity                            │
  │  (Application Data)  ← 0-RTT 数据,加密    │
  ├════════════════════════════════════════════→│
  │                                            │
  │  ServerHello                               │
  │  + pre_shared_key                          │
  │  {EncryptedExtensions}                     │
  │  + early_data (接受 0-RTT)                 │
  │  {Finished}                                │
  │←════════════════════════════════════════════┤
  │                                            │
  │  {Finished}                                │
  │  [Application Data]  ← 1-RTT 数据         │
  ├════════════════════════════════════════════→│

  0-RTT 数据在第一个网络包就发出去了
  → 对于 HTTPS GET 请求,响应可以更快开始

4.2 0-RTT 的前提条件

0-RTT 不是免费的——它需要满足严格的前提条件:

前提条件:
  1. 客户端和服务器之前有过一次完整握手
     → 客户端持有有效的 PSK(Pre-Shared Key)
     → PSK 来自上次握手后服务器发送的 NewSessionTicket

  2. PSK 未过期
     → 服务器在 NewSessionTicket 中设定了 ticket_lifetime
     → RFC 8446 限制最大为 604800 秒(7 天)

  3. 使用相同的密码套件
     → 0-RTT 数据使用 Early Secret 加密
     → Early Secret 来自 PSK + 上次握手的参数

  4. 服务器选择接受 0-RTT 数据
     → 服务器可以拒绝 0-RTT(返回不含 early_data 扩展)
     → 客户端必须准备好重新发送被拒绝的 0-RTT 数据

  5. 应用层协议支持
     → HTTP/2 和 HTTP/3 支持 0-RTT
     → 应用必须能处理 Early-Data 头标记

4.3 0-RTT 的重放攻击风险

0-RTT 模式有一个根本性的安全问题:没有前向保密,且无法防止重放攻击。

重放攻击场景:

  1. 客户端发送 0-RTT 请求: POST /transfer?amount=1000
  2. 攻击者捕获了这个网络包
  3. 攻击者重放这个包 → 服务器再次处理转账

  为什么 0-RTT 无法防重放:
  - 在 1-RTT 握手中,服务器的 ServerHello.random 确保每次握手唯一
  - 在 0-RTT 中,客户端在收到服务器响应前就发送了数据
  - 服务器无法提供"一次性"的随机数来防止重放

  对比:
  1-RTT: ClientHello → ServerHello(fresh random) → 数据
         每次 ServerHello.random 不同 → 自然防重放
  0-RTT: ClientHello + 数据 →
         没有 ServerHello → 没有 fresh random → 可重放

4.4 0-RTT 的安全使用指南

安全原则: 只对幂等(Idempotent)操作使用 0-RTT

  ✓ 安全(幂等操作):
    GET /index.html          ← 读取操作
    GET /api/user/profile    ← 查询操作
    HEAD /health             ← 健康检查

  ✗ 不安全(非幂等操作):
    POST /api/transfer       ← 转账
    POST /api/order          ← 下单
    PUT /api/counter/incr    ← 计数器递增
    DELETE /api/resource/123 ← 删除操作

服务器端的防重放策略:

策略一: 时间窗口限制
  - 服务器只接受最近 N 秒内的 0-RTT 数据
  - 超过窗口的 0-RTT 数据被拒绝
  - 但窗口内仍可能被重放

策略二: Strike Register(一次性令牌)
  - 服务器记录已处理的 0-RTT ClientHello 的标识
  - 如果相同的标识再次出现 → 拒绝
  - 代价: 需要跨服务器共享状态

策略三: 单次使用 Session Ticket
  - 每个 NewSessionTicket 只允许用于一次 0-RTT
  - 使用后立即作废
  - 代价: 无法跨多台服务器去重

最佳实践:
  - 应用层做幂等性保护(最可靠)
  - 服务器配合时间窗口限制
  - 关键业务接口禁用 0-RTT
# Nginx 配置 0-RTT
# nginx.conf
# ssl_early_data on;
# proxy_set_header Early-Data $ssl_early_data;
#
# 后端服务根据 Early-Data 头判断:
# Early-Data: 1 → 这是 0-RTT 数据,需要额外的幂等性检查

# 测试 0-RTT 是否生效
# 需要两次连接: 第一次建立 PSK,第二次使用 0-RTT
openssl s_client -connect example.com:443 \
  -tls1_3 -sess_out /tmp/tls13-sess.pem \
  </dev/null 2>/dev/null

openssl s_client -connect example.com:443 \
  -tls1_3 -sess_in /tmp/tls13-sess.pem \
  -early_data /tmp/early-data.txt 2>&1 | \
  grep -E "Early data|Reused"
# Early data was accepted
# Reused, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384

五、PSK 模式

5.1 基于 Resumption 的 PSK

TLS 1.3 用 PSK(Pre-Shared Key)统一了 TLS 1.2 中的 Session ID 和 Session Ticket 两种恢复机制。

NewSessionTicket 流程:

握手完成后 (任意时刻):

  服务器 → 客户端: NewSessionTicket
    {
      ticket_lifetime: 3600,        // 秒
      ticket_age_add: 0x12345678,   // 混淆客户端的 ticket_age
      ticket_nonce: 0x01,           // 用于派生 PSK
      ticket: <加密的 ticket 数据>,
      extensions: {
        max_early_data_size: 16384  // 0-RTT 最大数据量
      }
    }

  客户端存储:
    PSK = HKDF-Expand-Label(resumption_master_secret,
                            "resumption",
                            ticket_nonce,
                            Hash.length)
    + ticket_lifetime
    + ticket_age_add
    + ticket 数据本身

5.2 外部 PSK

除了基于恢复的 PSK,TLS 1.3 还支持外部预共享密钥(External PSK),即双方通过带外方式预先配置的密钥。

外部 PSK 的场景:
  - IoT 设备: 出厂预配置密钥
  - 企业内网: 通过安全渠道分发密钥
  - 特殊安全要求: 不信任 PKI 体系

外部 PSK 的模式:
  psk_ke:      仅 PSK(无前向保密)
  psk_dhe_ke:  PSK + ECDHE(有前向保密) ← 推荐

配置示例 (OpenSSL 3.x):
  openssl s_server -psk 0123456789abcdef \
    -psk_identity client1 -tls1_3 -nocert
  openssl s_client -psk 0123456789abcdef \
    -psk_identity client1 -tls1_3

5.3 PSK 与密钥交换模式

模式 前向保密 0-RTT 适用场景
仅 ECDHE 首次连接
PSK + ECDHE 恢复连接(推荐)
仅 PSK IoT 资源受限(不推荐)

需要注意的是,即使使用 PSK + ECDHE 模式(推荐),0-RTT 数据仍然只用 PSK 派生的 Early Secret 加密,没有 ECDHE 的前向保密保护。ECDHE 的前向保密只在 1-RTT 及后续的数据传输中生效。这也是 0-RTT 安全性低于 1-RTT 的另一个原因。

六、版本协商与向下兼容

6.1 supported_versions 扩展

TLS 1.3 改变了版本协商的方式。在 TLS 1.2 及之前,版本在 ClientHello 和 ServerHello 的 ProtocolVersion 字段中协商。TLS 1.3 引入了 supported_versions 扩展。

版本协商的变化:

TLS 1.2:
  ClientHello.version = 0x0303 (TLS 1.2)
  ServerHello.version = 0x0303 (TLS 1.2)

TLS 1.3:
  ClientHello.version = 0x0303 (伪装成 TLS 1.2!)
  + supported_versions 扩展: [0x0304, 0x0303]

  ServerHello.version = 0x0303 (伪装成 TLS 1.2!)
  + supported_versions 扩展: 0x0304

为什么伪装?
  大量中间设备(防火墙、IDS、代理)会检查 TLS 版本字段
  如果看到不认识的版本号(如 0x0304)就会丢包
  TLS 1.3 通过保持 0x0303 骗过这些中间设备
  真正的版本在扩展中协商

这个问题被称为 "中间设备僵化"(Middlebox Ossification)
  是 TLS 1.3 部署中最大的工程挑战之一

6.2 中间设备兼容性

TLS 1.3 为了兼容中间设备做的妥协:

1. 版本号伪装(如上所述)

2. ChangeCipherSpec 兼容
   TLS 1.3 协议本身不使用 ChangeCipherSpec
   但在 "compatibility mode" 下仍然发送一个假的 CCS
   以避免中间设备因"缺少 CCS"而断开连接

3. ServerHello.random 的特殊值
   如果服务器选择降级到 TLS 1.2,ServerHello.random 的最后 8 字节
   必须设为 0x44 0x4F 0x57 0x4E 0x47 0x52 0x44 0x01
   ("DOWNGRD" + 0x01)
   客户端检测到这个值 → 知道发生了降级,可以中止握手
# 检查服务器是否支持 TLS 1.3
openssl s_client -connect example.com:443 \
  -tls1_3 2>&1 | grep -E "Protocol|Cipher"
# Protocol  : TLSv1.3
# Cipher    : TLS_AES_256_GCM_SHA384

# 如果不支持 TLS 1.3:
# Protocol  : TLSv1.2
# → 自动降级到 TLS 1.2

# 查看 supported_versions 扩展
tshark -i eth0 -f 'tcp port 443' \
  -Y 'tls.handshake.type == 1' \
  -T fields \
  -e tls.handshake.extensions.supported_version

# 输出: 0x0304,0x0303
# → 客户端支持 TLS 1.3 和 TLS 1.2

七、从 TLS 1.2 升级到 TLS 1.3 的工程路径

7.1 升级前的检查清单

#!/bin/bash
# tls13_readiness.sh — TLS 1.3 升级就绪检查
HOST="${1:?Usage: $0 hostname}"
PORT="${2:-443}"

echo "=== TLS 1.3 升级就绪检查: $HOST:$PORT ==="

# 1. 检查 OpenSSL 版本
echo ""
echo "--- OpenSSL 版本 ---"
openssl version
# 需要 OpenSSL 1.1.1+ 才支持 TLS 1.3

# 2. 检查当前 TLS 配置
echo ""
echo "--- 当前 TLS 版本 ---"
echo | openssl s_client -connect "$HOST:$PORT" \
  -servername "$HOST" 2>/dev/null | \
  grep "Protocol"

# 3. 测试 TLS 1.3 连接
echo ""
echo "--- TLS 1.3 支持 ---"
result=$(echo | openssl s_client -connect "$HOST:$PORT" \
  -servername "$HOST" -tls1_3 2>&1)
if echo "$result" | grep -q "Protocol.*TLSv1.3"; then
  echo "  TLS 1.3: ✓ 已支持"
  echo "  密码套件: $(echo "$result" | grep "Cipher" | head -1)"
else
  echo "  TLS 1.3: ✗ 不支持"
fi

# 4. 检查密码套件兼容性
echo ""
echo "--- 密码套件检查 ---"
echo | openssl s_client -connect "$HOST:$PORT" \
  -servername "$HOST" 2>/dev/null | \
  grep "Cipher"
# 确保不依赖 TLS 1.3 中已移除的套件

# 5. 检查证书签名算法
echo ""
echo "--- 证书签名算法 ---"
echo | openssl s_client -connect "$HOST:$PORT" \
  -servername "$HOST" 2>/dev/null | \
  openssl x509 -noout -text 2>/dev/null | \
  grep "Signature Algorithm" | head -1
# SHA-1 签名的证书在 TLS 1.3 中不被接受

7.2 Nginx 升级配置

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # 同时支持 TLS 1.2 和 1.3(过渡期)
    ssl_protocols TLSv1.2 TLSv1.3;

    # TLS 1.2 的密码套件(仅 ECDHE + GCM)
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
    # TLS 1.3 密码套件由 OpenSSL 自动选择,无需手动配置

    # 服务器优先选择
    ssl_prefer_server_ciphers off;
    # 注意: TLS 1.3 中此选项无效
    # TLS 1.3 的密码套件顺序始终由客户端决定

    # ECDH 曲线
    ssl_ecdh_curve X25519:P-256:P-384;

    # 0-RTT(评估风险后启用)
    ssl_early_data on;
    # 传递 0-RTT 标记给后端
    proxy_set_header Early-Data $ssl_early_data;

    # Session Ticket
    ssl_session_tickets on;
    ssl_session_timeout 1d;
    # 确保 Ticket 密钥定期轮换
    # ssl_session_ticket_key /etc/nginx/ticket-keys/current.key;
    # ssl_session_ticket_key /etc/nginx/ticket-keys/previous.key;
}

7.3 常见升级问题

问题 症状 解决方案
中间设备阻断 TLS 1.3 握手超时 检查防火墙/IDS 是否支持 TLS 1.3
OpenSSL 版本太旧 不支持 -tls1_3 参数 升级到 OpenSSL 1.1.1+
客户端不兼容 老版本浏览器/SDK 无法连接 保持 TLS 1.2 兼容
Java 客户端 Java 8 不支持 TLS 1.3 升级到 Java 11+
企业代理 SSL Inspection 设备不认识 TLS 1.3 升级代理固件或旁路
0-RTT 重放 POST 请求被重复执行 禁用 0-RTT 或做幂等保护
# 检查客户端 TLS 1.3 支持情况

# Python
python3 -c "import ssl; print(ssl.OPENSSL_VERSION)"
# 需要 OpenSSL 1.1.1+

# curl
curl --version | grep TLS
# curl 7.61.0+ 支持 TLS 1.3

# Go
# Go 1.12+ 默认启用 TLS 1.3
# tls.Config{MinVersion: tls.VersionTLS13}

# Java
java -version
# Java 11+ 原生支持 TLS 1.3
# Java 8u261+ 部分支持(需要手动启用)

八、TLS 1.2 vs 1.3 完整对比

维度 TLS 1.2 TLS 1.3
RFC RFC 5246(2008) RFC 8446(2018)
握手 RTT 2 RTT(完整) / 1 RTT(恢复) 1 RTT(完整) / 0-RTT(恢复)
密码套件数量 37+ 种(含不安全的) 5 种(全部安全)
密钥交换 RSA / DHE / ECDHE 仅 (EC)DHE + PSK
前向保密 可选(取决于密钥交换) 强制
握手加密 无(明文传输证书) 有(ServerHello 后加密)
密钥推导 自定义 PRF HKDF(标准化)
密钥更新 重新协商 KeyUpdate 消息
会话恢复 Session ID / Ticket PSK(统一机制)
0-RTT 不支持 支持(有重放风险)
版本协商 ProtocolVersion 字段 supported_versions 扩展
压缩 支持但应禁用 已移除
重新协商 支持 已移除

九、性能测试

TLS 版本的性能差异主要体现在握手阶段。一旦握手完成,数据传输阶段的性能几乎相同(使用相同的 AEAD 算法)。

9.1 握手延迟对比

# 对比 TLS 1.2 和 TLS 1.3 的握手延迟

# TLS 1.2 握手延迟
for i in $(seq 1 10); do
  start=$(date +%s%N)
  echo | openssl s_client -connect example.com:443 \
    -tls1_2 -no_ticket 2>/dev/null >/dev/null
  end=$(date +%s%N)
  echo "TLS 1.2: $(( (end - start) / 1000000 ))ms"
done

# TLS 1.3 握手延迟
for i in $(seq 1 10); do
  start=$(date +%s%N)
  echo | openssl s_client -connect example.com:443 \
    -tls1_3 2>/dev/null >/dev/null
  end=$(date +%s%N)
  echo "TLS 1.3: $(( (end - start) / 1000000 ))ms"
done

# 使用 curl 测量完整的 HTTPS 请求时间
# TLS 1.2
curl -o /dev/null -s -w "\
  DNS:        %{time_namelookup}s\n\
  TCP:        %{time_connect}s\n\
  TLS:        %{time_appconnect}s\n\
  TTFB:       %{time_starttransfer}s\n\
  Total:      %{time_total}s\n" \
  --tlsv1.2 --tls-max 1.2 https://example.com

# TLS 1.3
curl -o /dev/null -s -w "\
  DNS:        %{time_namelookup}s\n\
  TCP:        %{time_connect}s\n\
  TLS:        %{time_appconnect}s\n\
  TTFB:       %{time_starttransfer}s\n\
  Total:      %{time_total}s\n" \
  --tlsv1.3 https://example.com

# 典型结果 (RTT=30ms 的场景):
# TLS 1.2: TLS 握手 ≈ 90ms  (3 × RTT, 含 TCP 握手)
# TLS 1.3: TLS 握手 ≈ 60ms  (2 × RTT, 含 TCP 握手)
# 节省约 30ms (一个 RTT)

9.2 吞吐量对比

数据传输阶段,TLS 1.2 和 1.3 使用相同的 AEAD 算法时,吞吐量几乎没有差异:

# 使用 openssl s_time 测试吞吐量
# TLS 1.2
openssl s_time -connect example.com:443 \
  -tls1_2 -time 10 2>/dev/null | tail -3
# 1234 connections in 10.00s; 123.4 connections/s

# TLS 1.3
openssl s_time -connect example.com:443 \
  -tls1_3 -time 10 2>/dev/null | tail -3
# 1567 connections in 10.00s; 156.7 connections/s
# → TLS 1.3 的连接建立速率更高(因为握手更快)
# → 但单连接的数据传输速率相同

十、总结

TLS 1.3 不是 TLS 1.2 的简单优化,而是一次安全理念的升级:

  1. “移除”比”添加”更重要。 TLS 1.3 的安全提升主要来自移除不安全的选项——RSA 密钥交换、CBC 模式、RC4、压缩、重新协商。当你无法选择不安全的配置时,系统整体的安全水位就提高了。这也是 TLS 1.3 只保留 5 个密码套件的设计哲学。

  2. 1 RTT 的节省来自”乐观猜测”。 客户端在 ClientHello 中就发送 ECDHE 公钥(猜测服务器会接受 X25519),如果猜对了就省下一个 RTT。这个优化在高延迟网络(RTT > 100ms)中效果显著——30% 以上的连接建立时间缩短。

  3. 0-RTT 是性能与安全的权衡。 它能在第一个包就发送加密数据,但代价是无法防止重放攻击。工程上的建议很简单:只对幂等操作使用 0-RTT,非幂等操作必须在应用层做幂等保护。

  4. 中间设备僵化是最大的部署障碍。 TLS 1.3 不得不通过”伪装成 TLS 1.2”来绕过不支持新协议的防火墙和代理。这个教训告诉我们:协议的可扩展性不只取决于协议本身的设计,还取决于中间设备的实现质量。

  5. 升级路径是渐进的。 不需要立即禁用 TLS 1.2——同时启用 TLS 1.2 和 1.3,支持 TLS 1.3 的客户端自动使用新协议,不支持的客户端降级到 TLS 1.2。当 TLS 1.2 流量占比降到足够低时,再考虑禁用。

下一篇我们进入证书工程——PKI 体系、ACME 协议与 Let’s Encrypt 的自动化证书管理,以及证书过期导致宕机的防范策略。


上一篇:TLS 1.2 握手完整解剖:从 ClientHello 到 Application Data

下一篇:证书工程:PKI 体系、ACME 与自动化管理

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2025-08-02 · network

【网络工程】TLS 1.2 握手完整解剖:从 ClientHello 到 Application Data

TLS 1.2 握手是 HTTPS 的基础机制。本文逐包分析完整握手的每一步——ClientHello 的密码套件协商、ServerHello 的参数选择、证书验证、密钥交换(RSA vs ECDHE)的安全差异,以及 ChangeCipherSpec 到 Application Data 的完整流程。结合 Wireshark 抓包和 OpenSSL 命令实操。

2025-08-08 · network

【网络工程】TLS 攻防:降级攻击、中间人与协议漏洞

TLS 协议在过去二十年里暴露了一系列严重漏洞。本文从攻击原理到防御工程,系统性剖析 BEAST、POODLE、Heartbleed、DROWN 等经典攻击的技术本质,详解版本降级攻击与 TLS_FALLBACK_SCSV 防御、CBC padding oracle 攻击的数学原理、以及生产环境的 TLS 安全配置检查清单。


By .