本篇文章我們來對比對比 TLS 1.2 和 TLS 1.3 中的密鑰計算。 一. TLS 1.2 中的密鑰在 TLS 1.2 中,有 3 種密鑰:預備主密鑰、主密鑰和會話密鑰(密鑰塊),這幾個密鑰都是有聯(lián)系的。 struct { 對于 RSA 握手協(xié)商算法來說,Client 會生成的一個 48 字節(jié)的預備主密鑰,其中前 2 個字節(jié)是 ProtocolVersion,后 46 字節(jié)是隨機數,用 Server 的私鑰加密之后通過 Client Key Exchange 子消息發(fā)給 Server,Server 用私鑰來解密。對于 (EC)DHE 來說,預備主密鑰是雙方通過橢圓曲線算法生成的,雙方各自生成臨時公私鑰對,保留私鑰,將公鑰發(fā)給對方,然后就可以用自己的私鑰以及對方的公鑰通過橢圓曲線算法來生成預備主密鑰,預備主密鑰長度取決于 DH/ECDH 算法公鑰。預備主密鑰長度是 48 字節(jié)或者 X 字節(jié)。 主密鑰是由預備主密鑰、ClientHello random 和 ServerHello random 通過 PRF 函數生成的。主密鑰長度是 48 字節(jié)??梢钥闯觯灰覀冎李A備主密鑰或者主密鑰便可以解密抓包數據,所以 TLS 1.2 中抓包解密調試只需要一個主密鑰即可,SSLKEYLOG 就是將主密鑰導出來,在 Wireshark 里面導入就可以解密相應的抓包數據。 會話密鑰(密鑰塊)是由主密鑰、SecurityParameters.server_random 和 SecurityParameters.client_random 數通過 PRF 函數來生成,會話密鑰里面包含對稱加密密鑰、消息認證和 CBC 模式的初始化向量,對于非 CBC 模式的加密算法來說,就沒有用到這個初始化向量。 Session ID 緩存和 Session Ticket 里面保存的也是主密鑰,而不是會話密鑰,這樣每次會話復用的時候再用雙方的隨機數和主密鑰導出會話密鑰,從而實現每次加密通信的會話密鑰不一樣,即使一個會話的主密鑰泄露了或者被破解了也不會影響到另一個會話。 二. TLS 1.2 中的 HMAC 和偽隨機函數TLS 記錄層使用一個有密鑰的信息驗證碼(MAC)來保護信息的完整性。密碼算法族使用了一個被稱為HMAC(在[HMAC]中描述)的 MAC 算法,它基于一個 hash 函數。如果必要的話其它密碼算法族可以定義它們自己的 MAC 算法。 此外,為了進行密鑰生成或驗證,需要一個 MAC 算法對數據塊進行擴展以增加機密性。這個偽隨機函數(PRF)將機密信息(secret),種子和身份標簽作為輸入,并產生任意長度的輸出。 在 TLS 1.2 中,基于 HMAC 定義了一個 PRF 函數。這個使用 SHA-256 hash 函數的 PRF 函數被用于所有的密碼算法套件。新的密碼算法套件必須顯式指定一個 PRF,通常應該使用 SHA-256 或更強的標準 hash 算法與 TLS PRF 一同使用。 首先,我們定義一個數據擴展函數,P_hash(secret, data),它使用一個 hash 函數擴展成一個 secret 和種子,形成任意大小的輸出: P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) + 這里"+"是指級聯(lián)。 A()被定義為: A(0) = seed 必要時 P_hash 可以被多次迭代,以產生所需數量的數據。例如,如果 P_SHA256 被用于產生 80 字節(jié)的數據,它應該被迭代 3 次(通過 A(3)),SHA_256 每次輸出 32 字節(jié)(256 bit),迭代 3 次才能產生 96 字節(jié)的輸出數據,最終迭代產生的最后 16 字節(jié)會被丟棄,留下 80 字節(jié)作為輸出數據。 TLS 的 PRF 可以通過將 P_hash 運用與 secret 來實現: PRF(secret, label, seed) = P_ label 是一個 ASCII 字符串。它應該以嚴格地按照它被給出的內容進行處理,不包含一個長度字節(jié)或結尾添加的空字符。例如,label "slithy toves" 應該通過 hash 下列字節(jié)的方式被處理: 73 6C 69 74 68 79 20 74 6F 76 65 73 上述數據是字符串 "slithy toves" 的十六進制格式。 PRF 使用的 Hash 算法取決于密碼套件和 TLS 版本,對應關系如下: PRF 算法Hash 算法prf_tls10TLS 1.0 和 TLS 1.1 協(xié)議,PRF 算法是結合 MD5 和 SHA_1 算法prf_tls12_sha256TLS 1.2 協(xié)議,默認是 SHA_256 算法(這是能滿足最低安全的算法)prf_tls12_sha384TLS 1.2 協(xié)議,如果加密套件指定的 HMAC 算法安全級別高于 SHA_256,則采用加密基元 SHA_384 算法 在 TLS 1.0 和 TLS 1.1 中,調用了兩次 P_HASH,一次是 MD5 一次是 SHA1,兩次的結果進行異或得到最后的結果。 r1 = P_MD5(...); 在 TLS 1.2 中,PRF 算法其實就是直接調用了 P_HASH 算法,默認是 SHA_256 算法。 三. TLS 1.2 中的密鑰計算TLS 1.2 中的密鑰算法主要是上一章談到的 PRF。PRF 主要用于導出主密鑰和會話密鑰(密鑰塊)的。 1. 計算主密鑰 為了開始連接保護,TLS 記錄協(xié)議要求指定一個算法套件,一個主密鑰和 Client 及 Server 端隨機數。認證,加密和消息認證碼算法由 cipher_suite 確定,cipher_suite 是由 Server 選定并在 ServerHello 消息中表明出來的。壓縮算法在 hello 消息里協(xié)商出來,隨機數也在 hello 消息中交換。所有這些都用于計算主密鑰。 對于所有的密鑰交換算法,相同的算法都會被用來將 pre_master_secret 轉化為 master_secret。一旦 master_secret 計算完畢,pre_master_secret就應當從內存中刪除。避免攻擊者獲取預備主密鑰,如果攻擊者獲取到了預備主密鑰,加上 ClientHello.random 和 ServerHello.random 傳輸過程中是不加密的,也容易獲取,那么攻擊者就可以合成主密鑰并進一步導出會話密鑰,這樣整個加密過程就被完全破解了。 master_secret = PRF(pre_master_secret, "master secret", 主密鑰的長度一直是 48 字節(jié)。預密鑰的長度根據密鑰交換算法而變。 RSA當RSA被用于身份認證和密鑰交換時,Client 會產生一個 48 字節(jié)的 pre_master_secret,用 Server 的公鑰加密,然后發(fā)送給 Server。Server 用它自己的私鑰解密 pre_master_secret。然后雙方按照前述方法將 pre_master_secret轉換為 master_secret。 struct { Diffie-Hellman一個傳統(tǒng)的 Diffie-Hellman 計算需要被執(zhí)行。協(xié)商出來的密鑰(Z)會被用做pre_master_secret,并按照前述方法將其轉換為 master_secret。在被用做pre_master_secret之前,Z 開頭所有的 0 位都會被壓縮。 注:Diffie-Hellman 參數由 Server 指定,可能是臨時的也可能包含在 Server 的證書中。 2. 計算增強型主密鑰在之前的文章中,我們看到了 ClientHello 的擴展中攜帶了 extended_master_secret 擴展,這個擴展標識 Client 和 Server 使用增強型主密鑰計算方式。 Server 在 ServerHello 中響應該擴展,返回了一個空的 extended_master_secret 擴展,表明會使用增強型主密鑰計算方式。 那么增強型主密鑰是如何計算的呢?計算方式如下: master_secret = PRF(pre_master_secret, "extended master secret", 上面的計算方式和普通計算主密鑰方式不同點在于:
除了來自 Client 和 Server 的密碼套件,密鑰交換信息和證書(如果有的話)之外,"session_hash" 還取決于包括 "ClientHello.random" 和 "ServerHello.random" 的握手日志。因此,擴展主密鑰取決于所有這些會話參數的選擇。 此設計反映了密鑰應該綁定到計算它們的安全上下文的建議 SP800-108。將密鑰交換消息的散列混合到主密鑰導出中的技術已經用于其他眾所周知的協(xié)議,例如 Secure Shell(SSH)RFC4251。Client 和 Server 不應接受不使用擴展主密鑰的握手,特別是如果它們依賴于復合認證等功能。
3. 計算會話密鑰會話密鑰(密鑰塊)用于 TLS 記錄層加密。記錄協(xié)議需要一個算法從握手協(xié)議提供的安全參數中生成當前連接狀態(tài)所需的密鑰。 enum { null(0), (255) } CompressionMethod; 主密鑰被擴張為一個安全字節(jié)序列,它被分割為一個 client_write_MAC_key,一個 server_write_MAC_key,一個 client_write_key,一個 server_write_key。它們中的每一個都是從字節(jié)序列中以上述順序生成。未使用的值是空。一些AEAD加密可能會額外需要一個 client_write_IV 和一個 server_write_IV。生成密鑰和 MAC 密鑰時,主密鑰被用作一個熵源。所以會話密鑰(密鑰塊)的長度和個數取決于協(xié)商出來的密碼套件,更準確的說是取決于加密參數 SecurityParameters,需要使用 PRF 函數擴展出足夠長的密鑰塊,計算如下: key_block = PRF(SecurityParameters.master_secret, 注意:計算會話密鑰和主密鑰使用 PRF 的三個入參都不同,PRF(secret, label, seed):主密鑰是 (pre_master_secret, "master secret", ClientHello.random + ServerHello.random),會話密鑰是 (SecurityParameters.master_secret, "key expansion", SecurityParameters.server_random + SecurityParameters.client_random),seed 順序有變化,Client 和 Server 隨機數的組合順序會調換。 直到產生足夠的輸出。然后,key_block會按照如下方式分開: client_write_MAC_key[SecurityParameters.mac_key_length] client_write_key、server_write_key、client_write_MAC_key 和 server_write_MAC_key 是加密和消息驗證碼需要的密鑰。Client 和 Server 分別擁有自己的一套密鑰,使用的密鑰是不同的。如果是分組加密方式,還需要初始化向量 client_write_IV 和 server_write_IV。如果是 AEAD 模式,client_write_MAC_key 和 server_write_MAC_key 可以不需要,使用 client_write_IV 和 server_write_IV 作為 nonce(隨機值) 。 目前,client_write_IV 和 server_write_IV 只能由 AEAD 的隱式 nonce 技術生成。 當前定義的密碼協(xié)議套件使用最多的是 AES_256_CBC_SHA256。它需要 2 x 32 字節(jié)密鑰和 2 x 32 字節(jié) MAC 密鑰,它們從 128 字節(jié)的密鑰數據中產生。 總結 TLS 1.2 密鑰計算流程如下: 四. TLS 1.2 Finished 校驗在 TLS 1.2 握手的最后,會發(fā)送 Finished 子消息,這條消息是加密的第一條消息,Finished 消息的接收者必須要驗證這條消息的內容是否正確。驗證的內容是通過 PRF 算法計算出來的。 verify_data = PRF(master_secret, 在計算 verify_data 的時候,PRF(secret, label, seed) 中 secret 是主密鑰,label 是 finished_label,Client 是 "client finished",Server 是 "server finished",seed 是所有握手消息的 hash 值。對于 Client 來說,handshake_messages 內容包含所有發(fā)送的消息和接收的消息,但是不包括自己發(fā)送的 Finished 消息。對于 Server 來說,handshake_messages 內容包含從 ClientHello 消息開始截止到 Finished 消息之前的所有消息,也包括 Client 的 Finished 子消息。
早期 TLS 協(xié)議,verify_data 的長度是 12 字節(jié),對于 TLS 1.2 協(xié)議來說,verify_data 的長度取決于密鑰套件,如果密碼套件沒有指定 verify_data_length,則默認長度也是 12 字節(jié)。 五. TLS 1.2 的無密鑰交換如果 CDN 廠商想支持 HTTPS,那么需要做哪些改動呢?國內的廠商的做法是:將自己 HTTPS 網站的私鑰上傳到 CDN 廠商提供的服務器上。某些對安全性要求非常高的客戶(比如銀行)想要使用第三方的 CDN,想加快自家網站的訪問速度,但是出于安全考慮,不能把私鑰交給 CDN 服務商。讀者如果已經看懂了上面 TLS 的密鑰計算的方法,完全沒有必要把私鑰上傳到第三方 CDN 服務器上。CloudFlare 很早就提供了 Keyless 服務,即你把網站放到它們的 CDN 上,不用提供自己證書的私鑰,也能使用 TLS/SSL 加密鏈接。 在握手階段,主要是協(xié)商出了 3 個隨機數。這 3 個隨機數產生了 TLS 記錄層需要的會話密鑰(密鑰塊)。握手完成以后,之后的加密都是對稱加密。唯一需要用到非對稱加密中的私鑰。如果是 RSA 密鑰協(xié)商,私鑰的作用是解密 Client 傳過來的預備主密鑰。非對稱加密中的公鑰用來加密發(fā)給 Client 的密鑰協(xié)商參數。但是 Server 的公鑰可以從證書中獲取。所以 CDN 唯一不能解決的問題是解密 Client 發(fā)過來的預備主密鑰。如果是 ECDHE 密鑰協(xié)商,私鑰的作用是對 DH 參數做簽名的。 解決辦法比較簡單: 如果是 RSA 密鑰協(xié)商,在 CDN 廠商的服務器收到 Client 發(fā)來的預備主密鑰的時候,把這個加密過的預備主密鑰發(fā)給用戶自己的 key server,讓用戶用自己的私鑰解密預備主密鑰,再發(fā)還給 CDN 廠商的服務器,這樣 CDN 廠商就有解密之后的預備主密鑰了,進而可以繼續(xù)計算主密鑰和會話密鑰(密鑰塊)了。流程如下: 如果是 DH 密鑰協(xié)商算法,預備主密鑰可以由 Server 和 Client 共同計算出來,但是 DH 相關的參數需要雙方協(xié)商出來。Server 將 DH 相關參數發(fā)給 Client 的時候,需要用到證書的私鑰。CDN 廠商會把 Client 隨機數,Server 隨機數和 DH 參數三者的 hash 發(fā)給用戶的 key server,key server 就它們簽名以后,發(fā)還給 CDN 廠商服務器。CDN 廠商將簽名后的消息發(fā)給 Client。這樣也就完成了密鑰協(xié)商。CDN 和 Client 相互算出預備主密鑰和主密鑰還有會話密鑰。流程如下: 六. TLS 1.3 中的密鑰在 TLS 1.3 中,不再使用 PRF 這種算法了,而是采用更標準的 HKDF 算法來進行密鑰的推導。而且在 TLS 1.3 中對密鑰進行了更細粒度的優(yōu)化,每個階段或者方向的加密都不是使用同一個密鑰。TLS 1.3 在 ServerHello 消息之后的數據都是加密的,握手期間 Server 給 Client 發(fā)送的消息用 server_handshake_traffic_secret 通過 HKDF 算法導出的密鑰加密的,Client 發(fā)送給 Server 的握手消息是用 client_handshake_traffic_secret 通過 HKDF 算法導出的密鑰加密的。這兩個密鑰是通過 Handshake Secret 密鑰來導出的,而 Handshake Secret 密鑰又是由 PreMasterSecret 和 Early Secret 密鑰導出,然后通過 Handshake Secret 密鑰導出主密鑰 Master Secret。 再由主密鑰 Master Secret 導出這幾個密鑰: client_application_traffic_secret:用來導出客戶端發(fā)送給服務器應用數據的對稱加密密鑰。 server_application_traffic_secret:用來導出服務器發(fā)送給客戶端應用數據的對稱加密密鑰。 resumption_master_secret:用來生成 PSK。 最終 server_handshake_traffic_secret、client_handshake_traffic_secret、client_application_traffic_secret、server_application_traffic_secret 這 4 個密鑰會分別生成 4 套 write_key 和 write_IV 用于對稱加密。 如果用到 early_data,還需要 client_early_traffic_secret,它也會生成 1 套 write_key 和 write_IV 用于加密和解密 0-RTT 數據。 七. TLS 1.3 中的 HMAC 和偽隨機函數Key Derivation Function (KDF) 是密碼學系統(tǒng)中必要的組件。它的目的是把一個 key 拓展成多個從密碼學角度來上說是安全的 key。TLS 1.3 使用的是 HMAC-based Extract-and-Expand Key Derivation Function (HKDF),HKDF 根據 extract-then-expand 設計模式,即 KDF 有 2 大模塊。第一個階段是將輸入的 key material 進行 "extracts",得到固定長度的 key,然后第二階段將這個 key "expands" 成多個附加的偽隨機的 key,輸出的 key 的長度和個數,取決于指定的加密算法。由于 extract 流程不是必須的,所以 expand 流程可以獨立的使用。 HMAC 的兩個參數,第一個是 key,第二個是 data。data 可以由好幾個元素組成,我們一般用 | 來表示,例如: HMAC(K, elem1 | elem2 | elem3) 1. Extract HKDF-Extract(salt, IKM) -> PRK
PRK 的計算方法如下: PRK = HMAC-Hash(salt, IKM) HKDF 的定義允許使用有隨機值 salt 和不帶隨機值 salt 的操作。這是為了兼容沒有 salt 的應用程序。但是強烈建議使用 salt 能夠顯著加強 HKDF 算法的強度。并且確保了哈希函數的不同用途之間的獨立性,支持 "源獨立" extraction,并加強了支持 HKDF 設計的分析結果。 隨機 salt 在兩個方面與初始密鑰材料 IKM 的根本不同是:它隨機 salt 是非加密的,可以重復使用。因此,隨機 salt 值可用于許多應用。例如,通過將 HKDF 應用于可再生的熵池(例如,采樣系統(tǒng)事件)而連續(xù)產生輸出的偽隨機數發(fā)生器(PRNG)可以確定鹽值并將其用于 HKDF 的多個應用而無需保護其 salt 的秘密性。在不同的應用程序域中,從 Diffie-Hellman 交換中導出加密密鑰的密鑰協(xié)商協(xié)議可以從通信方之間交換和驗證的公共 nonce 中獲取 salt 值,并把這種做法作為密鑰協(xié)議的一部分(這是 IKEv2 中采用的方法) 理想情況下,salt 值是長度為 HashLen 的隨機(或偽隨機)字符串。然而,即使質量較低的 salt 值(較短的尺寸或有限的熵)仍然可能對輸出密鑰材料的安全性做出重大貢獻;因此,如果應用程序可以獲得這些值,鼓勵應用程序設計者向 HKDF 提供 salt 值。 值得注意的是,雖然不是典型的情況,但某些應用甚至可能具有可供使用的加密 salt 值。在這種情況下,HKDF 提供更強大的安全保障。這種應用的一個例子是 IKEv1 在其“公鑰加密模式”中,其中提取器的 salt 是從加密的 nonce 計算的。類似地,IKEv1 的預共享模式使用從預共享密鑰導出的加密的 salt。 2. Expand HKDF-Expand(PRK, info, L) -> OKM
OKM 的計算方法如下: N = ceil(L/HashLen) 雖然 info 值在 HKDF 的定義中是可選的,但它在應用程序中通常非常重要。其主要目標是將派生的密鑰材料綁定到特定于應用程序和上下文的信息。例如,info 可以包含協(xié)議號,算法標識符,用戶身份等。特別地,它可以防止針對不同的上下文導出相同的密鑰材料(當在不同背景下使用相同的輸入密鑰材料(IKM)時)。如果需要,它還可以容納對密鑰擴展部分的附加輸入(例如,應用程序可能想要將密鑰材料綁定到其長度 L,從而使得 info 字段擴充至 L 長度)。info 有一個技術要求:它應該獨立于輸入密鑰材料 IKM 的值。 對比 TLS 1.2 中的 PRF 計算方法: PRF(secret, label, seed) = P_ 可以看到這兩個算法的區(qū)別。 在一些應用中,輸入密鑰材料 IKM 可能已經作為密碼強密鑰的存在(例如,TLS RSA 密碼套件中的預主密鑰將是偽隨機字符串,除了前兩個字節(jié))。在這種情況下,可以跳過 extract 提取部分并在 expand 擴展步驟中直接使用 IKM 作為 HMAC 的入參。另一方面,為了與一般情況兼容,應用程序仍然可以使用 extract 提取部分。特別是,如果 IKM 是隨機(或偽隨機)但長于 HMAC 密鑰,則 extract 提取步驟可用于輸出合適的 HMAC 密鑰(在 HMAC 的情況下,通過 extractor 提取器的進行縮短不是嚴格必要的,因為 HMAC 也需要長度達到一定程度才能工作)。但是請注意,如果 IKM 是 Diffie-Hellman值,就像使用 Diffie-Hellman 的 TLS 一樣,則不應跳過 extract 提取部分。這樣做會導致使用 Diffie-Hellman 值 g ^ {xy} 本身(不是均勻隨機或偽隨機字符串)作為 HMAC 的關鍵PRK。相反,HKDF 應該先將 g ^ {xy} 進行 extract 提取步驟(優(yōu)選具有 salt 值的),并把所得的 PRK 作為 HMAC expansion 部分的關鍵部分。 在所需的密鑰位數 L 不大于 HashLen 的情況下,可以直接使用 PRK 作為 OKM。但是,這不是推薦的,特別是因為它會省略使用 info 作為推導過程的一部分(并且不建議在 extract 提取步驟中添加 info 作為輸入 - 參見 HKDF-paper) 在 TLS 1.3 的密鑰派生過程使用 HMAC-based Extract-and-Expand Key Derivation Function (HKDF) [RFC5869] 定義的 HKDF-Extract 和 HKDF-Expand 函數,以及下面定義的函數: HKDF-Expand-Label(Secret, Label, Context, Length) = Transcript-Hash 和 HKDF 使用的 Hash 函數是密碼套件哈希算法。Hash.length 是其輸出長度(以字節(jié)為單位)。消息是表示的握手消息的串聯(lián),包括握手消息類型和長度字段,但不包括記錄層頭。請注意,在某些情況下,零長度 context(由 "" 表示)傳遞給 HKDF-Expand-Label。labels 都是 ASCII 字符串,不包括尾隨 NUL 字節(jié)。 由上面的函數調用關系,可以得到下面的結論: Derive-Secret(Secret, Label, Messages) = HKDF-Extract(salt, IKM) 就是 TLS 1.3 中 HKDF 的 Extract 過程;Derive-Secret(Secret, Label, Messages) 就是 TLS 1.3 中 HKDF 的 Expand 過程。 3. Transcript-Hash最后再來談談 Transcript-Hash 函數。TLS 中的許多加密計算都使用了哈希副本。這個值是通過級聯(lián)每個包含的握手消息的方式進來哈希計算的,它包含握手消息頭部攜帶的握手消息類型和長度字段,但是不包括記錄層的頭部。例如: Transcript-Hash(M1, M2, ... Mn) = Hash(M1 || M2 || ... || Mn) 作為此一般規(guī)則的例外,當 Server 用一條 HelloRetryRequest 消息來響應一條 ClientHello 消息時,ClientHello1 的值替換為包含 Hash(ClientHello1)的握手類型為 "message_hash" 的特殊合成握手消息。例如: Transcript-Hash(ClientHello1, HelloRetryRequest, ... Mn) = 設計這種結構的原因是允許 Server 通過在 cookie 中僅存儲 ClientHello1 的哈希值來執(zhí)行無狀態(tài) HelloRetryRequest,而不是要求它導出整個中間哈希狀態(tài)。 具體而言,哈希副本始終取自于下列握手消息序列,從第一個 ClientHello 開始,僅包括已發(fā)送的消息:ClientHello, HelloRetryRequest, ClientHello, ServerHello, EncryptedExtensions, server CertificateRequest, server Certificate, server CertificateVerify, server Finished, EndOfEarlyData, client Certificate, client CertificateVerify, client Finished。 通常上,實現方可以下面的方法來實現哈希副本:根據協(xié)商的哈希來維持一個動態(tài)的哈希副本。請注意,隨后的握手后認證不會相互包含,只是通過主握手結束的消息。 八. TLS 1.3 中的密鑰計算經過密鑰協(xié)商得出來的密鑰材料的隨機性可能不夠,協(xié)商的過程能被攻擊者獲知,需要使用一種密鑰導出函數來從初始密鑰材料(PSK 或者 DH 密鑰協(xié)商計算出來的 key)中獲得安全性更強的密鑰。HKDF 正是 TLS 1.3 中所使用的這樣一個算法,使用協(xié)商出來的密鑰材料和握手階段報文的哈希值作為輸入,可以輸出安全性更強的新密鑰。 從上一章中,我們知道,HKDF 包括 extract_then_expand 的兩階段過程。extract 過程增加密鑰材料的隨機性,在 TLS 1.2 中使用的密鑰導出函數 PRF 實際上只實現了 HKDF 的 expand 部分,并沒有經過 extract,而直接假設密鑰材料的隨機性已經符合要求。 這一章中,讓我們來看看 TLS 1.3 是如何對密鑰材料進行 extract_then_expand 的。這一章也展示了 TLS 1.3 比 TLS 1.2 在安全性上更上一層樓的原因。 TLS 1.3 中的所有密鑰都是由 HKDF-Extract(salt, IKM) 和 Derive-Secret(Secret, Label, Messages) 聯(lián)合導出的。其中 Salt 是當前的 secret 狀態(tài),輸入密鑰材料(IKM)是要添加的新 secret 。在 TLS 1.3 中,兩個輸入的 IKM 是:
TLS 1.3 完整的密鑰導出流程圖如下: 0 幾點說明:
如果給定的 secret 不可用,則使用由設置為零的 Hash.length 字節(jié)串組成的 0 值。請注意,這并不意味著要跳過輪次,因此如果 PSK 未被使用,Early Secret 仍將是 HKDF-Extract(0,0)。對于 binder_key 的計算,label 是外部 PSK(在 TLS 之外提供的那些)的 "ext binder" 和用于恢復 PSK 的 "res binder"(提供為先前握手的恢復主密鑰的那些)。不同的 labels 阻止了一種 PSK 替代另一種 PSK。 這存在有多個潛在的 Early Secret 值,具體取決于 Server 最終選擇的 PSK。Client 需要為每個潛在的 PSK 都計算一個值;如果沒有選擇 PSK,則需要計算對應于零 PSK 的 Early Secret。 一旦計算出了從給定 secret 派生出的所有值,就應該刪除該 secret。 TLS 1.3 中涉及到了 3 個 Secret 計算方法如下: Early Secret = HKDF-Extract(salt, IKM) = HKDF-Extract(0, PSK) TLS 1.3 中涉及到了 8 個密鑰計算方法如下: client_early_traffic_secret = Derive-Secret(Early Secret, "c e traffic", ClientHello) 例如: CLIENT_EARLY_TRAFFIC_SECRET edb6c73462794c0fe79296853fd17b06cd30e63e87e69c8864eba6996e5d9434 5a0d40c3afa57cbb5aa427456f8dc21b9c4c17bfb731600f93e35358f5b581cb EXPORTER_SECRET 是導出密鑰,用于用戶自定義的其他用途。 上面得到的 8 個密鑰除去 2 個用戶自定義需要的導出密鑰,和會話恢復的 resumption_master_secret,剩下的 5 個密鑰雖然是經過一次 HKDF 的 Expand 過程,但是這 5 個密鑰仍然只是“中間變量”,生成最后的加密參數還需要一次 Expand 過程: [sender]_write_key = HKDF-Expand-Label(Secret, "key", "", key_length) [sender] 表示發(fā)送方。每種記錄類型的 Secret 值顯示在下表中: +-------------------+---------------------------------------+ 每當底層 Secret 更改時(例如,從握手更改為應用數據密鑰或密鑰更新時),將重新計算所有流量密鑰材料。 resumption_master_secret 密鑰是為了會話恢復導出 PSK 的,計算方法如下: PskIdentity.identity = ticket Server 在 NewSessionTicket 中把 ticket 發(fā)送到 Client,Client 利用 ticket 生成 PskIdentity。再計算 PskBinderEntry: PskBinderEntry = HMAC(binder_key, Transcript-Hash(Truncate(ClientHello1))) Client 將 PskIdentity 和 PskBinderEntry 結合成 PSK,在需要會話恢復的時候把 PSK 作為 ClientHello 的擴展發(fā)給 Server。PSK 作為 Early Secret 的輸入密鑰材料 IKM。 Early Secret = HKDF-Extract(salt, IKM) = HKDF-Extract(0, PSK) 由 client_early_traffic_secret 生成的 write_key 和 write_iv 最終用于 0-RTT 的加密和解密。 TLS 1.3 0-RTT 密鑰計算流程如下: 九. TLS 1.3 Finished 校驗TLS 1.3 中的 Finished 并不算是整個握手中的第一條加密消息,作用和 TLS 1.2 是相同的,它對提供握手和計算密鑰的身份驗證起了至關重要的作用。 在 TLS 1.3 中 Authentication 消息的計算統(tǒng)一采用以下的輸入方式:
Finished 子消息根據 Transcript-Hash(Handshake Context, Certificate, CertificateVerify) 的值得出的 MAC 。使用從 Base key 派生出來的 MAC key 計算的 MAC 值。 對于每個場景,下表定義了握手上下文和 MAC Base Key +-----------+-------------------------+-----------------------------+ 用于計算 Finished 消息的密鑰是使用 HKDF,Base Key 是 server_handshake_traffic_ secret 和 client_handshake_traffic_secret。特別的: finished_key = 這條消息的數據結構是: struct { verify_data 按照如下方法計算: verify_data = HMAC [RFC2104] 使用哈希算法進行握手。如上所述,HMAC 輸入通常是通過動態(tài)的哈希實現的,即,此時僅是握手的哈希。 在以前版本的 TLS 中,verify_data 的長度總是 12 個八位字節(jié)。在 TLS 1.3 中,它是用來表示握手的哈希的 HMAC 輸出的大小。 注意:警報和任何其他非握手記錄類型不是握手消息,并且不包含在哈希計算中。 Finished 消息之后的任何記錄 Post-Handshake 都必須在適當的 client_application_traffic_secret_N 下加密。特別是,這包括 Server 為了響應 Client 的 Certificate 消息和 CertificateVerify 消息而發(fā)送的任何 alert。 十. TLS 1.3 KeyUpdate看到這里讀者可能會問,為什么在文章最后還會再討論 TLS 1.3 的 KeyUpdate 消息?因為這條消息會觸發(fā) TLS 1.3 重新計算密鑰。所以需要細究一下這條消息。 研究表明 如果使用同一個密鑰加密大量的數據,攻擊者有幾率可以通過記錄所有密文并找出特征,逆推出對稱加密密鑰。因此需要引進一個密鑰同步更新的機制,該機制同時也使用 HKDF 算法,在舊密鑰的基礎上衍生出新一輪的密鑰。 當加密的報文達到一定長度后,雙方也需要發(fā)送 KeyUpdate 報文重新計算加密密鑰。 KeyUpdate 握手消息用于表示發(fā)送方正在更新其自己的發(fā)送加密密鑰。任何對等方在發(fā)送 Finished 消息后都可以發(fā)送此消息。在接收 Finished 消息之前接收 KeyUpdate 消息的,實現方必須使用 "unexpected_message" alert 消息終止連接。發(fā)送 KeyUpdate 消息后,發(fā)送方應使用新一代的密鑰發(fā)送其所有流量。收到 KeyUpdate 后,接收方必須更新其接收密鑰。 enum {
如果 request_update 字段設置為 "update_requested",則接收方必須在發(fā)送其下一個應用數據記錄之前發(fā)送自己的 KeyUpdate,其中 request_update 設置為 "update_not_requested"。此機制允許任何一方強制更新整個連接,但會導致一個實現方接收多個 KeyUpdates,并且它還是靜默的響應單個更新。請注意,實現方可能在發(fā)送 KeyUpdate (把 request_update 設置為 "update_requested") 與接收對等方的 KeyUpdate 之間接收任意數量的消息,因為這些消息可能早就已經在傳輸中了。但是,由于發(fā)送和接收密鑰是從獨立的流量密鑰中導出的,因此保留接收流量密鑰并不會影響到發(fā)送方更改密鑰之前發(fā)送的數據的前向保密性。 如果實現方獨立地發(fā)送它們自己的 KeyUpdates,其 request_update 設置為 "update_requested" 并且它們的消息都是傳輸中,結果是雙方都會響應,雙方都會更新密鑰。 發(fā)送方和接收方都必須使用舊密鑰加密其 KeyUpdate 消息。另外,在接受使用新密鑰加密的任何消息之前,雙方必須強制接收帶有舊密鑰的 KeyUpdate。如果不這樣做,可能會引起消息截斷攻擊。 下一代流量密鑰的計算方法是,從 client_ / server_application_traffic_secret_N 生成出 client_ / server_application_traffic_secret_N + 1,然后按上一節(jié)所述方法重新導出流量密鑰。 下一代 application_traffic_secret 計算方法如下: application_traffic_secret_N+1 = 一旦計算了 client_ / server_application_traffic_secret_N + 1 及其關聯(lián)的流量密鑰,實現方應該刪除 client_ / server_application_traffic_secret_N 及其關聯(lián)的流量密鑰。 十一. TLS 1.3 中的密鑰導出在 TLS 1.3 中,有 2 個導出密鑰 exporter: early_exporter_master_secret = Derive-Secret(Early Secret, "e exp master", ClientHello) RFC5705 根據 TLS 偽隨機函數(PRF)定義 TLS 的密鑰材料 exporter。TLS 1.3 用 HKDF 取代 PRF,因此需要新的結構。exporter 的接口保持不變。 exporter 的值計算方法如下: TLS-Exporter(label, context_value, key_length) = Secret 可以是 early_exporter_master_secret 或 exporter_master_secret。除非應用程序明確指定,否則實現方必須使用 exporter_master_secret。early_exporter_master_secret 被定義用來在 0-RTT 數據需要 exporter 的設置這種情況中使用。建議為 early exporter 提供單獨的接口;這可以避免 exporter 用戶在需要常規(guī) exporter 時意外使用 early exporter,反之亦然。 如果未提供上下文,則 context_value 為零長度。因此,不提供上下文計算與提供空上下文得到的結果都是相同的。這是對以前版本的 TLS 的更改,以前的 TLS 版本中,空的上下文產生的輸出與不提供的上下文的結果不同。截至 TLS 1.3,無論是否使用上下文,都不會使用已分配的 exporter 標簽。未來的規(guī)范絕不能定義允許空上下文和沒有相同標簽的上下文的 exporter 的使用。exporter 的新用法應該是在所有 exporter 計算中提供上下文,盡管值可能為空。 exporter 標簽格式的要求在 [RFC5705] 第4節(jié) 中定義。 Reference:RFC 5246 RFC 8466 Keyless SSL: The Nitty Gritty Technical Details Cryptographic Extraction and Key Derivation: The HKDF Scheme
|