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

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

文章导航

分类入口
network
标签入口
#tls#tls12#handshake#ecdhe#rsa#cipher-suite#wireshark

目录

每一个 HTTPS 请求的背后都有一次 TLS 握手。TLS 1.2 自 2008 年 RFC 5246 发布以来,至今仍然是互联网上使用最广泛的 TLS 版本——尽管 TLS 1.3 正在快速普及,但大量存量系统、企业内部服务和老旧设备仍然依赖 TLS 1.2。

理解 TLS 1.2 握手不仅是面试考点,更是排查线上 TLS 问题的必备技能。证书错误、密码套件不匹配、握手超时——这些问题都需要你理解握手的每一步才能准确定位。

本文从 Wireshark 抓包的视角,逐消息分析 TLS 1.2 的完整握手流程。

一、TLS 握手的目标

TLS 握手需要完成三个核心任务:

1. 认证(Authentication)
   → 客户端验证服务器的身份(通过证书)
   → 可选: 服务器验证客户端的身份(mTLS)

2. 密钥协商(Key Exchange)
   → 双方协商出一个共享的对称密钥
   → 该密钥用于后续的加密通信
   → 密钥必须对窃听者不可知(前向保密)

3. 参数协商(Parameter Negotiation)
   → 协商 TLS 版本
   → 协商密码套件(加密算法组合)
   → 协商压缩方法(TLS 1.2 已弃用压缩)

二、完整握手流程

2.1 握手消息序列

客户端                                     服务器
  │                                          │
  ├──── ClientHello ─────────────────────────→│  第 1 步
  │                                          │
  │←──── ServerHello ────────────────────────┤  第 2 步
  │←──── Certificate ────────────────────────┤  第 3 步
  │←──── ServerKeyExchange ──────────────────┤  第 4 步(ECDHE 时)
  │←──── ServerHelloDone ────────────────────┤  第 5 步
  │                                          │
  ├──── ClientKeyExchange ───────────────────→│  第 6 步
  ├──── [ChangeCipherSpec] ──────────────────→│  第 7 步
  ├──── Finished ────────────────────────────→│  第 8 步
  │                                          │
  │←──── [ChangeCipherSpec] ─────────────────┤  第 9 步
  │←──── Finished ──────────────────────────┤  第 10 步
  │                                          │
  ├════ Application Data(加密)══════════════╡  数据传输
  │                                          │

总计: 2 RTT 才能开始传输应用数据

2.2 握手时序图

sequenceDiagram
    participant C as 客户端
    participant S as 服务器
    
    Note over C,S: ── 第 1 个 RTT ──
    C->>S: ClientHello (版本/Random/密码套件/SNI)
    S->>C: ServerHello (版本/Random/选定套件)
    S->>C: Certificate (证书链)
    S->>C: ServerKeyExchange (ECDHE 公钥 + 签名)
    S->>C: ServerHelloDone
    
    Note over C,S: ── 第 2 个 RTT ──
    Note over C: 验证证书链<br/>生成 ECDH 密钥对
    C->>S: ClientKeyExchange (ECDHE 公钥)
    C->>S: [ChangeCipherSpec]
    C->>S: Finished (加密的验证数据)
    
    S->>C: [ChangeCipherSpec]
    S->>C: Finished (加密的验证数据)
    
    Note over C,S: ══ Application Data (加密) ══

注意 ChangeCipherSpec 不属于握手消息(Handshake),而是一个独立的记录类型(Record Type = 20)。它的唯一作用是通知对方:“从现在开始,我发送的所有消息都用刚协商的密钥加密。”

2.3 用 Wireshark 抓包观察

# 抓取 TLS 握手包
# 方法一: tcpdump 抓取后用 Wireshark 分析
tcpdump -i eth0 -w tls-handshake.pcap \
  'tcp port 443 and host example.com' \
  -c 50

# 方法二: 使用 tshark 直接解析
tshark -i eth0 -f 'tcp port 443' \
  -Y 'tls.handshake' \
  -T fields \
  -e frame.time_relative \
  -e ip.src \
  -e ip.dst \
  -e tls.handshake.type

# 同时在另一个终端发起连接
curl -v https://example.com

# tshark 输出示例:
# 0.000000  192.168.1.100  93.184.216.34  1    (ClientHello)
# 0.032145  93.184.216.34  192.168.1.100  2    (ServerHello)
# 0.032200  93.184.216.34  192.168.1.100  11   (Certificate)
# 0.032250  93.184.216.34  192.168.1.100  12   (ServerKeyExchange)
# 0.032300  93.184.216.34  192.168.1.100  14   (ServerHelloDone)
# 0.035100  192.168.1.100  93.184.216.34  16   (ClientKeyExchange)
# 0.035200  192.168.1.100  93.184.216.34  20   (Finished)
# 0.068000  93.184.216.34  192.168.1.100  20   (Finished)

三、ClientHello 详解

3.1 ClientHello 消息结构

# 使用 openssl 查看 ClientHello 的详细内容
openssl s_client -connect example.com:443 -debug -msg 2>&1 | head -50

# ClientHello 包含的关键字段:
ClientHello 消息:
┌─────────────────────────────────────┐
│ Protocol Version: TLS 1.2 (0x0303) │
├─────────────────────────────────────┤
│ Random: 32 字节随机数               │
│   (前 4 字节是 Unix 时间戳,        │
│    后 28 字节是随机数据)             │
├─────────────────────────────────────┤
│ Session ID: 变长(0-32 字节)       │
│   (空 = 新连接,非空 = 尝试恢复)    │
├─────────────────────────────────────┤
│ Cipher Suites: 客户端支持的密码套件列表 │
│   TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 │
│   TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 │
│   TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 │
│   TLS_RSA_WITH_AES_128_GCM_SHA256   │
│   ...                               │
├─────────────────────────────────────┤
│ Compression Methods: [null]         │
│   (TLS 1.2 已弃用压缩,避免 CRIME)  │
├─────────────────────────────────────┤
│ Extensions:                         │
│   server_name (SNI): example.com    │
│   supported_groups: x25519, P-256   │
│   signature_algorithms: ...         │
│   ec_point_formats: uncompressed    │
│   session_ticket: (空 = 请求新票据)  │
│   ...                               │
└─────────────────────────────────────┘

3.2 SNI(Server Name Indication)

SNI 的工程意义:

问题: 一台服务器可能托管多个 HTTPS 网站
     (不同域名共享同一 IP)
     但 TLS 握手发生在 HTTP 请求之前
     服务器不知道客户端要访问哪个域名
     → 不知道该返回哪个证书

解决: 客户端在 ClientHello 中明文发送目标域名(SNI)
     服务器根据 SNI 选择对应的证书

安全问题:
  SNI 是明文的——网络观察者可以看到你要访问的域名
  即使 DNS 用了 DoH/DoT,SNI 仍然暴露目标
  → TLS 1.3 的 ECH(Encrypted Client Hello)解决此问题

3.3 密码套件命名规则

TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
 │    │     │         │   │   │    │
 │    │     │         │   │   │    └─ PRF 哈希算法
 │    │     │         │   │   └────── 加密模式
 │    │     │         │   └────────── 密钥长度
 │    │     │         └────────────── 对称加密算法
 │    │     └──────────────────────── 认证算法(证书类型)
 │    └────────────────────────────── 密钥交换算法
 └─────────────────────────────────── 协议

完整解读:
  ECDHE    — 使用椭圆曲线 Diffie-Hellman 临时密钥交换
  RSA      — 使用 RSA 证书进行服务器认证
  AES_128  — 使用 128 位 AES 对称加密
  GCM      — Galois/Counter Mode(认证加密)
  SHA256   — PRF 使用 SHA-256

3.4 推荐的密码套件

# 查看 OpenSSL 支持的密码套件
openssl ciphers -v 'HIGH:!aNULL:!MD5' | head -20

# 2025 年推荐的 TLS 1.2 密码套件(优先级从高到低):
# 1. TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384  (ECDSA 证书)
# 2. TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256  (ECDSA 证书)
# 3. TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384    (RSA 证书)
# 4. TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256    (RSA 证书)

# 必须禁用的:
# ✗ 任何使用 RC4 的套件(已被攻破)
# ✗ 任何使用 3DES 的套件(太慢且 Sweet32 攻击)
# ✗ 任何使用 CBC 模式的套件(Padding Oracle 攻击)
# ✗ 任何不使用 ECDHE/DHE 的套件(无前向保密)
# ✗ 任何使用 MD5/SHA-1 的套件(哈希碰撞风险)

四、ServerHello 与证书

4.1 ServerHello

ServerHello 消息:
┌─────────────────────────────────────┐
│ Protocol Version: TLS 1.2 (0x0303) │
├─────────────────────────────────────┤
│ Random: 32 字节服务器随机数          │
├─────────────────────────────────────┤
│ Session ID: 32 字节                 │
│   (新会话 ID 或复用客户端提供的)     │
├─────────────────────────────────────┤
│ Selected Cipher Suite:              │
│   TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 │
│   (从客户端列表中选择一个)           │
├─────────────────────────────────────┤
│ Compression Method: null            │
├─────────────────────────────────────┤
│ Extensions:                         │
│   renegotiation_info                │
│   session_ticket                    │
│   ...                               │
└─────────────────────────────────────┘

4.2 Certificate 消息

# 服务器发送证书链
# Certificate 消息包含:
#   1. 服务器证书(叶子证书)
#   2. 中间 CA 证书(一个或多个)
#   3. 不包含根 CA 证书(客户端本地已有)

# 查看服务器证书链
openssl s_client -connect example.com:443 -showcerts 2>/dev/null | \
  grep -E "s:|i:"
# s: = Subject (证书持有者)
# i: = Issuer (签发者)
#
# 0 s:CN = example.com
#   i:C = US, O = DigiCert Inc, CN = DigiCert TLS RSA SHA256 2020 CA1
# 1 s:C = US, O = DigiCert Inc, CN = DigiCert TLS RSA SHA256 2020 CA1
#   i:C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA

# 查看证书详细信息
openssl s_client -connect example.com:443 2>/dev/null | \
  openssl x509 -noout -text | head -30

4.3 证书验证过程

客户端验证证书的步骤:

1. 构建证书链
   服务器证书 → 中间 CA → 根 CA
   验证每一级的签名是否正确

2. 检查有效期
   notBefore ≤ 当前时间 ≤ notAfter
   → 过期或未生效的证书拒绝

3. 检查域名匹配
   证书的 CN 或 SAN(Subject Alternative Name)
   必须与请求的域名匹配
   支持通配符: *.example.com 匹配 www.example.com
   但不匹配 sub.www.example.com

4. 检查吊销状态
   CRL(证书吊销列表)或 OCSP(在线证书状态协议)
   → 确认证书未被吊销

5. 检查根 CA 是否受信任
   根 CA 必须在客户端的信任存储中
   → 不在信任存储中的根 CA 签发的证书被拒绝

五、密钥交换

5.1 RSA 密钥交换(已不推荐)

RSA 密钥交换流程:

1. 客户端生成 Pre-Master Secret(48 字节随机数)
2. 客户端用服务器证书中的 RSA 公钥加密 Pre-Master Secret
3. 发送加密后的 Pre-Master Secret(ClientKeyExchange)
4. 服务器用 RSA 私钥解密,得到 Pre-Master Secret
5. 双方用 Pre-Master Secret + Client Random + Server Random
   计算 Master Secret

问题: 没有前向保密(Forward Secrecy)
  如果攻击者获取了服务器的 RSA 私钥(即使是未来某天)
  可以解密之前捕获的所有 TLS 流量
  → 因为 Pre-Master Secret 是用这个私钥加密的

5.2 ECDHE 密钥交换(推荐)

ECDHE 密钥交换流程:

ServerKeyExchange:
1. 服务器生成临时 ECDH 密钥对(每次握手新生成)
2. 服务器发送 ECDH 公钥 + 选择的椭圆曲线
3. 服务器用 RSA/ECDSA 私钥对以上参数签名
   (证明是服务器本人发送的,防止中间人替换)

ClientKeyExchange:
4. 客户端生成自己的临时 ECDH 密钥对
5. 客户端发送自己的 ECDH 公钥

密钥计算:
6. 双方各自用"自己的 ECDH 私钥 + 对方的 ECDH 公钥"
   计算出相同的 Pre-Master Secret
7. Pre-Master Secret + Client Random + Server Random
   → Master Secret

前向保密:
  ECDH 临时密钥对在握手后即销毁
  即使攻击者未来获取了 RSA/ECDSA 私钥
  也无法还原 ECDH 密钥对 → 无法解密历史流量

5.3 RSA vs ECDHE 对比

特性 RSA 密钥交换 ECDHE 密钥交换
前向保密 ✗ 无 ✓ 有
性能 略快(无 ServerKeyExchange) 略慢(多一次签名验证)
安全性 低(私钥泄露=全部解密) 高(私钥泄露不影响历史)
TLS 1.3 已移除 唯一选项
推荐 强烈不推荐 必须使用
# 验证连接使用的密钥交换算法
openssl s_client -connect example.com:443 2>/dev/null | \
  grep "Server Temp Key"
# Server Temp Key: X25519, 253 bits
# → 使用了 ECDHE,曲线是 X25519

# 或查看选择的密码套件
openssl s_client -connect example.com:443 2>/dev/null | \
  grep "Cipher"
# New, TLSv1.2, Cipher is ECDHE-RSA-AES128-GCM-SHA256
# → ECDHE 密钥交换 + RSA 认证

5.4 椭圆曲线的选择

ECDHE 中的椭圆曲线(Elliptic Curve)决定了密钥交换的安全强度和计算效率。客户端在 ClientHello 的 supported_groups 扩展中声明自己支持的曲线,服务器从中选择一条。

曲线 密钥长度(bits) 等效 RSA 强度 性能 推荐
X25519 256 ~128 bit 最快 ✓ 首选
P-256(secp256r1) 256 ~128 bit ✓ 推荐
P-384(secp384r1) 384 ~192 bit 中等 可用
P-521(secp521r1) 521 ~256 bit 非必要不用
# 查看服务器选择的曲线
openssl s_client -connect example.com:443 2>/dev/null | \
  grep "Server Temp Key"
# Server Temp Key: X25519, 253 bits
# Server Temp Key: ECDH, P-256, 256 bits

# 强制客户端只使用特定曲线
openssl s_client -connect example.com:443 \
  -curves X25519 2>/dev/null | grep "Server Temp Key"

# 如果服务器不支持该曲线,握手会失败

# 查看 OpenSSL 支持的全部曲线
openssl ecparam -list_curves

X25519 由 Daniel J. Bernstein 设计,相比 NIST 曲线(P-256/P-384)具有两个工程优势:一是常量时间实现(Constant-Time),天然抵抗侧信道攻击;二是曲线参数透明(没有 NIST 曲线中”不可解释的种子”争议)。2025 年的部署建议是优先 X25519,兼容 P-256 作为后备。

六、Finished 消息与密钥推导

6.1 密钥推导过程

密钥推导(Key Derivation):

输入:
  - Pre-Master Secret(48 字节,来自密钥交换)
  - Client Random(32 字节,来自 ClientHello)
  - Server Random(32 字节,来自 ServerHello)

第一步: Master Secret
  master_secret = PRF(pre_master_secret,
                      "master secret",
                      ClientHello.random + ServerHello.random)
  → 固定 48 字节

第二步: Key Block(密钥材料块)
  key_block = PRF(master_secret,
                  "key expansion",
                  ServerHello.random + ClientHello.random)
  → 从中提取:
     client_write_MAC_key   (如果使用 HMAC 模式)
     server_write_MAC_key
     client_write_key       (对称加密密钥)
     server_write_key
     client_write_IV        (初始化向量)
     server_write_IV

注意: 客户端和服务器使用不同的密钥加密
     → 单方向的密钥泄露不影响另一方向

6.2 Finished 消息

Finished 消息的作用:
  验证整个握手过程没有被篡改

Finished 消息内容:
  verify_data = PRF(master_secret,
                    "client finished" 或 "server finished",
                    Hash(所有握手消息))

验证逻辑:
  1. 客户端发送 Finished:
     包含所有握手消息的哈希(ClientHello 到 ClientKeyExchange)
     用刚协商的密钥加密

  2. 服务器验证客户端的 Finished:
     重新计算哈希,与客户端发送的对比
     如果不一致 → 握手失败(可能是中间人攻击)

  3. 服务器发送 Finished:
     包含所有握手消息的哈希(含客户端的 Finished)
     用刚协商的密钥加密

  4. 客户端验证服务器的 Finished:
     重新计算哈希,与服务器发送的对比
     如果不一致 → 握手失败

七、会话恢复

7.1 Session ID 恢复

Session ID 恢复(服务器有状态):

首次握手:
  服务器在 ServerHello 中返回 Session ID
  握手完成后,服务器将 Session ID → Master Secret 的映射存储在内存中

恢复握手(缩短到 1 RTT):
  客户端                              服务器
    ├── ClientHello (Session ID=xxx) ──→
    │                                   查找 Session ID
    │←── ServerHello (Session ID=xxx) ──┤
    │←── [ChangeCipherSpec] ────────────┤
    │←── Finished ──────────────────────┤
    ├── [ChangeCipherSpec] ─────────────→
    ├── Finished ───────────────────────→
    ├══ Application Data ═══════════════╡

  节省: 跳过 Certificate + KeyExchange 步骤
  代价: 服务器需要维护 Session 缓存(内存/存储)
       多服务器时需要共享 Session 缓存

7.2 Session Ticket 恢复

Session Ticket 恢复(服务器无状态):

首次握手:
  服务器将会话状态加密后发给客户端(Session Ticket)
  客户端存储 Session Ticket

恢复握手:
  客户端                              服务器
    ├── ClientHello (含 Session Ticket) ──→
    │                                   解密 Ticket
    │                                   恢复会话状态
    │←── ServerHello ──────────────────────┤
    │←── [ChangeCipherSpec] ────────────────┤
    │←── Finished ──────────────────────────┤
    ├── [ChangeCipherSpec] ─────────────────→
    ├── Finished ───────────────────────────→

  优势: 服务器无需维护 Session 缓存
       天然支持多服务器(共享 Ticket 加密密钥即可)
  问题: Ticket 加密密钥如果泄露 → 所有使用该密钥的会话可被解密
       → 需要定期轮换 Ticket 加密密钥

7.3 Session ID vs Session Ticket 对比

特性 Session ID Session Ticket
状态存储 服务器端(内存) 客户端(加密 Ticket)
多服务器支持 需要共享缓存(Redis/Memcached) 共享 Ticket 加密密钥即可
内存开销 高(每个 Session 约 200 字节) 低(服务器无状态)
前向保密 不影响(密钥交换时已确定) 取决于 Ticket 密钥轮换频率
恢复延迟 1 RTT 1 RTT
RFC RFC 5246(TLS 1.2 内置) RFC 5077
# 测试 Session ID 恢复
openssl s_client -connect example.com:443 \
  -reconnect 2>&1 | grep -E "Session-ID|Reused"
# Reused, TLSv1.2, Cipher is ECDHE-RSA-AES128-GCM-SHA256
# → 表示 Session ID 恢复成功

# 测试 Session Ticket 恢复
openssl s_client -connect example.com:443 \
  -sess_out /tmp/session.pem 2>/dev/null </dev/null
openssl s_client -connect example.com:443 \
  -sess_in /tmp/session.pem 2>&1 | grep "Reused"
# Reused, TLSv1.2, ...
# → 表示 Session Ticket 恢复成功

# 查看 Session Ticket 信息
openssl s_client -connect example.com:443 2>/dev/null | \
  grep -A 2 "TLS session ticket"
# TLS session ticket lifetime hint: 300 (seconds)
# TLS session ticket:
# 0000 - 3a 45 67 89 ab cd ef ...

生产环境的建议:如果你只有单台服务器,Session ID 和 Session Ticket 都可用。如果有多台服务器做负载均衡,Session Ticket 更简单——只需在所有节点配置相同的 Ticket 加密密钥,但必须定期轮换(建议每小时轮换一次,保留上一轮密钥用于解密过渡期内的旧 Ticket)。

八、TLS Record 协议

握手消息(Handshake)、密码规格变更(ChangeCipherSpec)和应用数据(Application Data)都封装在 TLS Record 协议中传输。理解 Record 层有助于在 Wireshark 中定位问题。

TLS Record 格式:
┌────────────────┬──────────┬──────────┬──────────────────┐
│ Content Type   │ Version  │ Length   │ Fragment         │
│ (1 byte)       │ (2 bytes)│ (2 bytes)│ (≤ 16384 bytes)  │
└────────────────┴──────────┴──────────┴──────────────────┘

Content Type 值:
  20 = ChangeCipherSpec
  21 = Alert
  22 = Handshake
  23 = Application Data

Version:
  0x0301 = TLS 1.0
  0x0302 = TLS 1.1
  0x0303 = TLS 1.2
  注意: ClientHello 的 Record 层 Version 通常写 0x0301
       真正的版本在 Handshake 层的 ClientHello.version 中

一个容易混淆的点:多个 Handshake 消息可以打包在同一个 TCP 段中。例如 ServerHello、Certificate、ServerKeyExchange、ServerHelloDone 可能在一个 TCP 包里,包含多个 TLS Record——这就是为什么 Wireshark 中有时一帧显示多个 Handshake 消息。

# 用 tshark 观察 TLS Record 类型
tshark -r tls-handshake.pcap \
  -Y 'tls' \
  -T fields \
  -e frame.number \
  -e tls.record.content_type \
  -e tls.record.length

# 输出示例:
# 1  22  512   (Handshake: ClientHello)
# 2  22  3500  (Handshake: ServerHello + Certificate + ...)
# 3  22  130   (Handshake: ClientKeyExchange)
# 4  20  1     (ChangeCipherSpec)
# 5  22  40    (Handshake: Finished, 加密的)
# 6  20  1     (ChangeCipherSpec)
# 7  22  40    (Handshake: Finished, 加密的)
# 8  23  200   (Application Data)

8.1 Alert 协议

当握手过程出错时,TLS 通过 Alert 协议通知对方。Alert 消息包含两个字段:

Alert 消息:
┌─────────────┬──────────────────┐
│ Level       │ Description      │
│ (1 byte)    │ (1 byte)         │
└─────────────┴──────────────────┘

Level:
  1 = warning(警告)
  2 = fatal(致命,连接立即终止)

常见的 Alert Description:
  0  = close_notify           (正常关闭)
  10 = unexpected_message     (协议错误)
  20 = bad_record_mac         (MAC 验证失败)
  40 = handshake_failure      (握手参数不匹配)
  42 = bad_certificate        (证书无效)
  43 = unsupported_certificate (证书类型不支持)
  44 = certificate_revoked    (证书已吊销)
  45 = certificate_expired    (证书过期)
  48 = unknown_ca             (未知 CA)
  49 = access_denied          (访问被拒绝)
  70 = protocol_version       (协议版本不支持)
  71 = insufficient_security  (安全强度不足)
  80 = internal_error         (内部错误)
  86 = inappropriate_fallback (不当降级, TLS_FALLBACK_SCSV)
  90 = user_canceled          (用户取消)
  112 = unrecognized_name     (SNI 不识别)
# 用 tshark 捕获 TLS Alert
tshark -r tls-handshake.pcap \
  -Y 'tls.alert_message' \
  -T fields \
  -e frame.number \
  -e tls.alert_message.level \
  -e tls.alert_message.desc

# 常见场景:
# Alert(2, 40) = fatal handshake_failure
#   → 密码套件不匹配或协议版本不支持
# Alert(2, 48) = fatal unknown_ca
#   → 客户端不信任服务器的根 CA
# Alert(2, 42) = fatal bad_certificate
#   → 证书格式错误或签名验证失败

九、TLS 1.2 握手故障排查

9.1 常见握手错误

# 错误一: 密码套件不匹配
openssl s_client -connect legacy.example.com:443 \
  -cipher 'ECDHE-RSA-AES128-GCM-SHA256' 2>&1 | head -5
# 如果输出 "no ciphers available" 或 "handshake failure"
# → 服务器不支持客户端提供的任何密码套件

# 查看服务器支持的密码套件
nmap --script ssl-enum-ciphers -p 443 example.com

# 错误二: 证书验证失败
openssl s_client -connect example.com:443 2>&1 | grep "Verify"
# Verify return code: 0 (ok)                    → 正常
# Verify return code: 10 (certificate has expired) → 过期
# Verify return code: 18 (self signed certificate) → 自签名
# Verify return code: 21 (unable to verify)       → 链不完整

# 错误三: SNI 问题
# 默认发送 SNI
openssl s_client -connect 1.2.3.4:443 -servername example.com
# 不发送 SNI(可能得到默认证书)
openssl s_client -connect 1.2.3.4:443 -noservername

# 错误四: 协议版本不匹配
openssl s_client -connect example.com:443 -tls1 2>&1 | head -3
# 如果报错 → 服务器不支持 TLS 1.0(预期行为)

9.2 排查脚本

#!/bin/bash
# tls_check.sh — TLS 连接诊断
HOST="${1:?Usage: $0 hostname [port]}"
PORT="${2:-443}"

echo "=== TLS 诊断: $HOST:$PORT ==="

# 1. 基本连接测试
echo ""
echo "--- 连接测试 ---"
timeout 5 openssl s_client -connect "$HOST:$PORT" \
  -servername "$HOST" 2>/dev/null </dev/null | \
  grep -E "Protocol|Cipher|Verify|subject|issuer|Server Temp"

# 2. 证书有效期
echo ""
echo "--- 证书有效期 ---"
echo | openssl s_client -connect "$HOST:$PORT" \
  -servername "$HOST" 2>/dev/null | \
  openssl x509 -noout -dates 2>/dev/null

# 3. 证书 SAN
echo ""
echo "--- 证书 SAN ---"
echo | openssl s_client -connect "$HOST:$PORT" \
  -servername "$HOST" 2>/dev/null | \
  openssl x509 -noout -ext subjectAltName 2>/dev/null

# 4. 支持的协议版本
echo ""
echo "--- 协议版本支持 ---"
for proto in tls1 tls1_1 tls1_2 tls1_3; do
  result=$(echo | timeout 5 openssl s_client -connect "$HOST:$PORT" \
    -servername "$HOST" -"$proto" 2>&1)
  if echo "$result" | grep -q "CONNECTED"; then
    version=$(echo "$result" | grep "Protocol" | awk '{print $NF}')
    echo "  $proto: ✓ ($version)"
  else
    echo "  $proto: ✗"
  fi
done

# 5. 握手延迟
echo ""
echo "--- 握手延迟 ---"
start=$(date +%s%N)
echo | openssl s_client -connect "$HOST:$PORT" \
  -servername "$HOST" 2>/dev/null >/dev/null
end=$(date +%s%N)
elapsed=$(( (end - start) / 1000000 ))
echo "  TLS 握手: ${elapsed}ms"

十、Nginx TLS 1.2 配置最佳实践

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';

    # 服务器优先选择密码套件
    ssl_prefer_server_ciphers on;

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

    # 会话缓存和 Ticket
    ssl_session_cache shared:TLS:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;  # 禁用 Session Ticket(前向保密考虑)
                              # 或启用但确保密钥定期轮换

    # OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
    resolver 8.8.8.8 1.1.1.1 valid=300s;
    resolver_timeout 5s;

    # 安全头
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
}

十一、TLS 1.2 与 1.3 的关键差异预览

TLS 1.2 vs TLS 1.3 的主要变化:

握手 RTT:
  TLS 1.2: 2 RTT(完整握手)
  TLS 1.3: 1 RTT(完整握手)+ 0-RTT(恢复握手)

密码套件简化:
  TLS 1.2: 数十种组合(含不安全的)
  TLS 1.3: 仅 5 种(全部安全)

密钥交换:
  TLS 1.2: RSA / DHE / ECDHE
  TLS 1.3: 仅 (EC)DHE(强制前向保密)

移除的特性:
  ✗ RSA 密钥交换(无前向保密)
  ✗ CBC 模式加密(Padding Oracle 风险)
  ✗ RC4, 3DES, MD5, SHA-1
  ✗ 压缩(CRIME 攻击)
  ✗ 静态 DH
  ✗ 重新协商(Renegotiation)

新增的特性:
  ✓ 0-RTT 恢复模式
  ✓ 加密的 Certificate 消息
  ✓ 简化的密码套件协商
  ✓ 更快的握手

十二、总结

TLS 1.2 握手虽然正在被 TLS 1.3 取代,但理解它仍然是网络工程的必备技能:

  1. 2 RTT 是 TLS 1.2 完整握手的固有成本。 对于延迟敏感的场景,这意味着 60ms+ 的额外延迟(RTT=30ms 时)。会话恢复可以降到 1 RTT,但需要服务器端的状态管理或 Session Ticket 机制。

  2. ECDHE 不是可选项,是必选项。 RSA 密钥交换没有前向保密——如果私钥未来被泄露,攻击者可以解密所有历史流量。ECDHE 每次握手使用临时密钥,私钥泄露不影响历史数据。TLS 1.3 已经强制移除了 RSA 密钥交换。

  3. 密码套件的选择直接决定安全级别。 只使用 ECDHE-*-AES-*-GCM-SHA* 系列,禁用 CBC 模式(Padding Oracle 攻击)、RC4(已被攻破)、3DES(Sweet32 攻击)。服务器应启用 ssl_prefer_server_ciphers 确保使用安全套件。

  4. 证书链验证是握手中最容易出问题的环节。 证书过期、中间 CA 缺失、域名不匹配、根 CA 不受信任——每一种都会导致握手失败。openssl s_client -showcerts 是排查证书问题的最有效工具。

  5. 用 Wireshark 看过一次握手,比读十遍 RFC 更有效。 抓一次真实的 TLS 握手包,逐消息分析 ClientHello、ServerHello、Certificate、KeyExchange、Finished,你就真正理解了 TLS。

下一篇我们进入 TLS 1.3——理解它如何将握手优化到 1 RTT,0-RTT 恢复的安全权衡,以及从 TLS 1.2 升级的工程路径。


上一篇:DNS 故障排查实战:从超时到劫持

下一篇:TLS 1.3 工程实践:1-RTT 与 0-RTT 的安全权衡

同主题继续阅读

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

2025-08-03 · network

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

TLS 1.3 将握手从 2 RTT 压缩到 1 RTT,并引入了 0-RTT 恢复模式。本文从工程视角剖析 TLS 1.3 的简化设计——移除了哪些不安全的特性、1-RTT 握手的每一步变化、PSK 模式与 0-RTT 的重放攻击风险控制,以及从 TLS 1.2 升级的工程路径和兼容性陷阱。

2026-04-22 · network

网络工程索引

汇总本站网络工程系列文章,覆盖分层模型、以太网、IP、TCP、DNS、TLS、HTTP/2/3、CDN、BGP 与故障诊断。


By .