QUIC作为HTTP2.0形成草案,提上日程以来最重要的(我认为是最重要的,如果你非要说TCP,就当我什么都没说)传输协议,它有很多可以快速秒掉TCP的特质,本文来介绍其中一个,即0RTT。
首先解释一下什么是0RTT。
所谓的0RTT就是,通信双方发起通信连接时,第一个数据包便可以携带有效的业务数据。而我们知道,这个使用传统的TCP是完全不可能的,除非你使能了TCP Fast Open特性,而这个很难,因为几乎没人愿意为了这个收益去对操作系统的网络协议栈大动手脚。未使能Fast Open的TCP传输第一笔数据前,至少要等1个RTT:
此外,对于HTTPS这种应用而言,由于还需要额外的TLS握手,0RTT就更不可能了。
如果碰到Certificate过大过长的,握手完成还不止3个RTT…
但是QUIC就可以。本文来解释一下它是怎么做的。
首先声明一点,如果一对使用QUIC进行加密通信的双方此前从来没有通信过,那么0RTT是不可能的,即便是QUIC也是不可能的,因此,我们先从这种从末谋面的通信双方开始,为了讨论的方便,我们把加密通信双方成为 S S (即)和 C C (即)而不是 A A (即)和 B B (即),毕竟本文是在讲网络,而不是在聊安全。
我略过 DH D H 算法的介绍,以保证我能在一个小时内写完本文,在切入QUIC之前,只说QUIC使用了 DH D H 算法进行密钥协商。
Step 0:配置服务器 S S 密钥对
在生成一个素数 p p 和一个整数( g g 是的一个原根,不懂可略过),同时随机生成一个数 Kpri K p r i ,计算:
Kpub=gKpri mod p K p u b = g K p r i m o d p
将 {p,g,Kpub} { p , g , K p u b } 三元组打成一个config包。
-
Step 1: C C 首次发起连接
简单地发送Client Hello到 S S 。
Step 2:首次回应 C C
用config封装成一个数据包回复给 C C ,显然内含有元组。
-
Step 3: C C 发送加密数据
收到 {p,g,Kpub} { p , g , K p u b } 后随机生成一个数 Kc_pri K c _ p r i 做如下计算:
计算公钥: Kc_pub=gKc_pri mod p K c _ p u b = g K c _ p r i m o d p
计算对称密钥: K1=KKc_pripub mod p K 1 = K p u b K c _ p r i m o d p
准备业务数据payload1,设加密函数为 Enc(key,data) E n c ( k e y , d a t a ) ,将下列元组 D1 D 1 发送给 S S :
注意,该阶段开始,payload便是加密的了。
Step 4: S S 发送加密数据
收到
D1
D
1
后,做以下计算:
计算对称密钥:
K1′=KKpric_pub mod p
K
1
′
=
K
c
_
p
u
b
K
p
r
i
m
o
d
p
可以证明,
K1′
K
1
′
和
C
C
端的是相等的:
K1′=KKpric_pub mod p=(gKc_pri mod p)Kprimod p=gKc_priKpri mod p
K
1
′
=
K
c
_
p
u
b
K
p
r
i
m
o
d
p
=
(
g
K
c
_
p
r
i
m
o
d
p
)
K
p
r
i
m
o
d
p
=
g
K
c
_
p
r
i
K
p
r
i
m
o
d
p
K1=...=gKpriKc_pri mod p=gKc_priKprimod p=K1′
K
1
=
.
.
.
=
g
K
p
r
i
K
c
_
p
r
i
m
o
d
p
=
g
K
c
_
p
r
i
K
p
r
i
m
o
d
p
=
K
1
′
因此 K1′ K 1 ′ 可解密密文 Enc(K1,payload1) E n c ( K 1 , p a y l o a d 1 ) 获取明文payload。
也许你会觉得 K1 K 1 就可以做此后通信的对称密钥了吧,然而并不是。为了所谓的前向安全性,此时 S S 会继续生成第二个对称密钥
S
S
在发送自己的payload2之前,随机生成一个数,做如下计算:
计算新的通信公钥:
Kn_pub=gKn_pri mod p
K
n
_
p
u
b
=
g
K
n
_
p
r
i
m
o
d
p
计算新的通信对称密钥:
K2=KKn_pric_pub mod p
K
2
=
K
c
_
p
u
b
K
n
_
p
r
i
m
o
d
p
有了新的通信对称密钥
K2
K
2
,就可以将下面的元组发送给
C
C
了:
这个元组
D2
D
2
中除了包含
S
S
的加密数据之外,还包括
S
S
新生成的一个公钥。
Step 5:收到 S S 的
C
C
收到发来的
D2
D
2
后,解出其中的
Kn_pub
K
n
_
p
u
b
,做如下运算:
计算新的通信密钥:
K2′=KKc_prin_pub mod p
K
2
′
=
K
n
_
p
u
b
K
c
_
p
r
i
m
o
d
p
(可以证明
K2′=K2
K
2
′
=
K
2
)
用
K2′
K
2
′
可以正确解密出payload2。
此后的通讯,
S
S
和便可以用
K2
K
2
做通信对称密钥了。
值得注意的是,这个 K2 K 2 是在1个RTT内新生成的,虽然耗费了1个RTT协商出了这个 K2 K 2 ,但是在这个RTT中业务数据却依然可以加密通信的,只不过使用的是 K1 K 1 ,即使用 C C 记忆中的端配置协商出的一个“不安全”的密钥,该密钥仅仅加密一趟数据。
Step 6:
C
C
和断开连接
S
S
和之间通信一会儿后,断开连接。
…
Step 7:
C
C
直接发送加密数据
过了一会儿或者一段时间后,又想和
S
S
通信,注意,此时已经有了
S
S
的config元组,也许是
C
C
缓存在内存了,也许是写入磁盘了,无论怎样,只要拥有
{p,g,Kpub}
{
p
,
g
,
K
p
u
b
}
,它就可以直接从Step 3开始了,也就是说直接通过
{p,g,Kpub}
{
p
,
g
,
K
p
u
b
}
以及自己生成的随机数私钥计算出一个对称密钥,然后直接发送payload了。
嗯,这就是所谓的0RTT。
Step 8: S S 发送加密数据
这里在收到 C C 的加密数据后,重复Step 4重新计算出一个新的“安全对称密钥”即可将之作为直至断开为止的对称密钥。
整个过程如下图所示:
好了,介绍完了。
所以说,QUIC的0RTT加密数据传输并非无条件的,然而请注意,QUIC的0RTT和一般意义上的Session重用思路完全不同:
- 并没有保存 C C 的任何信息;
- 连接发起的个RTT使用的密钥是临时的,在接下来的 0.5 0.5 RTT使用的密钥会被重新计算。
整个过程中,我们可以领略DH算法的一些特质。从Step 2和Step 3,我们可以看到这个算法是真正的按需协商的,也就是只有到你真正需要密钥的当即,才会进行实际的运算,这就大大减少或者说基本杜绝了离线攻击的机会,此外就是,DH算法非常简单易用。
然而,DH算法仅仅可以用来做密钥协商,对于通信双方而言,身份认证是必须的,因此,实际中的QUIC远没有上面的流程那么简单,实际涉及到的领域包括X.509证书认证,算法套件管理等等复杂的内容,很显然,本文并没有包含这些东西,本人也不是很精通这些,但我认识超级精通这些的人,而且是好几个。
据说,QUIC作为一个试验场,很多idear都会被平移到更加规范的标准中,比如BBR之于TCP(不过我是非常不看好TCP的,BBR在QUIC上持续持久发展难道不更好吗?)。同样这个0RTT的思路也将会被吸纳到在途的TLS1.3版本中,非常期待。
这一切非常感谢Google,一家伟大的公司。从看到HTTP的弊端到SPDY,然后再到HTTP2.0,进而又看到了TCP的弊端,因此从SPDY/HTTP2.0直接衍生出QUIC,子啊QUIC本身的进化过程中,对于TCP也是择其善者而从之,其不善者而改之,这就是我们现在接触到的QUIC协议,集HTTP2.0,TCP于大成的QUIC协议。
不管怎么说,我个人是比较看好QUIC的。
不多说。