TLS 0-RTT 早期数据:应用开发者必须了解的安全风险与解决方案

本文深入解析TLS 1.3的0-RTT(零往返时间)特性,探讨其如何通过早期数据提升性能但引入重放攻击风险,并提供针对CDN、HAProxy、nginx等环境的实际防护方案与最佳实践建议。

TLS 0-RTT 早期数据:应用开发者必须了解的安全风险与解决方案

TLS 1.3 代表了二十多年大规模传输安全部署经验的结晶。它在很大程度上简化并提升了 TLS 的安全性,可以作为 TLS 1.2 的直接替代品。然而,协议中的一个新特性对某些现有应用构成了重大安全风险:TLS 0-RTT(也称为早期数据)。这种性能优化可能导致未实现自身防重放机制的应用遭受重放攻击。在某些情况下,仅升级 TLS 依赖就可能引入应用层漏洞。

让我们通过一个易受攻击的应用示例,分析其脆弱性原因,并讨论在各应用层可以采取哪些措施来解决这个问题。

一个易受攻击的应用

假设你的公司运营一个带有买卖 API 的平台。由于各种遗留原因,公司通过 API GET /api/sell/(item)/(qty)GET /api/buy/(item)/(qty) 来实现这些操作。

后来,运维团队升级了 TLS 基础设施以支持 TLS 1.3。这可能是在 CDN 上启用、升级 TLS 硬件卸载盒或更新负载均衡器软件。他们将此更新视为任何其他标准修补过程。毕竟,这是一个透明升级,就像 TLS 1.0 到 1.1 或 1.1 到 1.2 一样……除非启用了 0-RTT。如果启用,那么上述 API 现在容易受到任意重放攻击。0-RTT 中隐含的设计权衡使得以下攻击成为可能:

一个之前登录到系统的用户走进咖啡店,连接上 WiFi,并发起买卖操作。得益于 TLS 1.3 0-RTT,此交易无需初始握手的往返时间,节省了 300 毫秒!当然,所有通信仍通过 TLS 加密。

攻击者从 WiFi 捕获该流量并再次向服务器发送相同请求。与 TLS 1.2 不同,此请求不会被 TLS 层拒绝,买卖操作会再次发生。

这是一个令人惊讶的结果!相关 API 现在容易受到传输层的新攻击,而无需对实际应用进行任何代码更改。为什么?答案在于 0-RTT 的设计。

什么是 0-RTT?

TLS 0-RTT(“零往返时间”的缩写,正式名称为“TLS 早期数据”)是一种降低 TLS 连接首字节时间的方法。TLS 1.3 仅需要 1-RTT(单次往返)协议,而 TLS 1.2 及以下版本需要两次,但设计者想要更多!对于客户端和服务器拥有预共享密钥(PSK)的连接,客户端可以选择使用此密钥加密早期数据,并将其与 ClientHello 一起发送。这允许服务器在发送自己的 ServerHello/EncryptedExtensions/Finished 消息后立即响应请求的数据。这消除了整个通信往返:零往返时间。在移动环境中,这可能节省大量时间(数百甚至数千毫秒)。PSK 可以通过带外方式获取,但通常是从早期握手中保留的。早期数据通信通常限于与客户端之前通信过的服务器。

它如何破坏你的应用?

当然,减少往返时间需要某些权衡。在典型的 TLS 1.3 连接模型中,每个会话都有一个称为前向保密性的属性。前向保密性保证即使当前会话的私钥以某种方式泄露,过去的会话也是安全的。TLS 1.3(以及具有临时密钥交换模式的 1.2)通过为每次握手生成一组新密钥来提供此功能。不幸的是,由于 PSK 无法在没有往返的情况下刷新,通过 0-RTT 发送的初始请求不是前向安全的。它是使用上一个会话的密钥加密的。

然而,一个更重要的担忧是 0-RTT 请求无法防止重放攻击。为了应对这一点,应用层需要从其 TLS 实现获取有关接收到的请求是否为 0-RTT 的信息。有了这些信息,应用可以通过拒绝在非幂等操作上允许 0-RTT 请求或通过直接的反重放防御(如 nonce)来拒绝 0-RTT,这些 nonce 可以对照共享全局状态进行检查,以确认给定请求从未被见过。RFC 8470 试图记录适用于 0-RTT 的此类缓解措施。

不幸的是,实现强大的防御说起来容易做起来难。到目前为止,Web 应用通常不需要了解其传输安全性的变幻莫测。截至本文撰写时,解决这一问题的努力有限。假设上,应用应该已经能够处理重放。现实从未如此方便。Go 的新 TLS 1.3 支持不包括 0-RTT,部分原因是担心如何安全地暴露它。Cloudflare 选择禁止 0-RTT,仅允许没有查询字符串的 GET 请求, specifically 试图缓解此问题,同时通过添加的标头代理有关任何给定请求的附加信息。不幸的是,我们之前的示例应用仍然易受攻击,因为它使用 GET 请求!

你能做什么?

最重要的是,升级到 TLS 1.3!它是一个比其前身更好、更安全的协议。然而,作为升级的一部分,在审计应用是否存在此类漏洞之前,请禁用 0-RTT。如果你使用带有 TLS 终止的 CDN,请阅读文档以确定他们为你转发哪些信息以在应用层解释。否则,如果你无法访问特定连接细节,则需要确保对敏感操作有非常强大的反重放防御措施。

如果你是 Web 框架开发者,应该认真考虑可以提供哪些 API 给你的消费者,以帮助他们管理此风险,同时提供性能优势。这可能需要与框架运行的各种服务器合作,提出一个通用 API 来代理所需信息。例如,如果上述易受攻击应用中使用的 Web 框架有一个幂等性注解,那么以这种方式注解的路由可以自动启用 0-RTT,而所有其他路由将拒绝 0-RTT 请求(从而自动回退到标准握手)。

如果你在应用中直接使用像 OpenSSL 这样的 TLS API,则需要实现各种回调,如 SSL_CTX_set_allow_early_data_cb,并仔细考虑与会话管理相关的重放保护影响。0-RTT 支持仅在消费这些新 API 时启用,因此你可以随时间选择加入。

密码学家一直在研究如何在 0-RTT 请求的背景下获得可用的前向保密性。一些最近发表的研究(Session Resumption Protocols and Efficient Forward Security for TLS 1.3 0-RTT)提出使用可穿刺伪随机函数来显著减少会话数据库的大小,但在计算复杂性和后妥协安全性方面存在权衡。截至发布时,这是一个活跃的研究领域,没有真正适合部署的解决方案。

如果你想利用 TLS 1.3 的性能,同时确保你的应用和用户在 0-RTT 世界中安全,请联系 Trail of Bits 的工程和密码学团队。我们很乐意帮助你安全地设计你的应用。

常见问题解答

如果我 behind a CDN 怎么办?

对于基于 CDN 的终止,你需要检查他们的文档以了解他们提供哪些功能。Cloudflare(是少数为此提供公共文档的 CDN 公司之一)使用名为 CF-0RTT-Unique 的标头,应用需要跟踪从该标头接收的值,并在非幂等端点上拒绝重复项。

如果我使用 HAProxy 终止怎么办?

默认情况下,启用 TLS 1.3 不会启用 0-RTT 支持。

你可以通过在配置中的 bind 或 server 行添加 allow-0rtt 来启用 0-RTT。一旦启用,0-RTT 请求将通过标头 Early-Data: 1 代理到应用层,并且可以通过返回 425 状态码来拒绝请求。这种代理信息的方法在 RFC 8470 中编码。

如果我使用 nginx 终止怎么办?

默认情况下,启用 TLS 1.3 不会启用 0-RTT 支持。

你可以通过在配置中使用 ssl_early_data on; 来启用 0-RTT。你还需要在代理指令中添加 proxy_set_header Early-Data $ssl_early_data; 以确保 Early-Data 标头传递到你的应用。

如果我使用 Apache httpd 终止怎么办?

httpd 2.4.37 及以上版本支持 TLS 1.3,但目前(2019 年 3 月)没有 0-RTT 支持。

应用内终止:Go、Python、Ruby、C

Go 从 1.12 开始支持 TLS 1.3,但没有 0-RTT 支持。

Python 目前(2019 年 3 月)没有早期数据支持。

Ruby 目前(2019 年 3 月)没有早期数据支持。

C 应用可以利用他们想要的任何 TLS 栈。每个单独的库都不同,但最常见的 OpenSSL 仅在调用 SSL_CTX_set_max_early_data(或 SSL_set_max_early_data)且值大于零时启用 0-RTT。开发者还可以使用 SSL_CTX_set_allow_early_data_cb 来设置一个回调函数,确定是否应接受给定的 0-RTT 请求。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计