TLS早期数据(0RTT):应用开发者必须了解的安全风险
TLS 1.3代表了二十多年大规模传输安全部署经验的结晶。它主要简化和提升了TLS的安全性,可作为TLS 1.2的直接替代方案。然而,协议中的一项新功能对某些现有应用构成了重大安全风险:TLS 0-RTT(又称早期数据)。这种性能优化可能导致未实现自身防重放机制的应用遭受重放攻击。在某些情况下,仅升级TLS依赖就可能引入应用层漏洞。
一个易受攻击的应用程序案例
假设某公司运营一个具有买卖API的平台。由于历史遗留原因,公司通过GET /api/sell/(item)/(qty)和GET /api/buy/(item)/(qty) API实现这些操作。
当运维团队将TLS基础设施升级至支持TLS 1.3时(可能在CDN、TLS硬件卸载设备或负载均衡器上启用),如果0-RTT被启用,上述API将面临任意重放攻击风险。攻击场景如下:
用户登录系统后连接咖啡店WiFi发起买卖请求。借助TLS 1.3 0-RTT,该交易无需初始握手往返,节省300ms!所有通信仍通过TLS加密。
攻击者捕获该请求并重新发送至服务器。与TLS 1.2不同,此请求不会被TLS层拒绝,买卖操作将再次执行。
关键问题:应用代码未做任何修改,但传输层的新攻击使API变得脆弱。
什么是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通过每次握手生成新密钥实现这一点。但不幸的是,由于PSK无需往返无法刷新,通过0-RTT发送的初始请求不具备前向保密性,它使用先前会话的密钥加密。
更严重的问题是0-RTT请求无法防止重放攻击。为应对此问题,应用层需要从TLS实现获取接收请求是否为0-RTT的信息。应用可通过两种方式拒绝0-RTT:在非幂等操作上拒绝0-RTT请求,或通过nonces等直接防重放机制(通过共享全局状态检查确认请求未被重复处理)。RFC 8470尝试记录此类缓解措施。
然而,实现强大防御并非易事。截至目前,Web应用通常无需了解传输安全的复杂性。Go的新TLS 1.3支持未包含0-RTT,部分原因是担心如何安全暴露该功能。Cloudflare选择仅允许无查询字符串的GET请求使用0-RTT,并通过添加标头代理额外信息。但前文示例应用仍易受攻击,因为它使用GET请求!
应对措施
首要措施:升级至TLS 1.3!这是比前代更安全、更优秀的协议。但在升级过程中,请禁用0-RTT,直至审计应用是否存在此类漏洞。如果使用带TLS终止的CDN,请阅读文档以确定他们在应用层转发的信息。否则,如果无法访问特定连接细节,需确保对敏感操作实施非常强大的防重放防御。
Web框架开发者:应认真考虑提供哪些API帮助用户管理风险,同时提供性能优势。这可能需要与框架运行的各种服务器协作,制定通用API以代理所需信息。例如,如果易受攻击应用中使用的Web框架具有幂等性注解,则以此方式注解的路由可自动启用0-RTT,而其他路由将拒绝0-RTT请求(从而自动回退到标准握手)。
直接使用TLS API(如OpenSSL)的应用:需要实现各种回调(如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)提出使用可穿刺伪随机函数显著减小会话数据库大小,但需权衡计算复杂性和泄露后安全性。截至发布时,这仍是一个活跃研究领域,尚无真正适合部署的解决方案。
常见问题解答
使用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请求。
如果您希望利用TLS 1.3的性能优势,同时确保应用和用户在0-RTT环境中安全,请联系Trail of Bits的工程和密码学团队。我们很乐意帮助您安全地设计应用。