QUIC 是如何做到 0RTT 的

0RTT

0RTT 是指双方通信的第一个数据包就可以携带有效的业务数据。 显然,传统基于 TCP 的 HTTP 无法做到这一点

前向安全性

简单理解概念,前向安全是指用来产生会话密钥的长期密钥泄露出去,不会造成之前通讯时使用的会话密钥的泄露,也就不会泄漏以前的通讯内容。 前向安全能够保护过去进行的通讯不受密码或密钥在未来暴露的威胁。如果系统具有前向安全性,就可以保证在主密钥泄露时历史通讯的安全,即使系统遭到主动攻击也是如此。

笛福赫尔曼密钥交换

QUIC 使用了 DH 算法进行密钥协商。 DH 算法依赖离散对数这一数学困难问题,过程简单描述为,选取一个质数 p 和 p 的一个生成元 g,通讯双方分别根据自己私钥、p、g 计算出对应的公钥,并将公钥发送给对方,双方再根据对方的公钥和自己的私钥计算出通讯密钥:

至于其实现的原理,不是本文的重点,网上有很多 DH 算法的内容,我自己也写过,但是我不贴。

QUIC 的连接过程

在 client 端本地没有任何 server 端信息的时候,是无法做到 0RTT 的,下面先来梳理一下 client 首次和 server 通信的流程:

首次连接

  1. server 端生成一个质数 p 和一个整数 g,其中 g 是 p 的一个生成元,同时随机生成一个数 Ks_pri 作为私钥,并计算出公钥 Ks_pub = g^Ks_pri mod p,将 {Ks_pub,p,g} 三元组打包成 config,等待客户端连接
  2. client 首次发起连接,简单发送 client hello 给 server
  3. server 将已经生成好的 config 返回给 client
  4. client 随机生成一个数 Kc_pri 作为自己的私钥,并根据 config 中的 g 和 p 计算出公钥 Kc_pub = g^Kc_pri mod p
  5. client 计算通信使用的密钥 K = Ks_pub ^Kc_pri mod p
  6. client 用 K 加密需要发送的业务数据,并带上自己的公钥 Kc_pub 一起发送给 server
  7. server 计算 K = Kc_pub ^ Ks_pri mod p,根据笛福赫尔曼密钥交换的原理可以证明两端计算的 K 是一样的
  8. 这里不能使用 K 作为后续通讯的密钥(下面解释),server 需要生成一个新的私钥 K1s_pri,并计算新公钥 K1s_pub = g^K1s_pri mod p,然后计算新的通讯密钥 K1 = Kc_pub^K1s_pri mod p
  9. server 用 K1 加密需要返回的业务数据,并带上自己的新公钥 K1s_pub 一起发送给 client
  10. client 根据新的 server 公钥计算通讯密钥 K1 = K1s_pub ^ Kc_pri mod p,并用 K1 解密收到的数据包
  11. 之后双方使用 K1 作为密钥进行通讯,直到本次连接结束 可以看到,首次连接的时候,在第 3 步时,就已经开始发送实际的业务数据了,而第 1 步和第 2 步正好一去一回花费了 1RTT 时间,所以,首次连接的成本是 1RTT

非首次连接

Client 在首次连接后,会把 server 的 config 存下,之后再次发起连接时,因为已经有 config 了,可以直接从上面的第 3 步开始,而这一步已经可以发送业务数据了,所以,非首次连接时,QUIC 可以做到 0RTT

K1 存在的必要性

为什么要再生成一个 K1,不能直接用 K 作为后续通讯的密钥? server 的 config 是静态配置的,是可以长期使用的,其 Ks_pub 和 Ks_pri 是提前生成计算好的,为了等待后续 client 连接时计算 K,Ks_pri 是不能被销毁的。 想想上面提到的前向安全性,如果攻击者事先记录下了所有通讯过程中的数据包,而后续 server 的 Ks_pri 泄漏,那就可以根据公开的 config 算出 K,这样后续的通讯内容就全都可以解密了。而 K1 是由双方动态生成的公私钥对计算得来的,最迟在通讯结束后,双方的临时公私钥对就会销毁,从根本上杜绝了泄漏的可能。 换句话说,使用 K 作为通讯密钥,未来万一静态配置在 server 的私钥泄漏,那 K 也就泄漏了,所有历史消息都将泄漏;使用 K1 作为通讯密钥,双方私钥在短时间内就会被销毁,K1 不会泄漏,历史消息的安全性就得到了保障。