New Session Ticket Message
首先我還是想說一下這個擴展:
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;
- identity:其中存儲的就是
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(Round-trip time)
前面的時候也說過這個0-RTT,先來看一下RTT的定義:
- 在雙方通信中,發訊方的信號(Signal)傳播(Propagation)到收訊方的時間(意即:傳播延遲(Propagation delay)),加上收訊方回傳消息到發訊方的時間。
之前理解的不是特別到位,現在再來梳理一下:
如果要想達到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密鑰計算的流程圖:
補充
Transcript-Hash
當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)