写点什么

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:563674
用户头像

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

关注

评论

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

软件测试/测试开发丨Web自动化测试中显式等待与隐式等待该怎么用

测试人

软件测试 自动化测试 测试开发

既然有了MySQL,为什么还要有MongoDB

Java你猿哥

Java MySQL 数据库 mongodb Java工程师

慕了!17年阿里架构师把Spring Boot的精髓都总结出来了

Java你猿哥

Java spring Spring Boot Spring MVC Java工程师

龙智被SmartBear评为2022年“最具动力营销团队”

龙智—DevSecOps解决方案

自动化测试 UI测试 UI测试自动测试

集成华为运动健康服务干货总览

HarmonyOS SDK

HMS Core

HummerRisk 使用教程:镜像检测

HummerCloud

镜像安全 云原生安全

研发效能 | DevOps如何改变游戏公司工作方式?

龙智—DevSecOps解决方案

DevOps 游戏开发

AI大模型已经出现不可预测的能力

Baihai IDP

人工智能 深度学习 NLP 大模型 ChatGPT 企业号 4 月 PK 榜

机器学习实战系列[一]:工业蒸汽量预测(最新版本下篇)含特征优化模型融合等

汀丶人工智能

人工智能 数据挖掘 机器学习 LightGBM

“ONE”有引力,4月21日见!

博睿数据

智能运维 博睿数据 发布会 Bonree ONE

如何选择合适的云数据库架构与规格

NineData

数据库 阿里云 AWS RDS 数据库架构设计

版本控制 | 告别繁琐,P4VJS带来全新的Diff体验

龙智—DevSecOps解决方案

版本控制 版本管理

Web2D工业组态工具软件——Sovit2D

2D3D前端可视化开发

web组态 组态编辑器 工业组态软件 web组态软件 2D组态

KaiwuDB 成为中国信通院数据库应用创新实验室-汽车行业工作组副组长单位

KaiwuDB

车联网 KaiwuDB 数据库行业标准制定 汽车工作组

你kin你擦!阿里终于肯把内部高并发编程高阶笔记开源出来了

Java你猿哥

Java nginx 高并发 SpringCloud 面经

selenium源码通读·1 | 源码目录

Python 源码 自动化测试 selenium

【ChatGPT系列话题】金融行业大语言模型应用落地

易观分析

人工智能 金融 模型

Hive 和 Spark 分区策略剖析

vivo互联网技术

spark hive

DAMS大会 | 博睿数据分享《一体化智能可观测平台建设之路》

博睿数据

可观测性 智能运维 博睿数据 Bonree ONE

龙智荣获Perforce公司颁发的2022年度销售与技术两项大奖

龙智—DevSecOps解决方案

版本控制

以 100GB SSB 性能测试为例,通过 ByteHouse 云数仓开启你的数据分析之路

字节跳动数据平台

大数据 数据仓库 云原生 数据仓库服务 云数仓

PyTorch深度学习实战 | 预测工资——线性回归

TiAmo

深度学习 线性回归 PyTorch 梯度下降法

超实用VS Code for Windows快捷键

SEAL安全

vscode 企业号 4 月 PK 榜

如何打造企业专属A/B平台?火山引擎DataTester开放平台技术揭秘

字节跳动数据平台

大数据 AB testing实战 开放平台 企业号 4 月 PK 榜 企业增长

户外LED显示屏对恶劣环境的防护措施!

Dylan

LED显示屏 全彩LED显示屏 户外LED显示屏

一种元数据同步的方法

KaiwuDB

数据复制 KaiwuDB 元数据同步

Atlassian Server用户新选择 | 迁移到数据中心版前,您需要做这些准备(2)

龙智—DevSecOps解决方案

Atlassian Atlassian 云版 数据中心版 server版

免费可商用开源GPT模型问世,50G权重直接下载,性能不输GPT-3

Openlab_cosmoplat

开源项目 开源社区

华大北斗高精度芯片助力上汽名爵MG7智能驾驶

江湖老铁

又是一季金三银四,Spring之AOP知识要点总结

Java你猿哥

spring Spring Boot ssm aop

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