Contents

HTTP/3

Warning
本文最后更新于 February 21, 2021,文中内容可能已过时,请谨慎使用。

不得不说国外的很多文章写的都十分优秀,将技术的历史背景和细节都讲得十分出色。下面是转自 掘金翻译计划 ,这里有很多对外国优秀文章的翻译。关于HTTP/3相关的知识。下面做一些简短的记录。

  • 原文地址:HTTP/3: From root to tip
  • 原文作者:Lucas Pardue
  • 译文出自:掘金翻译计划
  • 译者:Starrier

HTTP/3 起源

HTTP/3 是 QUIC 传输层的 HTTP 应用程序映射。该名称在最近(2018 年 10 月底)草案的第 17 个版本中被正式提出(draft-ietf-quic-http-17),在 11 月举行的 IETF 103 会议中进行了讨论并形成了初步的共识。HTTP/3 以前被称为 QUIC(以前被称为 HTTP/2)。在此之前,我们已经有了 gQUIC,而在更早之前,我们还有 SPDY。事实是,HTTP/3 只是一种适用于 IETF QUIC 的新 HTTP 语法 —— 基于 UDP 的多路复用和安全传输。


这篇文章将讲述 HTTP/3 的发展历史。详细发展图见 Cloudflare Secure Web Timeline

HTTP/3 分层模型(蛋糕模型)

HTTP 演进

在我们关注 HTTP 之前,值得回忆的是两个共享 QUIC 的名称。就像我们之前解释得那样,gQUIC 通常是指 Google QUIC(协议起源),QUIC 通常用于表示与 gQUIC 不同的 IETF 标准(正在开发的版本)。


HTTP/1

HTTP/1.1 是非常成功的协议,时间线显示 1999 年以后 IETF 并不活跃。然而,事实是,多年的积极使用,为 RFC 2616 研究潜在问题提供了实战经验,但这也导致了一些交互操作的问题。此外,RFC(像 2817 和 2818)还对该协议进行了扩展。2007 年决定启动一项改进 HTTP 协议规范的新活动 —— HTTPbis(“bis” 源自拉丁语,意为“二”、“两次”或“重复”),它还采用了新的工作组形式。最初的章程详细描述了尝试解决的问题。


简而言之,HTTPbis 决定重构 RFC 2616。它将纳入勘误修订,合并在此期间发布的其他规范的一些内容。文件将被分为几个部分,这导致 2017 年 12 月发布了 6 个 I-D:


  1. draft-ietf-httpbis-p1-messaging
  2. draft-ietf-httpbis-p2-semantics
  3. draft-ietf-httpbis-p4-conditional
  4. draft-ietf-httpbis-p5-range
  5. draft-ietf-httpbis-p6-cache
  6. draft-ietf-httpbis-p7-auth

图表显示了这项工作是如何在长达 7 年的草案过程中取得进展的,在最终被标准化之前,已经发布了 27 份草案。2014 年 6 月,发布了 RFC 723x 系列(x 范围在 0-5)。HTTPbis 工作组的主席以 “RFC2616 is Dead” 来庆祝这一成果。如果它不够清楚,这些新文档就会弃用旧的 RFC 2616


SPDY

尽管 IETF 的 RFC 723x 系列的工作繁忙,但是技术的进步并未停止。人们继续加强、扩展和测试因特网上的 HTTP。而 Google 已率先开始尝试名为 SPDY(发音同 Speedy)的技术。该协议宣称可以提高 Web 浏览性能,一个使用 HTTP 原则的用例。2009 年底,SPDY v1 发布,2010 年 SPDY v2 紧随其后。


Google 对 SPDY 实验表明,改变 HTTP 语法是有希望的,维持现有 HTTP 语义是有意义的。比如,保留 URL 的使用格式 —— https://,可以避免许多可能影响采用的问题。看到一些积极的结果后,IETF 决定考虑 HTTP/2.0。2012 年 3 月 IETF 83 期间举行的 HTTPbis 会议的 slides显示了请求、目标和成功标准。它还明确指出 “HTTP/2.0 与 HTTP/1.x 连线格式不兼容”。


gQUIC 横空出世

2012 - 2015 之间,Google 继续进行试验,他们发布了 SPDY v3 和 v3.1。他们还开始研究 gQUIC(当时的发音类似于 quick),在 2012 年年初,发布了初始的公共规范。gQUIC 的早期版本使用 SPDY v3 形式的 HTTP 语法。这个选择是有意义的,因为 HTTP/2 尚未完成。SPDY 二进制语法被打包到可以用 UDP 数据报发送数据的 QUIC 包中。


gQUIC 使用巧妙的设计来实现性能优化。其中一个是破坏应用程序与传输层之间清晰的分层。这也意味着 gQUIC 只支持 HTTP。因此,gQUIC 最后被称为 “QUIC”。它是 HTTP 下一个候选版本的同义词。QUIC 从过去的几年到现在,一直在持续更新,QUIC 也被人们理解为是初始 HTTP 的变体。不幸的是,这正是我们在讨论协议时,经常出现混乱的原因。


gQUIC 继续在实验中摸索,最后选择了更接近 HTTP/2 的语法。也正因为如此,它才被称为 “HTTP/2 over QUIC”。但因为技术上的限制,所有存在一些非常微妙的差别。一个示例是,HTTP 头是如何序列化并交换的。这是一个细微的差别,但实际上,这意味着 HTTP/2 式 gQUIC 与 IETF’s HTTP/2 并不兼容。


17年-20年市面上大多商用的QUIC均是gQuic,客户端一般为cronet(chromium的网络库),服务端接入层的Nginx一般经过chromium 的封装


最后,同样重要的是,我们总是需要考虑互联网协议的安全方面。gQUIC 选择不使用 TLS 来提供安全性。转而使用 Google 开发的另一种称为 QUIC Crypto 的方法。其中一个有趣的方面是有一种加速安全握手的新方法。以前与服务器建立了安全会话的客户端可以重用信息来进行“零延迟往返握手”或 0-RTT 握手。0-RTT 后来被纳入 TLS 1.3。


什么是HTTP/3

当然,现在可以说什么是HTTP/3 了,gQUIC 并非与众不同。在2015 年 6 月的 draft-tsvwg-quic-protocol-00 中,写有 “QUIC:基于 UDP 的安全可靠的 HTTP/2 传输” 已经提交。请记住我之前提过的,几乎都是 HTTP/2 的语法。


Google 宣布将在布拉格举行一次 Bar BoF IETF 93 会议。如有疑问,请参阅 RFC 6771。提示:BoF 是物以类聚(Birds of a Feather)的缩写。

总之,与 IETF 的合作结果是 QUIC 在传输层提供了许多优势,而且它应该与 HTTP 分离。应该重新引入层与层之间清楚的隔离。此外,还有返回基于 TLS 握手的优先级

大约是一年后,在 2016 年,一组新的 I-D 集合被提交:

这里是关于 HTTP 和 QUIC 的另一个困惑的来源。draft-shade-quic-http2-mapping-00 题为 “HTTP/2 使用 QUIC 传输协议的语义”,对于自己的描述是 “HTTP/2 式 QUIC 的另一种语义映射”。但这个解释并不正确。HTTP/2 在维护语义的同时,改变了语法。而且,我很早之前就说过了,“HTTP/2 式 gQUIC” 从未对语法进行确切的描述,记住这个概念。


2016 年在柏林举行 IETF 96 会议决定了有数百人参加了这次会议。会议结束时,达成了一致的共识:QUIC 将被 IETF 采用并标准化。


之后的QUIC必将全面IETF化


将 HTTP 映射到 QUIC 的第一个 IETF QUIC I-D —— draft-ietf-quic-http-00,采用了 Ronseal 方法来简化命名 —— “HTTP over QUIC”。不幸的是,它并没有达到预期效果,整个内容中都残留有 HTTP/2 术语的实例。Mike Bishop —— I-D 的新编辑,发现并修复了 HTTP/2 的错误名称。在 01 草案中,将描述修改为 “a mapping of HTTP semantics over QUIC”。

随着时间和版本的推进,“HTTP/2” 的使用逐渐减少,实例部分仅仅是对 RFC 7540 部分的引用。从 2018 年 10 月开始向前回退两年的时间开始计算,I-D 如今已经是第 16 版本。虽然 HTTP over QUIC 与 HTTP/2 有相似内容,但始终是独立的(非向后兼容的 HTTP 语法)。然而,对那些不密切关注 IETF 发展的人来说(人数众多),他们并不能从名称中发现一些细微的差异。标准化的重点之一是帮助通信和互操作性。但像命名这样的简单事件,才是导致社区相对混乱的主要原因。 回顾 2012 年的内容,“HTTP/2.0 意味着 wire 格式与 HTTP/1.x 格式不兼容”。IETF 遵循现有线索。IETF 103 是经过深思熟虑才最终达成一致的,即:“HTTP over QUIC” 命名为 HTTP/3。互联网正在促使世界变得更加美好,我们可以继续进行更加重要的的探讨。


总结

概况来说就是:HTTP/3 只是一种适用于 IETF QUIC 的新 HTTP 语法 —— 一种基于 UDP 多路复用的安全传输层。仍有许多有趣的领域需要深入探索。

参考文章

QUIC的实现

QUIC 握手


在2016年11月国际互联网工程任务组(IETF)召开的第一次QUIC工作组会议,受到了业界的广泛关注。这也意味着QUIC开始了它的标准化过程,成为新一代传输层协议,形成了最新的iQUIC。IETF在QUIC的加密协议上就放弃了google的加密协议使用了标准的TLS1.3。

QUIC 握手

QUIC 连接的建立整体流程大致为:QUIC在握手过程中使用Diffie-Hellman算法协商初始密钥,初始密钥依赖于服务器存储的一组配置参数,该参数会周期性的更新。初始密钥协商成功后,服务器会提供一个临时随机数,双方根据这个数再生成会话密钥。客户端和服务器会使用新生的的密钥进行数据加解密。

以上过程主要分为两个步骤:初始握手(Initial handshake)最终 与重复 握手(Final (and repeat) handshake) ,分别介绍下这两个过程。

初始握手(Initial handshake)

在连接开始建立时,客户端会向服务端发送一个打招呼信息,(inchoate client hello (CHLO)),因为是初次建立,所以,服务端会返回一个拒绝消息(REJ),表明握手未建立或者密钥已过期。

但是,这个拒绝消息中还会包含更多的信息(配置参数),主要有:

  1. Server Config:一个服务器配置,包括服务器端的Diffie-Hellman算法的长期公钥(long term Diffie-Hellman public value)
  2. Certificate Chain:用来对服务器进行认证的信任链
  3. Signature of the Server Config:将Server Config使用信任链的叶子证书的public key加密后的签名
  4. Source-Address Token:一个经过身份验证的加密块,包含客户端公开可见的IP地址和服务器的时间戳。

在客户端接收到拒绝消息(REJ)之后,客户端会进行数据解析,签名验证等操作,之后会将必要的配置缓存下来。 同时,在接收到REJ之后,客户端会为这次连接随机产生一对自己的短期密钥(ephemeral Diffie-Hellman private value) 和 短期公钥(ephemeral Diffie-Hellman public value)。

之后,客户端会将自己刚刚产生的短期公钥打包一个Complete CHLO的消息包中,发送给服务端。这个请求的目的是将自己的短期密钥传输给服务端,方便做前向保密。


在发送了Complete CHLO消息给到服务器之后,为了减少RTT,客户端并不会等到服务器的响应,而是立刻会进行数据传输。

为了保证数据的安全性,客户端会自己的短期密钥和服务器返回的长期公钥进行运算,得到一个初始密钥(initial keys)。接下来他接收到客户端使用初始密钥加密的数据之后,就可以使用这个初识密钥进行解密了,并且可以将自己的响应再通过这个初始密钥进行加密后返回给客户端。

最终(与重复)握手

那么,之后的数据传输就可以使用初始密钥(initial keys)加密了吗? 其实并不完全是,因为初始密钥毕竟是基于服务器的长期公钥产生的,而在公钥失效前,几乎多有的连接使用的都是同一把公钥,所以,这其实存在着一定的危险性。 所以,为了达到前向保密 (Forward Secrecy) 的安全性,客户端和服务端需要使用彼此的短期公钥和自己的短期密钥来进行运算。

那么现在问题是,客户端的短期密钥已经发送给服务端,而服务端只把自己的长期密钥给了客户端,并没有给到自己的短期密钥。 所以,服务端在收到Complete CHLO之后,会给到服务器一个server hello(SHLO)消息,这个消息会使用初始密钥(initial keys)进行加密。

这个CHLO消息包中,会包含一个服务端重新生成的短期公钥。 这样客户端和服务端就都有了对方的短期公钥(ephemeral Diffie-Hellman public value)。 这样,客户端和服务端都可以基于自己的短期密钥和对方的短期公钥做运算,产生一个仅限于本次连接使用的前向保密密钥 (Forward-Secure Key),后续的请求发送,都基于这个密钥进行加解密就可以了。 这样,双方就完成了最终的密钥交换、连接的握手并且建立了QUIC连接。 当下一次要重新创建连接的时候,客户端会从缓存中取出自己之前缓存下来的服务器的长期公钥,并重新创建一个短期密钥,重新生成一个初识密钥,再使用这个初始密钥对想要传输的数据进行加密,向服务器发送一个Complete CHLO 请求即可。这样就达到了0 RTT的数据传输。


所以,如果是有缓存的长期公钥,那么数据传输就会直接进行,准备时间是0 RTT

以上,通过使用Diffie-Hellman算法协商密钥,并且对加密和握手过程进行合并,大大减小连接过程的RTT ,使得基于QUIC的连接建立可以少到1 RTT甚至0 RTT。

下面是建立握手的完整过程


小结

QUIC的通讯过程在初次没有建立过连接时使用1-RTT的握手机制,同时保证连接的建立和达到安全的保障。以下是QUIC的1-RTT的握手过程:

  1. Server端会持有0-RTT公私钥对,并且生成SCFG(服务端的配置信息对象),把公钥放入SCFG中;
  2. 客户端初次请求时,需要向服务端获取0-RTT公钥,这个需要消耗一个RTT,这也QUIC的1-RTT的所在;
  3. 客户端在收到0-RTT公钥以后会缓存起来,同时生成自己的临时公私钥对,经过前面的一个RTT后客户端把自己的临时私钥与服务端发过来的0-RTT的公钥根据DH算法生成一个加密密钥K1,同时使用K1加密数据同时附送自己的临时公钥一起发送服务端,此时已有用户数据发送;
  4. 在服务端收到用户使用K1加密的用户数据和客户端发来的临时公钥以后,会做如下几件事:
    • 使用0-RTT私钥与客户端发来的临时公钥通过DH算法生成K1解密用户数据并递交到应用;
    • 生成服务端临时公私钥对,使用临时公私钥对的私钥,与客户端发来的客户端临时公钥,生成K2加密服务端要传输的数据
    • 把服务端的临时公钥和使用K2加密的应用数据发送到客户端
  5. 客户端收到服务端发送的服务端临时公钥和使用K2加密的应用数据后会再次使用DH算法把服务端的临时公钥和客户端原来的临时私钥重新生成K2解密数据,并且从此以后使用K2进行数据层的加解密
  • 1RTT握手

0-RTT是QUIC一个很关键的属性,能够在连接的第一个数据报文就可以携带用户数据。但是我们也可以看到如果客户端和服务端从来没有通讯过,那么是不存在0-RTT的,需要一个完成的RTT之后才能承载用户数据。

  • 0RTT握手

这个流程是gQUIC的流程,iQUIC由于使用的是TLS1.3,握手阶段报文的细节会有些不一样,例如首个请求的是证书、PSK等信息。在0-RTT阶段使用的是session复用的ticket方式。

gQUIC使用的是gQUIC Crypto,并不是TLS1.3

  • 安全考虑

UDP的安全性存在的几个关键的地方,源地址欺骗攻击,UDP放大攻击等。在QUIC中有设计了源地址TOKEN(STK)验证的安全机制来解决源地址的欺骗攻击,在通讯过程中服务器要求确认客户端的源地址TOKEN,这个源地址TOKEN根据数据包的源地址和服务器的时间戳等因子生成STK,随后和响应数据包一起发送到客户端,而在后续的数据传输过程中客户端需要透传这个STK到服务端,从而服务端可以进行校验。当服务端发现连接对应的源地址发送变化时会主动发送RETRY报文进行服务端主动源地址验证。客户端也可以主动发起源地址验证信息。源地址验证可以保护两类攻击问题,源地址欺骗攻击和UDP放大攻击。

  1. 连接建立时,为了验证客户端的地址是否是攻击者伪造的,服务端会生成一个令牌(token)并通过重试包(Retry packet)响应给客户端。客户端需要在后续的初始包(Initial packet)带上这个令牌,以便服务端进行地址验证。

  2. 服务端可以在当前连接中通过 NEW_TOKEN 帧预先发布令牌,以便客户端在后续的新连接使用,这是 QUIC 实现 0-RTT 很重要的一个功能。

  3. 当我们的网络路径变化时(比如从蜂窝网络切换到 WIFI),QUIC 提供了连接迁移(connection migration)的功能来避免连接中断。QUIC 通过路径验证(Path Validation)验证网络新地址的可达性(reachability),防止在连接迁移中的地址是攻击者伪造的。