8. 0-RTT and Anti-Replay (0-RTT 和反重放)
如第 2.3 节和 Appendix E.5 所述, TLS 不为 0-RTT data 提供内建 replay protection. 需要关注两类潜在威胁:
- network attacker 通过简单复制一组 0-RTT data flight 发起 replay attack.
- network attacker 利用 client retry 行为, 安排 server 接收同一 application message 的多个副本.
第一类攻击可以通过共享状态来防止, 以保证 0-RTT data 最多只被接受一次. server SHOULD 通过实现本节描述的方法之一或等价方法提供这种级别的 replay protection. 但出于运行方面的考虑, 并非所有部署都会维护这种级别的状态. 因此, 在正常运行中, client 通常不知道 server 实际实现了哪些机制, 所以 MUST 只发送它认为可安全重放的 early data.
除了 replay 的直接影响外, 即使通常认为幂等的操作也可能被大量 replay 利用, 例如 timing attack 或 resource limit exhaustion. 这些问题可以通过确保每个 0-RTT payload 只能被 replay 有限次数来缓解. server MUST 确保其任一实例, 无论是机器, 线程还是相关服务基础设施内的其他实体, 对同一个 0-RTT handshake 最多接受一次 0-RTT. 这会把 replay 数量限制为部署中的 server 实例数量. server SHOULD 在可行时进一步限制 0-RTT replay.
第二类攻击无法在 TLS layer 防止, MUST 由应用处理. 任何 client 实现某种 retry 行为的应用, 本来就需要某种 anti-replay defense.
8.1. Single-Use Tickets (一次性票据)
最简单的 anti-replay defense 是 server 只允许每个 session ticket 使用一次. 例如, server 可以维护所有未使用且有效 ticket 的数据库, 每个 ticket 使用后即从数据库删除. 如果提供未知 ticket, server 回退到完整 handshake.
如果 ticket 是自包含的, 而不是数据库 key, 并且对应 PSK 在使用后被删除, 则使用该 PSK 建立的 connection 具备 forward secrecy. 当 PSK 不与 (EC)DHE 一起使用时, 这会提升所有 0-RTT data 和 PSK 用法的安全性.
该机制要求在多 server 分布式环境中共享 session database. 与自加密 ticket 相比, 它可能更难同时实现高可用和高性能.
8.2. ClientHello Recording (ClientHello 记录)
另一种 anti-replay 方法是记录从 ClientHello 派生出的唯一值, 通常为 random value 或 PSK binder, 并拒绝重复值. 记录所有 ClientHello 会导致状态无限增长, 因此 server 可以只在给定时间窗口内记录 ClientHello, 并使用 "obfuscated_ticket_age" 确保 ticket 不会在窗口外复用.
实现该机制时, server 收到 ClientHello 后首先验证 PSK binder. 然后计算 expected_arrival_time, 如果该时间位于记录窗口之外, 则拒绝 0-RTT 并回退到 1-RTT handshake. 如果该时间位于窗口内, server 检查是否已记录匹配 ClientHello. 如果找到匹配项, 它可以用 "illegal_parameter" alert 中止 handshake, 或接受 PSK 但拒绝 0-RTT. 如果未找到匹配项, 则接受 0-RTT 并在 expected_arrival_time 位于窗口内期间存储该 ClientHello.
server MUST 只从 ClientHello 中已验证的部分派生 storage key. 如果 ClientHello 包含多个 PSK identity, 未验证 binder 不应参与 storage key, 否则攻击者可能构造看似不同的 ClientHello 来污染 replay cache 或绕过重复检测.
在分布式系统中, 该机制不需要存储所有未使用 ticket, 因而可能更易实现. 代价是 anti-replay defense 可能较弱, 因为可靠存储和检索已接收 ClientHello 较难. 一种较强设计是让某个 ticket 只由单一 storage zone 权威处理, 并在其他 zone 拒绝该 ticket 的 0-RTT.
实现刚启动时, 如果记录窗口任何部分与启动时间重叠, SHOULD 拒绝 0-RTT, 否则可能接受启动期间最初发送的 replay.
8.3. Freshness Checks (新鲜度检查)
由于 ClientHello 指示 client 发送它的时间, server 可以高效判断某个 ClientHello 是否可能是近期 replay, 并只对这类 ClientHello 接受 0-RTT, 否则回退到 1-RTT handshake. 这对第 8.2 节的 ClientHello storage mechanism 是必要的, 因为否则 server 需要存储无限数量的 ClientHello.
server 需要存储 session ticket 创建时间, 以及 client 和 server 之间 round-trip time 的 offset estimate:
adjusted_creation_time = creation_time + estimated_RTT
该值可以编码进 ticket, 从而避免为每个未使用 ticket 保持状态. server 可以通过从 client "pre_shared_key" extension 中的 "obfuscated_ticket_age" 参数减去 ticket 的 "ticket_age_add" 值, 确定 client 视角下的 ticket age. server 可以如下确定 ClientHello 的 expected_arrival_time:
expected_arrival_time = adjusted_creation_time + clients_ticket_age
收到新的 ClientHello 时, server 将 expected_arrival_time 与当前 server wall clock time 比较. 如果差异超过某个范围, 则拒绝 0-RTT, 但仍可以完成 1-RTT handshake.
freshness check 本身不足以阻止 replay, 因为它无法检测误差窗口内的 replay. 此外, freshness check 只在收到 ClientHello 时执行, 不在随后收到 early application data record 时执行.