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