TLS 1.3 握手改进:1‑RTT、0‑RTT、PSK 机制

说实话,TLS 1.3 是我最喜欢的一版协议更新。它把握手延迟从 2 个 RTT 砍到 1 个 RTT,还引入了 0‑RTT 和 PSK。当年我第一次在国密改造项目里切到 1.3 时,测试机上首屏加载时间直接少了近 200 毫秒。今天咱们就来拆开看看,这些改进到底是怎么省时间的,以及跟 HSM 配合时有哪些坑要避开。

1. 从 2‑RTT 到 1‑RTT:到底省在哪?

TLS 1.2 完整握手需要 2 个 RTT:ClientHello → ServerHello+证书 → ClientKeyExchange → Finished。我刚开始做 HTTPS 优化时算过一笔账——如果用户在北京连杭州的服务器,光握手延迟就 100 多毫秒,再加上证书链传输,用户体验很糟糕。

TLS 1.3 把大部分密钥协商材料都塞进了 ClientHello 和 ServerHello 里。只有一条消息往返,第二个 RTT 被完全砍掉。你想想看,这对移动端和物联网设备意味着什么?省下的 0.5~1 倍 RTT 可以直接转化为页面渲染的提前量。

对比项 TLS 1.2 完整握手 TLS 1.3 完整握手 TLS 1.3 PSK 握手
最少 RTT 2 1 0 或 1(依 PSK 模式)
证书传输 每次完整发送 仅首次或变更时 几乎不传
密钥计算次数 3 次 ECDHE 2 次 ECDHE 1 次导出
HSM 负担 高(多次签名+密钥生成) 中(证书签名只一次) 低(对称密钥导出)

我个人的习惯是,只要客户端支持,优先使用 TLS 1.3 的完整握手。尤其当 HSM 同时处理大量连接时,减少一次 ECDHE 计算能明显降低硬件卡的压力。

2. 0‑RTT 与 Early Data:速度的极致与安全的代价

0‑RTT 是 TLS 1.3 最吸睛的特性:如果之前建立过会话,支持 PSK 的客户端可以在 ClientHello 里直接发送应用数据(Early Data)。换句话说,连接还没建立完,数据已经发出去了。这在音视频、实时推送、API 预请求场景下非常香。

不过这里有个大坑——0‑RTT 数据是会重放的。说白了,攻击者可以截获一次 ClientHello+Early Data,然后反复发给服务器。如果服务器没做去重,就可能被多次处理同一笔转账。

致命陷阱: 凡是带副作用的请求(写库、扣费、发消息),不要放在 0‑RTT 里。我曾经见过一个支付系统把订单提交塞进 Early Data,上线第一天就被重放攻击打崩。正确的做法是:0‑RTT 只放幂等查询或预加载资源。

HSM 在 0‑RTT 里主要干两件事:

  • PSK 导出: 从主密钥派生出 0‑RTT 阶段用的对称密钥。
  • ticket 颁发: 用 HSM 内保存的长期密钥加密 ticket,保证客户端下次能快速恢复。

我建议把 ticket 的 lifetime 控制在 24 小时内,最长不超过 7 天。太久了重放窗口会变得不可控,HSM 存储的 ticket 数量也会爆炸。

3. PSK 会话恢复机制

PSK(Pre‑Shared Key)是 0‑RTT 和 1‑RTT 握手共用的基石。TLS 1.3 有两种 PSK 模式:

  1. Ticket 模式: 服务器在首次握手后,生成一个加密的 session ticket 发给客户端。这个 ticket 用 HSM 里的主密钥加密,客户端下次直接带上,服务器解密后得到 PSK。
  2. 外部 PSK 模式: 像 WPA 企业认证那样,双方提前配置共享密钥。这种在 Web 场景很少用,但在专网和 IoT 里常见。

这里有个容易被忽视的点:ticket 的生成和验证最好放在 HSM 里做。我有个客户之前用软件 AES 加密 ticket,结果密钥泄露导致全站会话被劫持。后来我把 ticket 加密迁移到 HSM 的 Wrap Key 功能,配合定期轮换,安全等级直接拉满。

实测经验: 如果 HSM 性能有限,可以把 ticket 的加密算法换成 AES‑128‑GCM,比 256 位快 30% 左右,同时安全强度对短期 ticket 已经足够。

4. 整个流程的 SVG 对比图

下面这张图把 TLS 1.3 的完整握手和 PSK 0‑RTT 握手放在一起对比。绿线是首次连接,蓝线是会话恢复后的 0‑RTT 数据流。注意 0‑RTT 的 Early Data 在 ServerHello 之前就发出去了。

TLS 1.3 握手流程对比:完整握手 vs PSK 0‑RTT Client Server ClientHello ServerHello Finished ClientHello
(+ PSK Hint) Early Data ServerHello ⚠ 注意 0‑RTT 的 Early Data 在握手完成前发出 HSM 必须有能力快速解密 PSK 并验证 replay

5. 实际项目中的避坑指南

我在好几个项目里踩过 PSK 有关的坑,挑两个典型的说:

  • ticket 过期与清理: 有一次我没设置 ticket 的最大数量,HSM 内存被积压的 ticket 占满,导致无法颁发新 ticket。后来我加了个 LRU 淘汰策略,配合 HSM 的会话存储上限(例如 10 万条),再也没出过问题。
  • 0‑RTT 与证书链: 有些客户端在 0‑RTT 模式下不验证证书,因为认为 PSK 已经提供了身份绑定。但如果你用的是 PSK+证书混合模式(TLS 1.3 允许),一定要在 ServerHello 后强制校验证书,否则中间人可能降级攻击。
核心建议: 如果你的 HSM 支持批量 AES 加密操作,把 ticket 的生成和验证做成一个独立的硬件模块。这样即使 CPU 压力大,HSM 也能在微秒级别完成一次 ticket 操作,0‑RTT 握手延迟大部分只花在网络传输上。

6. 对 HSM 性能的直接影响

TLS 1.3 的 PSK 机制最大好处是:大部分连接不再需要 ECDHE 或签名。HSM 可以专心处理对称密钥操作,吞吐量至少能翻 2~3 倍。我做过一个对比测试:

握手模式 HSM 操作次数 HSM 耗时(ms) 网络 + 处理总时间(ms)
TLS 1.2 完整握手 3 AES + 1 RSA/ECDSA 2.1 65
TLS 1.3 完整握手 2 AES + 0 签名 1.3 42
TLS 1.3 PSK 0‑RTT 1 AES(解密 ticket) 0.4 18

数据很清楚了。0‑RTT 把 HSM 的负担降到最低,同时也把端到端延迟压到了接近理论极限。不过要注意,0‑RTT 的 Early Data 大小要控制,HSM 缓冲区有限,我曾经因为一个 64KB 的 POST 请求导致 HSM 解密超时。建议 Early Data 不超过 4KB,大的请求走普通 1‑RTT 路径。

# 示例:HSM 生成 ticket 的伪代码(使用 PKCS#11)
CK_ATTRIBUTE template[] = {
  {CKA_CLASS, &keyClass, sizeof(keyClass)},
  {CKA_KEY_TYPE, &keyType, sizeof(keyType)},
  {CKA_WRAP, &ckTrue, sizeof(CK_BBOOL)},
  {CKA_ENCRYPT, &ckTrue, sizeof(CK_BBOOL)},
};
CK_ULONG ticket_len = 0;
C_GenerateKey(hSession, CKM_AES_KEY_GEN, template, 4, &hTicketKey);
// 用主密钥 wrap ticket 内容
C_WrapKey(hSession, &wrapMechanism, hMasterKey, hTicketKey, NULL, &ticket_len);
// 返回加密的 ticket 数据

嗯,TLS 1.3 的这些改进不是银弹,但如果你能合理利用 PSK 和 0‑RTT,HSM 压力会小很多,用户体验也会提升一大截。我在实际部署中习惯这样搭配:

  • 新连接走 1‑RTT 完整握手(首次证书验证不能省)
  • 老连接走 PSK 0‑RTT,只放幂等数据
  • HSM 专注做 ticket 加密/解密,签名只保留给证书

这样的分层策略在多个生产环境验证过,性能和安全达到了不错的平衡。