写点什么

Facebook 为快速安全的移动连接打造零往返协议

  • 2017-02-07
  • 本文字数:6618 字

    阅读完需:约 22 分钟

每天都有数十亿人在 Android 和 iOS 设备上通过 Facebook 与朋友建立联系。对我们移动应用和服务器之间传输的数据提供保护,可以帮助人们更安全地使用 Facebook。

我们的移动应用使用了一种名为 Mobile Proxygen 的自定义网络栈,这是一种使用 C++14 开发的跨平台 HTTP 客户端,使用我们自己的开源 Proxygen 库构建而来。借此我们可以在服务器和客户端之间共享同一套代码,更快速地提供新的安全和性能改进。

我们在移动应用中使用了传输层安全(TLS)1.2 协议,并使用带 OpenSSL 的 Folly 作为 TLS 的具体实现。由于增加了至少一轮往返,TLS 1.2 会延长建立连接所需的时间,为了降低 TLS 的延迟,过去多年来人们提出了多种新的协议和改进。Facebook 传输安全团队曾通过多种方式设法尽可能改善 TLS 的速度,包括通过技术手段在距离用户最近的边缘位置终止 TLS 连接,重复使用 HTTP2 连接,使用会话重用(Session resumption)和 TLS 抢跑(False start),推断式连接启动,以及使用现代化的 Cipher 套件。从我们的移动应用到 Facebook 建立的大部分 TLS 连接仅额外增加了一轮往返(1-RTT)。

回顾一年前的数据,我们发现在建立连接的过程中,1-RTT 优化后的安全握手过程依然需要较长的时间。例如在印度等新兴市场,用户往往要花费 600ms(75 百分位数)的时间才能建立 TLS 连接。我们认为有必要采取一些措施,降低这些请求的延迟,进而减少建立安全连接所需的时间。

我们打算使用零往返(0-RTT)安全协议进行一些实验。与 TLS 1.2 等 1-RTT 安全协议不同,这些协议意在确保安全性,并且不产生额外的往返延迟的前提下建立安全的连接。TCP 已经深度融入到我们的基础架构中,为了避免一次性对整个基础架构进行较大的调整,我们希望逐步进行这样的实验。同样基于 TCP 协议的 TLS 1.3 目前提供了 0-RTT 功能,然而在我们研究各类选项的时候,TLS 1.3 还处于萌芽状态,暂未提供 0-RTT 功能。此外还可以选择基于 UDP 的 QUIC,这也是一种 0-RTT 协议,在分析过该协议的安全模式后,很多学术机构对该协议的加密模式产生了一定的关注。我们希望让基于 UDP 的 QUIC 所具备的低延迟特性能够适用于 TCP,借此更快速地建立安全连接,因此我们使用 QUIC 加密协议构建了一个基于 TCP 的实验性零往返协议。

过去一年来,我们已经为移动应用和负载均衡器构建并部署了零往返协议,并获得了显著的性能改进,例如连接延迟降低了 41%,处理请求的总时间降低了 2%。在有关 0-RTT 协议的实践过程中,我们还收获了很多宝贵的工程经验,例如 API 设计、安全属性,以及部署,并将我们的一些成果贡献给了业已成熟的 TLS 1.3。希望我们通过本文分享的经验也能适用于未来打算部署 TLS 1.3 的应用。

对 QUIC 协议进行的改动

为了使其更加安全和高效,我们在零往返协议中对 QUIC 加密模式进行了大量改动。此外我们还设法让该协议可以通过 TCP 运行。因此可以认为,我们的零往返协议是在原本 QUIC 加密规范基础上进行的一系列改进。本节将介绍有关加该协议密码学的相关细节,以及帮助大家理解这一加密模式所需掌握的相关知识。

概括来看,QUIC 的加密协议是这样工作的:如果某个客户端以前从未与服务器通信过,会发送一则 Inchoate Client Hello 消息并通过 1-RTT 下载一个名为 Server Config(SCFG)的暂存消息。该消息中包含一个 Diffie-Hellman 共享,下一次客户端将使用该共享派生初始密钥(或 0-RTT 密钥),并立刻使用该密钥加密数据。1-RTT 完成后,服务器将发出一个新的暂存 Diffie-Hellman 共享,借此派生出一组名为前向(Forward)安全密钥的新密钥。

QUIC 密钥派生过程的变化

原始的 QUIC 规范包含两种类型的密钥:

  • 初始密钥(或 0-RTT 密钥),用于发送初始数据,可从长存的服务器配置中派生而来。
  • 前向安全密钥(或 1-RTT 密钥),用于在服务器向客户端发送 Server Hello 消息后传输数据所用。

客户端发送 Client Hello(CHLO)后,服务器使用加密的 Server Hello(SHLO)消息作为回应。其中包一组新的公钥(PUBS),这是一种可用于派生出 Forward 安全密钥的 Diffie-Hellman 共享。该消息会使用初始 0-RTT 密钥进行加密,服务器通过正确解密 SHLO 可成功完成身份验证。

然而我们发现这种密钥派生方法存在密钥被重复使用的弱点。初始密钥以及对 SHLO 进行的现时(Nonce)加密完全是通过 Client Hello 消息派生而来的,因此如果攻击者“重播”相同的 CHLO,服务器会使用相同密钥对不同的 SHLO 消息进行现时加密。AEAD 密码算法的安全特性被破坏了,进而威胁到 QUIC 的安全性,除非我们能通过额外的有状态方法检测相同的 CHLO 消息。

在零往返协议中,我们引入了另一种通过明文方式传输的现时机制,并会通过一个新的密钥加密 SHLO。此外我们也已经将该弱点报告给谷歌,他们为 QUIC 提供的“多样化现时(Diversification nonce)”解决了这个问题。

带内服务器配置轮换

我们对服务器配置(Server Config)的有效时间进行了限制,原因在于,如果该配置在有效时段内被盗,将能被一直用于冒充服务器。在 QUIC 协议中,让包含已缓存老旧 SCFG 的客户端使用新 SCFG 的唯一方法是继续使用原来的 SCFG,被拒绝,然后获得新的 SCFG。这种方法的不足之处在于,如果客户端发送了 0-RTT 数据,在轮换配置时必须丢弃这些数据并进行重播。

我们对协议进行了改进,使得我们可以在带内(In-band)直接发送新的 SCFG。服务器随时维护着包含三个配置的清单:上一个配置、当前配置,以及下一个配置。如果检测到客户端在使用老的 SCFG,我们会让客户端完成连接,随后通过加密的 SHLO 为客户端提供新的 SCFG,进而客户端可以将自己的 SCFG 更新为新版本。这种方式可以避免客户端因为使用老旧配置而被拒绝的退化情况。

TLS 1.2 还提供了一种通过刷新会话票证(Session Ticket)实现相同目的的做法,然而这种方法无法保障前向安全,因为新老会话票证会共享同一个主密钥。在 QUIC 协议中,新密钥需要另一个 Diffie-Hellman 操作,刷新后可以保证前向安全。

被拒后重试行为的安全性

就算通过带内的方式刷新服务器配置,依然会遇到客户端继续使用老配置的情况。此时无法避免要拒绝客户端并回退至 1-RTT,但连接依然无法防范重播。客户端可以发送 0-RTT 数据,但不能立刻发送常规数据。

我们对零往返协议进行了性能优化,可以在客户端的服务器配置被拒绝的时间段内向客户端发送额外的服务器现时,这样即可使用该现时,立即开始发送常规的 1-RTT 非重播安全数据。

0-RTT 数据的时效

相比通过 1-RTT 或 TLS 1.2 等协议发送的常规数据,0-RTT 数据有着不同的安全特性。与常规的 1-RTT 数据不同,攻击者可以无穷尽地重播 0-RTT 数据,如果应用无法妥善地保护自己,这会造成一种非常有意思的攻击。例如,攻击者可以将一个 HTTP POST 请求重播两次,并在缺乏遏制机制的情况下让该请求被执行两次。攻击者还可以将发往银行的同一个 GET 请求重播任意次数,通过查看响应的长度判断银行账户余额的变化情况。0-RTT 数据必须以截然不同的方式妥善应对。1-RTT 完成后,客户端将可以发送任何数据,因为连接又可以防范重播了。

我们使用的一种缓解措施是减小 0-RTT 的有效时长。客户端可以向我们发送启动连接的时间,我们会将该时间与服务器时间进行对比,以确定该 0-RTT 数据是在多久之前创建的。如果 0-RTT 数据在有效期过期之后重播,服务器将拒绝这样的数据,借此禁止攻击者无穷尽地重播这些数据。然而随着降低有效期,我们发现很多客户端的时钟存在较大偏差,进而产生了很多误报。

为了解决这个问题,当客户端成功连接后,我们会下发一个时钟偏差校正值。客户端下一次连接时,需要这样计算自己的客户端时间:

复制代码
client_time = client_real_time + clock_skew_correction

我们还发现客户端的时钟偏差存在一定的方差,但由于该方差并不是那么大,因此可以强制实施严格的 0-RTT 数据有效期。

对 TCP 的修改

为了兼容 TCP,我们在零往返协议中取消了 QUIC 数据包的显式序列编号,并增加了显式长度字段。QUIC 是基于 UDP 的,因此不需要长度字段,而由于 UDP 数据包可以重新排序,因此必须具备显式序列编号。

零往返协议的部署

端口的选择

我们决定将零往返协议运行在与 TLS 相同的 443 端口上。在服务器端,我们在已接受套接字(Accepted socket)上使用 MSG_PEEK 预览连接的前几个字节内容,并确定是要使用 TLS 或是零往返协议。我们还需要通过更细化的方法让客户端决定是否使用零往返协议,因此决定不使用 Alt-svc。

Zero RTT API

我们还面临一个重大的问题:如何将 0-RTT 集成于 Mobile Proxygen。网络栈是一种复杂的猛兽,而我们非常有必要确保 0-RTT 以后必须能轻松地测试和维护。

我们考虑过两种可行的 API:

  • 更改原有的套接字 API,让connect()也能接受数据,例如connectWithData (ip, port, data)
  • 让客户端继续使用相同的connect()write()套接字 API。为了启用 0-RTT,客户端可调用新的enableZeroRTT()API。随后我们可以立即将调用返回至connect(),这样客户端就可以使用write()写入 0-RTT 数据。

在考虑如何将 0-RTT 集成到客户端时,我们发现以我们这种复杂度的网络栈来说,很难用可行的方式集成第一种 API。该 API 实际上破坏了建立和使用连接的不同组件之间的分隔。诸如 HTTP 等组件本身是通过连接发送数据的,但也需要负责处理部分连接逻辑,这会使这些组件变得更复杂。这种方式还会妨碍数据的流动,例如,如果整个网络的 RTT 有较大差异,就很难判断需要等待缓冲多少数据再调用connectWithData

因此我们选择构建第二种 API。网络栈的其他部分可以像以前一样使用相同的 API。这种方法的一个不足在于,整个方法的复杂度被转移到零往返协议本身的实现中,因为需要处理 0-RTT 的状态。但该方法的优势在于,可以让我们在获得 Server Hello 消息之前持续流传输 0-RTT 数据。RTT 的方差非常大,因此我们可以根据经验估算在等待服务器发送 Server Hello 的同时,我们可以发送多少数据,这就产生了 1-RTT。此外我们发现该数据本身的方差也很大。如果不使用流式 API,Mobile Proxygen 就必须精确判断在绑定一个 0-RTT connectWithData()之前,必须等待应用生成多少数据,这一点实现起来也很复杂。由于数据方差大,我们不需要流式 API 事先判断要等待的数据量,因此部署起来更简单,也更高效。

选择对 0-RTT 来说不会造成危险的请求

在决定构建流式 API 后,我们需要构建一种机制,以确保只通过 0-RTT 发送安全的请求。通过 0-RTT 发送非幂等请求是一种不安全的做法,因为攻击者可以重播这种请求,但就算幂等的请求,这样做也可能不够安全。举例来说,如果有个 GET 事务可以返回银行账户余额,攻击者可以将这样的 0-RTT 请求重播多次,通过查看回应的长度判断余额的变化情况。

只有应用本身的代码可以真正确定通过 0-RTT 发送数据是否是安全的做法。

因此我们为 Mobile Proxygen 增加了一个 API:

复制代码
setRequestIdempotency(RETRY_SAFE).

该 API 可以告诉网络栈数据不仅可以通过 0-RTT 安全地发送,而且可以执行其他操作,例如重试请求。我们与 HTTP 工作组就这个 API 进行了讨论,一致认同“重试安全”是一个必要的特性。我们只通过 0-RTT 发送符合重试安全要求的请求,并且只在应用的代码明确指定这是一种安全做法的情况下执行这种操作。而各种浏览器的计划是通过 0-RTT 发送所有数据。

决定发送“重试安全”请求的时机

我们的产品可以一次发送多种不同类型的请求。一旦确定了哪些请求是重试安全的,我们还需要知道什么时候可以安全地发送非重试安全的请求。在发送重试安全的请求,而非发送非重试安全请求的过程中,零往返协议的套接字必须处于一种特性状态下,因为此时还没有得到来自服务器的回应。

我们希望确保整个抽象尽可能简单,并且避免在内存中缓冲太多的数据。

在 Mobile Proxygen 中我们构建了一个可根据多种条件对请求进行调度的请求调度器。例如高优先级请求会比低优先级请求更快速进行调度。我们还为重试安全请求提供了一个自定义的请求调度器,如果某个请求是非重试安全的,并且传输工作尚未开始执行 1-RTT,重试安全调度器会阻止这种请求调度自己的头部或正文,将其保留在队列中。

当传输符合重试安全要求时,重试安全调度器会得到一个回调,此时可以安全地调度非重试安全请求,并释放请求队列。

数据交付的可靠性

在对 TLS 和零往返协议进行性能分析对比时,我们发现 TLS 连接的错误率比零往返协议略低一些。通过我们自己的网络栈数据,我们发现大部分请求错误发生在建立连接的过程中。由于使用了流式 API,当我们知道可以发送 0-RTT 加密数据后,我们会立刻将零往返协议连接返回给 Mobile Proxygen。在网络栈获得能够发送数据的连接时,零往返连接只进行了一次 TCP 往返,而 TLS 此时已经进行了两次往返(包括 TCP)。

提到这个事情是因为,在建立连接时,网络栈会试图打开多个连接。TLS 连接成功概率高于零往返连接是因为 TLS 连接会等待更多的往返,实际上这等同于进行了额外的连接重试。

为了让零往返协议与 TLS 实现相似的结果,我们在 Mobile Proxygen 中增加了重试行为,借此在我们知道请求在获得服务器回应前就已失败的情况下加快重试速度。该方法提高了零往返连接的可靠性,同时也能让 TLS 1.3 客户端从中获益。

为了适应不同的中间设备(Middlebox),我们还构建了从零往返协议到 TLS 1.2 的回退,但实际上这些设备的使用并不广泛,主要出现在少数几个 ASN 中。

重播缓存

缩短 0-RTT 数据有效期的时间窗口可以大幅降低攻击者无止境重播 0-RTT 请求可能造成的风险。然而在这个时间窗口内,依然有可能多次重播请求,因此攻击者依然有可能用统计学的方式分析回应的时间,进而对请求获得进一步了解。为了防范这种问题,我们实验了重播缓存,该技术可对每个时间窗口内发送的 0-RTT Client Hello 进行缓存,进而拒绝重复的消息。重播缓存并不能彻底禁止重播,毕竟我们的目标是让客户端自动将被拒绝的 0-RTT 请求以 1-RTT 数据的方式重新发送,但该技术可以将重播的次数限制为客户端的重试次数。通过使用 Bloom 筛选器,我们的重播缓存可以用最少量资源处理大量握手,而代价仅仅是很少量的误报率。我们尚未在零往返协议中全面启用重播缓存(对于零往返协议,我们可以细化地控制哪些请求可作为 0-RTT 数据发送,因为我们可以控制客户端选择重试安全请求的代码),不过我们认为可以在部署 TLS 1.3 时开始部署重播缓存。

收益

性能

相比 TLS 1.2,零往返协议有了显著的性能改进。我们发现建立连接所需的时间降低了 41%(75 百分位数),请求处理总时间整体减少了 2%。各种请求有着自己的差异,而零往返协议对应用启动时因为无法重复使用连接而发出的请求能带来最大价值。这样改进也让我们应用的冷启动速度有了飞速提升。

针对 TLS 1.3 的贡献

零往返协议目前还是实验性的,但如我们预期,在性能改进放面取得了非常大的成功。从我们的 Android 和 iOS 应用中产生的大部分流量已经在使用零往返协议。同时我们还将这一过程中获得的经验贡献给了 TLS 1.3 和 QUIC。例如,TLS 1.3 中的票证寿命功能就得到了零往返协议的启发。我们还在 TRON 2 上介绍了自己的 API 设计,并就流传输功能进行了讨论,借此服务器无需等待上一个数据传输操作完成,即可开始发出响应。为了明确 0-RTT 对浏览器的影响,我们还针对重试安全进行了多次讨论。希望真个社区在未来可以通过 TLS 1.3 获得类似的性能收益。

未来计划

我们的传输安全团队正在构建自己的 TLS 1.3 实现,并会在可行时纳入零往返协议。我们认为在大量社区成员的贡献下,TLS 1.3 的协议设计非常出色。TLS 1.3 不仅改善了性能,同时提供了一种更简单并且更安全的设计。我们非常期待着在不远的未来能够实现并部署该协议。

相比 TLS 1.3,我们更愿意让零往返协议成为一种实验和探索。我们为零往返协议进行的大部分工程抽象和设计都会立刻应用到 TLS 1.3 中。

任何在意安全性和性能的应用都应该考虑使用 TLS 1.3,并考虑本文中提到的有关 0-RTT 数据的问题。零往返协议帮助我们更好地理解了 0-RTT 数据的影响,借此我们也对 TLS 1.3 的开发做出了自己的贡献。

扩展阅读

浏览器的简单小调整,减少了Facebook 60% 的请求

作者 Subodh Iyengar , Kyle Nekritz 阅读英文原文 Building Zero protocol for fast, secure mobile connections


感谢郭蕾对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2017-02-07 16:563227
用户头像

发布了 283 篇内容, 共 107.6 次阅读, 收获喜欢 62 次。

关注

评论

发布
暂无评论
发现更多内容

世优科技AI数字人产品“世优BOTA”发布!全面提升AI虚拟员工能力

联营汇聚

constchar*类型的实参与char*类型的形参不兼容

linux大本营

指针 const C++

文本搜索工具ack与grep

坚果

Linux 三周年连更

为开发者搭建创新舞台 OpenHarmony创新赛正式启动

最新动态

nvmf协议

linux大本营

网络协议 nvme RDMA技术

详细讲解spdk里的examples/nvmf/nvmf/nvmf.c文件

linux大本营

nvme 文件存储

写一个epoll的服务端程序

linux大本营

事件驱动 epoll C++ I/O 多路复用

FBEC大会 | 瑞云科技 CTO 赵志杰:元宇宙时代的基础设施——实时云渲染

3DCAT实时渲染

实时渲染 实时云渲染 实时渲染云

发送GET请求时,浏览器请求的参数是指什么

linux大本营

HTTP url GET

Logrotate正则匹配文件名

linux大本营

正则表达式 Linux

【源码分析】【seata】at模式分布式事务-tm实现逻辑

如果晴天

源码分析 分布式事务 seata Seata框架

九层天塔DApp合约系统开发搭建

薇電13242772558

NFT

再写一个版本,要求使用类进行封装,提供完善的接口,并对函数进行完善注释

linux大本营

RabbitMQ AMQP C++

使用Plist编辑器——简单入门指南

雪奈椰子

linux如何计算一个shell程序运行的时间

linux大本营

Shell Linux系统

Apple 的 plist 编辑器入门指南:基础操作与高级功能详解

雪奈椰子

DevOps 与研发效能资深技术专家张乐:研发效能的升维思考与降维执行

万事ONES

C语言system函数判断返回错误

linux大本营

C语言

已知非空线性链表由list指出,链结点的构造为(data,next)。写-算法,将链表中数据域值最小的那个链结点移到链表的最前面。要求:不得额外申请新的链结点

linux大本营

链表 C语言 数据结构与算法

一个能在Linux下使用C++编写的访问RabbitMQ接收发送数据的可编译代码

linux大本营

RabbitMQ 消息队列 C++

浅析低代码开发的典型应用构建场景

力软低代码开发平台

qemu怎么用

linux大本营

qemu

linux如何设置一个环境变量,在shell退出之后也不会失效

linux大本营

Linux Shell export

敏捷宣言和原则

老王同学

读书笔记

PerformingTestCOMPILER_KNOWS_STDLIB-Failed

linux大本营

cmake C++

使用Plist编辑器——简单入门指南

终于有人把动态规划、冒泡排序、二叉树、链表、栈全部讲清楚了

收到请回复

程序员 java

构建数据中台——数据只有被使用起来,才能创造价值

引迈信息

数据库 数据中台 低代码 JNPF 数字化、操作系统、中台

seq命令

linux大本营

Shell 脚本

编程中最难的就是命名?这几招教你快速上手

阿里巴巴云原生

阿里云 编程 云原生

logrotate怎么使用

linux大本营

Linux 日志管理

Facebook为快速安全的移动连接打造零往返协议_移动_Subodh Iyengar_InfoQ精选文章