首先我还是想说一下这个扩展:
struct {
uint32 ticket_lifetime;
uint32 ticket_age_add;
opaque ticket_nonce<0..255>;
opaque ticket<1..2^16-1>;
Extension extensions<0..2^16-2>;
} NewSessionTicket;
关于每个字段的含义我就不解释了,在TLS1.3概述这篇博客中有详细的解释。
有人可能会问:为什么要说这个呢?
在之前的一篇博客中一直在说TLS1.3中的密钥计算相关的问题,其中我说到了PreSharedKeyExtension
这个扩展里面的binder_key
是由PSK
计算出的Early Secret
得来,
struct {
opaque identity<1..2^16-1>;
uint32 obfuscated_ticket_age;
} PskIdentity;
NewSessionTicket
中的ticket
。HKDF-Expand-Label(resumption_master_secret,
"resumption", ticket_nonce, Hash.length) =PskIdentity.identity = ticket
在PreShareKey
中 PskIdentity
和 PskBinderEntry
结合成 PSK
。
我们可以看到使用了resumption_master_secret
这个密钥,这就和之前的内容联系起来了,我们知道这个密钥是由Master Secret
经Derive-Secret
函数计算得来:
0 -> HKDF-Extract = Master Secret
|
|
+-----> Derive-Secret(., "res master",
ClientHello...client Finished)
= resumption_master_secret
ticket_nonce
就是NewSessionTicket
扩展中对应的字段,因为 ticket_nonce
值对于每个 NewSessionTicket
消息都是不同的,所以每个 ticket
会派生出不同的 PSK
。这样的话,整个PreSharedKey
扩展就都理解清楚了。
我还想说一下NewSessionTicket
中的extensions
,它里面涉及的是early_data
扩展,表示该 ticket 可用于发送 0-RTT 数据,正是因为有这个扩展,才会使得TLS1.3具有0-RTT
这样的握手时间,极大提高了握手效率。在这里,扩展对应的名称是max_early_data_size
这个字段表示使用 ticket 时允许 Client 发送的最大 0-RTT 数据量(以字节为单位)。Server 如果接收的数据大小超过了 max_early_data_size
字节的 0-RTT 数据,应该立即使用 "unexpected_message" alert
消息终止连接。
前面的时候也说过这个0-RTT,先来看一下RTT的定义:
之前理解的不是特别到位,现在再来梳理一下:
如果要想达到0-RTT的时间需要一些条件:
client
和server
之前有过一次握手,并且结束之后server
发送的NewSessionTicket
中携带max_early_data_size
扩展,表明server
愿意接受EarlyData
。client
发送了PreSharedKey
扩展,这里面有一个规定,其中的identities
字段中的第一个标识被用来标识 0-RTT 的,并且server
正确相应成功恢复会话。client
也需要发送EarlyData
扩展,并且在clienthello
后面紧跟Application Data
。server
端需要在它的EncryptedExtensions
中返回自己的 EarlyData
扩展,表明它准备处理 early data
。其中的EncryptedExtensions
是由server
发送出的,在所有的握手中,Server 必须在 ServerHello 消息之后立即发送 EncryptedExtensions 消息。这是在从 server_handshake_traffic_secret 派生的密钥下加密的第一条消息。其中包含着应该被保护的扩展:
struct {
Extension extensions<0..2^16-1>;
} EncryptedExtensions;
有关前面的密钥计算,只有选择使用PSK
握手模式时,也就是说之前握手过,这是第二次握手,这时密钥的计算是从头开始的,也就是:
0
|
v
PSK -> HKDF-Extract = Early Secret
这一部分开始,如果没有使用PSK
回复回话,那么这一步也是不能省略的,只不过就是hash.length
长度的0用于计算HKDF-Extract(0,0)
。下面是我画的0-RTT密钥计算的流程图:
当server
发现客户端没有提供合适的key_share
组时,会发送HelloRetryRequest
消息,此消息的结构和serverhello
是基本一致的,里面也带有key_share
扩展通过它来告知client
需要作出那些更改,client
会根据它作出相应的更改,如:更换key_share
组,然后会再次发送修改后的clienthello
消息。
Client Server
ClientHello
+ key_share -------->
<-------- HelloRetryRequest
+ key_share
ClientHello
+ key_share -------->
ServerHello
+ key_share
{EncryptedExtensions}
{CertificateRequest*}
{Certificate*}
{CertificateVerify*}
{Finished}
<-------- [Application Data*]
{Certificate*}
{CertificateVerify*}
{Finished} -------->
[Application Data] <-------> [Application Data]
在这种情况下,对握手信息的计算就需要作出一些改变,主旨就是HelloRetryRequest
消息是包含在握手消息内的,也就是说 握手记录 包括最初的ClientHello / HelloRetryRequest
交换; 它不会重新设置新的ClientHello
。
我们来看一下计算公式:
Transcript-Hash(ClientHello1, HelloRetryRequest, ... Mn) =
Hash(message_hash || /* Handshake type */
00 00 Hash.length || /* Handshake message length (bytes) */
Hash(ClientHello1) || /* Hash of ClientHello1 */
HelloRetryRequest || ... || Mn)