TLS1.3 复用验证流程
前言
在我司写TLS1.3自动机的时候我负责的就是复用(reuse)和0-RTT的流程,这两个流程的功能介绍实质上是非常简单的,一些中间的运算可能稍微复杂,我当时是对着cavium卡和openssl 1.1.1d的代码做的研究。以后再写点openssl代码的东西吧
复用流程
复用的流程看起来是非常简单的,见下:
简单说说,我们针对复杂一些的复用过程(PSK+ECDHE流程)
- 客户端发送ClientHello报文,该报文当中携带两个关键的拓展:key_share和pre_shared_key拓展。key_share和完整的ECDHE握手时一致的,也就是计算出来的椭圆曲线公钥。而pre_shared_key包含两个部分,分别为Psk identity和PSK Binders。PSK identity用于告诉服务端:我携带的身份(identity)是啥。而PSK Binder则是利用上次链接计算出来的PSK和本次的Clienthello报文的部分经过一番计算的结果。PSK Binders的作用是验证,这个PSK identity的拥有者是我,我不是冒充的。该拓展的结构如下:
- 服务端决定接受客户端的复用,回复ServerHello消息,消息中携带两个拓展pre_share_key和key_share拓展。和客户端不同,服务端的pre_share_key拓展指示选择了第几个客户端发送过来的PSK identity。因为客户端可以发送多个PSK identity,因此需要标明选择选择的身份的序号。这个序号从0开始。
- 服务端发送EncryptedExtesnsions。该消息的拓展长度是可以为零的。我们现在说的是复用流程,如果是0-RTT流程,服务端需要在该部分发送EarlyData拓展表示接受EarlyData报文。具体哪些拓展可以在该拓展中发送我以后再写文章介绍吧。
- 服务端发送Finished报文。Finished报文本质上来说是对已经发送的内容做了一次认证。没有什么需要赘述的。
- 客户端发送Finished报文。Finished报文本质上来说是对已经发送的内容做了一次认证。没有什么需要赘述的。
复用流程中每一步的功能
尽管每一个学过公钥密码学基础的人都知道每步的作用,但我还是单摘出来这部分。毕竟我希望这篇文章帮到初学者。下面我写的序号分别对应“复用流程”中每一步的序号。
- ClientHello报文的PSK identity是明文传输的,所以任何人可以伪造。这个时候PSK Binders就可以起到认证identity的作用。首先上次连接的复用主密钥(resumption_master_secret)只有这个身份的拥有者有,那么由复用主密钥计算出来的PSK必然也只属于身份拥有者所有。从而如果Binders计算正确就证明客户端确实是该身份的拥有者,即和服务端通话的是上次连接的客户端。
- 服务端回复ServerHello消息,只是明确告诉客户端我选择了你的第X个身份,这个时候并没有直接证明服务端的身份。
- 服务端回复EncryptedExtension消息,这时候没有明确证明服务端身份。但是我们知道上次连接协商出来的PSK是只有服务端和客户端所共有的,而EncryptedExtension消息是由PSK计算出来的握手密钥保护的,因此客户端能够解密出来正常的EncryptedExtension消息就证明了服务端的身份
- 服务端发送Finished消息,该消息是对服务端Finished之前所有消息的认证。经过握手密钥保护的Finsiehd消息,证明了服务端的身份。
- 客户端发送Finished消息,该消息是对客户端Finished之前所有消息的认证。经过握手密钥保护的Finsiehd消息,证明了客户端的身份。
复用流程中的计算过程
客户端发送ClientHello中的计算过程
首先,客户端需要计算出来自己Key Share拓展的内容。该过程简单来讲是一个椭圆曲线点乘的结果,值得注意的是,这是一个非对称操作,耗时巨大。
之后,我这里假设已经计算出来了PSK的值,从复用主密钥计算出PSK的过程就不讲了。Key schedule那部分说过了。 首先需要从PSK计算出来初期密钥(Early Secret),即:
Early Secret = HKDF-Extract(0, PSK)
之后计算Binder key,如果是通过带外数据传输的PSK,标签使用”ext binder”,如果是通过上次连接建立的,标签使用”res binder”。即:
Binder Key = Derive-Secret(Early Secret, “ext binder” “res binder”, “”)
计算完了Binder key还没完,并不直接使用Binder Key计算Binder的值,还要再进行一步演算。方法和计算Finished报文的key一样,但是为了区分,姑且称之为Hmac Key。计算方法如下:
Hmac_key = HKDF-Expand-Label(Binder key, “finished”, “”, Hash.length)
演算到这步之后,所有的key都算完了。可以计算Binder了。客户端可以携带多个PSK identity,因此也要携带多个PSK Binder。每个Binder的计算都是对ClientHello报文剔除掉pre_shared_key拓展的binders部分做一次哈希的结果做HMAC。这里面暗含了个两个东西:一个是计算binder的时候包含了pre_shared_key拓展的身份部分,另一个是pre_share_key拓展在ClientHello报文的末尾。计算过程如下:
one_binder = HMAC(hmac key, Transcript-Hash(ClientHello移除掉binders的部分))
服务端发送ServerHello时
首先,服务端首先需要校验客户端的身份信息,验证Binder的有效性,验证的过程和上面是一致的。验证通过之后,服务端需要计算出来自己Key Share拓展的内容。这同样是一个非对称操作,耗时巨大。 服务端发送的ServerHello消息是明文,不需要record layer进行加密。但实现的时候我们直接把握手密钥直接算出来了。
服务端发送EncryptedExtensions
服务端发送EncryptedExtension时使用上一步计算出来的握手密钥,在record layer进行加密即可。
服务端发送Finsiehd消息
服务端首先需要利用主密钥计算出Finished使用的密钥Finish key,再对上面的所有的消息都算一次hash,再进行Hmac。即:
finished = HMAC(finish key, Transcript-Hash(ClientHello + ServerHello + EncryptedExtension))
客户端发送Finished消息
客户端首先需要利用主密钥计算出Finished使用的密钥Finish key,再对我手中上面的所有的消息都算一次hash,再进行Hmac。即:
finished = HMAC(finish key, Transcript-Hash(ClientHello + ServerHello + EncryptedExtension + server.finished))
复用流程的一些细节
我一直没想明白为什么不能直接使用binder key计算binder,而是使用binder key衍生的hmac key计算binder。从安全角度来说没想明白为啥,只是觉得方便代码迭代,函数公用?
结尾
写到这里差不多就可以结束了,TLS这块还有啥不明白的直接告诉我就成了