TLS 1.3 协议与证书链

系统讲解 TLS 1.3 的握手流程、密钥派生与证书链验证:从 1-RTT 与 0-RTT 的会话恢复机制、HKDF 密钥调度与 AEAD 密码套件,到 X.509 证书链、OCSP Stapling 与 CT 日志,并给出 OpenSSL 抓包验证与生产配置调优的完整实践。

TLS(Transport Layer Security)是 HTTPS 的地基,但 TLS 1.2 的握手要两个往返(2-RTT)才能开始传数据,密码套件里塞满了早已不安全的 RC4、CBC 与静态 RSA 密钥交换,还留着压缩、重协商这些被攻击者反复利用的边角。TLS 1.3(RFC 8446)做的是「减法」:砍掉所有已知不安全的选项,把握手压缩到 1-RTT,并引入 0-RTT 让重复连接几乎零延迟。

本文按协议的实际执行顺序展开:先看 1-RTT 握手里每个报文的字段与密钥用途,再拆解 0-RTT 与会话恢复的 PSK 机制及其重放风险,然后回到证书链如何从信任锚一路验证到叶证书,最后落到密钥派生(HKDF)与生产环境的抓包验证、配置调优。

一、TLS 1.3 的设计动机与握手流程

1.1 从 TLS 1.2 到 1.3 的减法

TLS 1.3 不是加功能,而是砍功能。凡是历史上出过漏洞、或者能用更简单方式替代的机制,一律删除,剩下的组合更少、更容易审计。这直接改变了协议的安全默认值:前向保密从「可选」变成「强制」。

TLS 1.2 → TLS 1.3 删除项:
  - RSA 静态密钥交换(无前向保密,密钥泄露可回溯解密)
  - CBC 模式与 RC4 流密码(BEAST、POODLE、Lucky13)
  - 压缩(CRIME 攻击可推断明文)
  - 重协商(Renegotiation,历史上可注入前缀)
  - 自定义 DH 参数(Logjam 攻击)
  - 明文形式的 Certificate 之外的多余字段

1.2 1-RTT 完整握手

TLS 1.3 把密钥交换算法(如 X25519)的公钥直接放进 ClientHello 的 key_share 扩展,服务端收到即可计算共享密钥。因此服务端在第一个响应里就能加密发送证书与 Finished,客户端验证后立刻发送应用数据——整个握手只要一个往返。

Client                                          Server
------                                          ------
ClientHello
  + supported_versions = 1.3
  + key_share (X25519 公钥)
  + signature_algorithms
  + server_name (SNI)
                        ------------------>
                                                ServerHello
                                                  + key_share
                                                {EncryptedExtensions}
                                                {Certificate}
                                                {CertificateVerify}
                                                {Finished}
                        <------------------
{Finished}
  + Application Data
                        ------------------>
                                                Application Data

注意 {} 包裹的报文全部是加密的:从 ServerHello 之后,握手记录就受握手密钥保护。这意味着 SNI 之外的握手内容在链路上不可见,只有 SNI 仍是明文(除非用 ECH)。

1.3 HelloRetryRequest 与密钥共享

如果客户端猜的密钥交换组服务端不支持(比如客户端只发了 X25519,服务端只配了 P-256),服务端会回一个特殊的 ServerHello——HelloRetryRequest,要求客户端换组重发。这会多花一个往返,所以生产环境应让客户端预置常见组(X25519 优先)。

HelloRetryRequest 触发条件:
  - ClientHello 的 key_share 不含服务端偏好的组
  - 服务端在 supported_groups 里选了另一个组

代价:握手从 1-RTT 退化为 2-RTT
缓解:客户端默认发送 X25519 + P-256 两个 key_share

二、0-RTT 与会话恢复

2.1 PSK 与 Session Ticket

TLS 1.3 的会话恢复统一走 PSK(Pre-Shared Key)机制:服务端在首次握手后用 NewSessionTicket 下发一个加密的票据,客户端下次连接时在 ClientHello 里带上它,双方直接派生密钥,跳过证书验证。

会话恢复(1-RTT,PSK 模式):
  1. 首次握手完成后,服务端发送 NewSessionTicket
     - ticket_lifetime
     - ticket_age_add(用于 0-RTT 年龄计算)
     - ticket_nonce(派生 PSK 用)
  2. 客户端缓存 ticket
  3. 下次连接,ClientHello 携带 pre_shared_key 扩展
  4. 服务端解密 ticket,恢复 PSK,直接进入加密握手

2.2 0-RTT 与重放风险

0-RTT(Early Data)允许客户端在收到任何服务端响应之前就发送应用数据,代价是这部分数据不具备前向保密,且可能被重放。攻击者截获 0-RTT 数据包后原样重发,服务端无法区分,因为 0-RTT 数据不参与握手的 nonce 校验。

0-RTT 的安全边界:
  - 无前向保密:PSK 泄露可解密 early data
  - 可重放:无服务端挑战,重放不可检测
  - 因此只允许幂等请求(GET、安全查询)
  - 禁止:POST 转账、下单、任何有副作用的写操作

2.3 反重放机制

服务端可以用「单次票据」或「时间窗口 + 缓存」来限制重放。单次票据把 ticket 用一次即作废,最简单但牺牲恢复率;时间窗口方案缓存最近见过的 early data,拒绝重复的,代价是状态开销。

方案原理代价
单次票据ticket 用过即失效恢复率下降,并发连接互斥
时间窗口缓存最近窗口内的 early data 指纹内存状态,窗口越大越贵
拒绝 0-RTT只接受 1-RTT 恢复零风险,但失去 0-RTT 延迟收益

三、证书链与验证

3.1 X.509 证书结构与关键扩展

TLS 服务端在 Certificate 报文里发送证书链:叶证书(叶/服务器证书)在前,中间证书随后。根证书不发送,客户端本地信任库已有。验证时逐级用上级公钥验证下级签名。

X.509 证书关键字段:
  tbsCertificate:
    version, serialNumber
    signatureAlgorithm          # 签发算法
    issuer / subject            # 颁发者 / 主体 DN
    validity                    # notBefore / notAfter
    subjectPublicKeyInfo        # 公钥
    extensions:
      subjectAltName (SAN)      # 关键:现代校验只看 SAN
      basicConstraints          # CA:TRUE 才能签发下级
      keyUsage / extKeyUsage    # serverAuth
      CRLDistributionPoints / OCSP  # 吊销信息
      authorityInfoAccess (AIA) # OCSP 与签发者地址
  signatureValue

3.2 链式验证过程

验证的核心是「信任锚 → 中间 CA → 叶证书」的签名链,再叠加一系列约束检查。任何一环缺失都会导致 unable to verify the first certificate。

验证步骤:
  1. 从叶证书向上构建链,直到命中本地信任库中的根证书
  2. 每一级:用 issuer 公钥验证 subject 签名
  3. 检查 basicConstraints:中间证书必须 CA:TRUE
  4. 检查有效期(notBefore / notAfter)
  5. 检查 SAN 是否匹配目标主机名(不再看 CN)
  6. 检查 keyUsage / extKeyUsage 是否允许 serverAuth
  7. 查询吊销状态(CRL 或 OCSP)

3.3 OCSP Stapling、CT 与 SNI

在线查询 OCSP 会带来隐私与延迟问题,OCSP Stapling 让服务端主动附上由 CA 签名的吊销状态,客户端无需外联。CT(Certificate Transparency)则要求证书写入公开日志,任何为域名签发的证书都可被监控发现。

机制解决的问题部署要点
OCSP Stapling吊销查询延迟与隐私泄露Nginx ssl_stapling on,需配 resolver
Must-Staple防止降级到在线查询证书扩展 status_request
CT 日志检测未授权证书签发现代 CA 强制,浏览器校验 SCT
SNI单 IP 多域名虚拟主机明文发送,可用 ECH 加密
# 查看证书链与验证结果
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null

# 只看叶证书的 SAN 与有效期
echo | openssl s_client -connect example.com:443 2>/dev/null \
  | openssl x509 -noout -subject -ext subjectAltName -dates

# 验证链是否完整(-CAfile 指定信任锚)
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt fullchain.pem

四、密钥派生与密码套件

4.1 HKDF 密钥调度

TLS 1.3 用 HKDF(HMAC-based Key Derivation Function)分两阶段派生密钥:先 HKDF-Extract 把共享密钥和盐揉成固定长度的伪随机密钥,再 HKDF-Expand 按不同标签派生出各个用途的密钥。所有握手密钥都从同一个「密钥调度」树状展开。

密钥调度(简化):
                0
                |
   PSK ───> HKDF-Extract ──> Early Secret
                                   |
                                 Derive-Secret
                                   |
   (EC)DHE ─> HKDF-Extract ──> Handshake Secret
                                   |
                                 Derive-Secret
                                   |
                0 ───> HKDF-Extract ──> Master Secret
                                          |
                                   Derive-Secret
                                          |
                         client_application_traffic_secret_0
                         server_application_traffic_secret_0

每个 Secret 再派生出:key(AEAD 密钥)+ iv(nonce 基数)

4.2 AEAD 密码套件

TLS 1.3 只保留 AEAD(带关联数据的认证加密)套件,且把密钥交换、认证算法与 AEAD 算法解耦。套件名里只剩 AEAD 与哈希两个参数,签名算法单独协商。

密码套件AEAD哈希特点
TLS_AES_128_GCM_SHA256AES-128-GCMSHA-256有 AES-NI 时最快
TLS_AES_256_GCM_SHA384AES-256-GCMSHA-384更高强度
TLS_CHACHA20_POLY1305_SHA256ChaCha20-Poly1305SHA-256无 AES 加速时更快
TLS_AES_128_CCM_SHA256AES-128-CCMSHA-256物联网/受限设备

4.3 KeyUpdate 与密钥轮换

长时间连接下,TLS 1.3 允许任一方发送 KeyUpdate 请求轮换应用密钥,限制单个密钥加密的数据量,降低密钥被破解后的暴露面。响应方更新自己的密钥,请求方在收到响应后更新。

KeyUpdate 语义:
  update_requested     -> 对方必须回复自己的 KeyUpdate
  update_not_requested -> 单向轮换,对方可稍后再轮换
  约束:每轮换一次,traffic_secret 序号 +1

五、工程实践与排错

5.1 用 OpenSSL 验证握手细节

排查 TLS 问题时,s_client 是最直接的工具。加上 -tls1_3 强制版本,-msg 打印握手报文,-keylogfile 导出密钥后可在 Wireshark 里解密。

# 强制 TLS 1.3,查看协商的套件与组
openssl s_client -connect example.com:443 -tls1_3 -groups X25519 </dev/null 2>&1 | head -30

# 打印握手报文(观察 key_share、EncryptedExtensions)
openssl s_client -connect example.com:443 -tls1_3 -msg </dev/null 2>&1 | grep -A2 ">>>"

# 导出密钥供 Wireshark 解密
openssl s_client -connect example.com:443 -keylogfile keys.log </dev/null

5.2 Nginx 生产配置要点

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;          # TLS 1.3 下交给客户端选更快
ssl_session_cache shared:SSL:50m;       # 会话恢复缓存
ssl_session_timeout 1d;
ssl_session_tickets on;                 # 0-RTT 依赖 ticket
ssl_early_data on;                      # 开启 0-RTT(仅幂等接口)
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 valid=300s;            # OCSP Stapling 需要
add_header Strict-Transport-Security "max-age=63072000" always;

5.3 常见故障对照

现象原因排查
unable to verify first certificate服务端未发中间证书openssl s_client -showcerts
certificate has expired证书过期检查 notAfter,配置自动续期
handshake failure套件/版本不匹配强制 -tls1_3 复现,看协商日志
首包慢、后续快未启用会话恢复检查 ssl_session_cache 与 ticket
0-RTT 数据被拒反重放窗口触发仅对幂等请求启用 early data

5.4 会话恢复与 OCSP 的性能权衡

会话恢复能省掉一次完整的证书验证与密钥交换,但不同恢复方式的开销与安全属性差异很大。选择哪种,取决于对「延迟」和「前向保密」的偏好。

恢复方式额外往返前向保密适用场景
完整握手1-RTT有首次连接
PSK 恢复(含 DHE)1-RTT有通用推荐
PSK 恢复(纯 PSK)1-RTT无极低延迟内部服务
0-RTT0-RTT无幂等请求

大集群下推荐「有状态缓存 + 无状态票据兜底」:缓存命中走内存,未命中用票据解密,避免所有节点共享会话表。注意票据密钥需在集群内轮换与同步。

5.5 硬件加速与卸载

现代 CPU 的 AES-NI、ARM 的 Crypto Extensions 能把 AES-GCM 的吞吐提升数倍;Linux 的 kTLS 则把记录层的加解密下推到内核甚至网卡。判断是否需要,先看 CPU 是否成为瓶颈——用 top 观察 si(软中断)与 sy(系统)占比。

# 确认 CPU 是否支持 AES-NI
grep -o aes /proc/cpuinfo | head -1

# 测本地加解密能力(对比 aes-128-gcm 与 chacha20-poly1305)
openssl speed -evp aes-128-gcm
openssl speed -evp chacha20-poly1305

# 观察软中断与系统 CPU 占比,判断是否需卸载
mpstat -P ALL 1
# 开启 kTLS:把 TLS 记录层发送卸载到内核
ssl_conf_command Options KTLS;
优化点手段收益
对称加密AES-NI / ChaCha20数倍吞吐
记录层kTLS 内核卸载减少用户态拷贝
握手会话恢复 + 0-RTT降低往返
证书验证OCSP Stapling消除在线查询延迟
密钥交换优先 X25519更快、更安全

5.6 一次握手的耗时分解

定位 TLS 慢,最好把握手拆成可测量的阶段。用 openssl s_client -timed 或浏览器开发者工具的 Timing 面板,能看清时间花在哪里。

典型 HTTPS 连接时间线(冷启动):
  DNS 解析        :  5 ~ 50 ms
  TCP 三次握手     :  1 个 RTT
  TLS 握手         :  1 个 RTT(TLS 1.3)
  首字节(TTFB)   :  1 个 RTT
  ------------------------------------
  总计             :  DNS + 3 个 RTT

优化点:
  - DNS:预解析、HTTPDNS、缩短 TTL 链路
  - TCP+TLS:TLS 1.3 把两者合并到 2 个 RTT
  - 复用:会话恢复或 0-RTT 直接砍掉 TLS 往返

六、小结

主题核心要点落地建议
握手1-RTT,密钥交换内联在 ClientHello客户端预置 X25519 + P-256
0-RTTPSK 恢复,可重放、无前向保密只对幂等请求开启
证书链信任锚到叶证书逐级验签服务端必须发完整中间链
密钥派生HKDF 分层派生,AEAD 加密优先 AES-GCM,无加速选 ChaCha20
排错s_client 抓报文与密钥保存 keylog 便于 Wireshark 解密

TLS 1.3 把「安全」从配置项变成了协议默认值:前向保密强制、AEAD 强制、握手加密。理解握手报文顺序、密钥调度树与证书链验证,是排查一切 HTTPS 问题的共同起点——想进一步看它在传输层的邻居,可以对照 网络安全与加密通信 里的证书管理,以及 HTTP/3 与 QUIC 中 QUIC 自带的 0-RTT 实现差异。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「network」更多文章

  1. MPTCP 与多路径传输
  2. 流量整形与 TC/qdisc
  3. 网络命名空间与容器网络底层