QUIC加密协议
摘要
QUIC加密協議是QUIC的一部分,它為連接提供了傳輸安全性。QUIC加密協議是 注定要消亡的。未來它將由TLS 1.3替代,但在TLS 1.3 最終啟用之前QUIC需要一個加密協議。
借助于當前的QUIC加密協議,當客戶端已經緩存了關于服務器的信息時,它可以無需往返就建立一個加密的連接。TLS,相反地,至少需要兩次往返(算上TCP的3次握手)。QUIC握手應該比普通的TLS 握手(2048-bit RSA)高效大約5倍,而且安全等級更高。
源地址欺騙
Internet上的協議很少能在無需至少一個初始化往返的情況下就可以工作的。大多數協議由于TCP而需要一個往返,基于TLS的協議在應用數據可以傳輸前,需要至少額外的一個或更多往返。
這些往返都交換nonces:在TCP的情況下是序列號(或SYN cookies),在TLS的情況下是密碼學隨機值(client_random 和 server_random)。TCP nonce防止IP地址欺騙,而TLS nonce防止重放攻擊。任何想要尋求減少往返的協議都不得不以某種方式解決這兩個問題。
作為一個反例,DNS是一個沒有任何初始化往返的協議,因此它不得不自己處理IP地址欺騙和重放攻擊。DNS簡單地忽略IP地址欺騙,并因此使鏡像DDoS攻擊成為一個真正的問題。對于重放的保護,DNSSEC依賴時鐘同步和短時的簽名。根據設計,這允許有限時間內的重放,由于那與DNS的緩存語義相互協調。但那不是重放保護的嚴格形式,因為重放是被允許的。
在QUIC中,我們分開處理這兩個問題。
IP地址欺騙問題通過給客戶端分配一個,即期的,“源地址token”來處理。從客戶端的視角來看,這是透明的字節串。從服務器的視角來看,它是一個認證的加密塊(比如 AES-GCM),其中包含,至少,客戶端的IP地址,和一個服務器的時間戳。服務器將只為給定的IP給那個IP發送一個源地址token。客戶端收到token被視為對IP地址擁有所有權的證明,與TCP序列號的接收 所采用的方式一樣。
客戶端可以在未來的請求中包含源地址token以證明對它們的源IP地址的所有權。如果客戶端的IP地址變了,則token也過時了,或者客戶端沒有token,則服務器可以拒絕連接,并返回一個新的token給客戶端。但是如果客戶端的IP地址保持不變,則它可以復用源地址token以避免獲取一個新token所需的網絡往返。
Token的生命周期主要與服務器有關,但由于源地址token是不記名token,它們可能被盜取并被復用以繞過基于IP地址的限制。(盡管攻擊者將無法收到響應。)源地址token還可以被收集,并可能在IP地址的所有權發生變化之后使用(比如,在DHCP池中)。簡少token的壽命,以減少在無需額外往返的情況下處理的請求的數量為代價來改善這兩個問題。
源地址token,不像TCP序列號的交換,不要求源表現出連續的接收發送到源IP地址的分組的能力。這允許源地址token被用于持續地請求來自于服務器的業務,即使下行鏈路已經飽和,其丟包率足夠高以至于不能建立TCP連接。這種 “自 DOS” 攻擊可用于 DOS 相同下行鏈路中的其他用戶。
然后,我們注意到 類似的技巧實際上可能用于TCP,因此QUIC并沒有在這方面明顯地使事情變得更糟。確實,一旦連接建立好,QUIC在包中包含了一個熵位,并要求接收者發送它們聲稱已經收到的熵的散列值 - 從而解決TCP的問題。
為了最小化延遲,服務器可以動態地決定放松源地址限制。可以想象服務器跟蹤來自不同IP地址的請求的數量,并且只有當“未請求”連接的數量超過全局或某個IP范圍的限制時,才需要源地址token。這可能是有效的,但不清楚這是否是全球穩定的。如果大量的QUIC服務器實現了這種策略,則大量的鏡像DDoS攻擊可以在它們之間分割,使得任何一臺服務器都不會達到攻擊閾值。
重放攻擊
在TLS中,每一方都生成一個隨機數,通過強制它們在密鑰中包含(假定在所有時間唯一)該值,用于確保另一方是新的。沒有往返,客戶端仍然可以包括一個隨機值以確保服務器是新的,但服務器沒有機會為客戶端這樣做。
在沒有來自服務器的輸入的情況下提供重放保護基本上是非常昂貴的。它需要服務器端的一致狀態。盡管如果服務器是單臺機器的話這是合理的,但現代的網站都遍布全世界。
因此,QUIC在服務器的第一次應答之前不為客戶端的數據提供重放保護。這依賴應用去確保這樣的信息在被攻擊者重放時是安全的。比如,在Chrome中,只有GET請求在握手確認前發送。
握手開銷
在TLS中,服務器基于客戶端廣告的它們支持的參數為每個連接選擇連接參數。在QUIC中,服務器的首選項完全是枚舉的和靜態的。它們與Diffie-Hellman公共值一起捆綁到一個“服務器配置”中。這個服務器配置含有一個過期時間,并由服務器的私鑰簽名。由于服務器配置是靜態的,因而不是每個連接都需要簽名操作,而是單個簽名足以滿足許多連接。
使用Diffie-Hellman可用的連接密鑰。服務器的 Diffie-Hellman 值在服務器配置中發現,客戶端在它的第一個握手消息中提供。由于服務器配置必須保留一段時間,以允許0-RTT握手,這給連接的前向安全性設置了上限。既然服務器記錄了服務器配置的 Diffie-Hellman 密鑰,則如果它們泄漏的話用那個服務器配置加密的數據可能被解密。
這樣QUIC提供了兩個層面的保密:來自于客戶端的初始數據使用服務器的服務器配置中的 Diffie-Hellman 值加密,這可能持續幾天。一接收到連接,服務器就用一個 短暫Diffie-Hellman 值來響應,然后連接被重新計算密鑰。
(相對于前向安全的TLS連接,這可能似乎提供更少的前向安全性。然而,為了避免往返,通常在大規模部署中都會啟用TLS Session Tickets。SessionTicket 密鑰足以解密連接,但為了恢復的有效性,它必須保持合理的時間段 - 通常是幾天。SessionTicket密鑰和服務器配置密鑰類似,且有效的安全性實際上比QUIC要高,因為它的前向安全模式更優越。)
單個連接是通常的前向安全性的范圍,但是用于一個單獨的連接的短暫的密鑰,和在 60 秒內用于所有連接的密鑰的安全性的差異是微不足道的。因此我們可以在小的時間跨度中將服務器的 Diffie-Hellman 密鑰生成分攤在所有的連接上。
(由于服務器配置和 Diffie-Hellman 私有值是服務器為了處理QUIC連接所需的所有東西,證書的私鑰從不需要放在服務器上。相反,短期證書的一種形式可以通過簽署短期服務器配置并僅安裝在服務器上來實現。)
如果我們設 S 是一個密鑰操作(比如RSA解密),P 是一個公鑰操作(比如 RSA加密),F 是一個 Diffie-Hellman,定點的,標量乘法,而且 A 是一個任意點,標量乘法則:
(客戶端驗證證書鏈的操作沒有包含在內。)
如果我們為這些中的每個選擇公共基元(RSA 2048用于公共和私有操作,ECDH P-256用于TLS前向安全,Curve25519用于QUIC的),那么我們獲得在i7-3770S上的括號中的示例時間。如果 QUIC使用 P-256,則服務器時間將是 300μs,客戶端將是385μs,所以相當多的收益來自于更好的基元。
粗略統計TLS會話恢復率大約為 50%,但 QUIC 不包含顯式地會話恢復。然而,它可以在不在協議中支持恢復的情況下獲得許多恢復的益處,通過使客戶端和服務器維護一個 Diffie-Hellman 結果的緩存。只要客戶端沒有旋轉其臨時密鑰,這個可選的緩存就可以消除使用同一服務器多次握手的計算負擔。如果我們假設TLS的恢復率為 50%,并假設QUIC緩存不做任何事,則相對于簡介中提到的TLS,我們獲得大約 5x 的速度提升。
Wire協議
QUIC是一個數據報協議,一旦密鑰建立,則每個數據報的完整載荷(在UDP層之上)都是被認證和加密的。底層的數據包協議為加密層提供了可靠的發送方式,任意大小的消息。這些消息具有一個統一的,鍵值格式。
鍵是 32-bit tags。這試圖在魔術數字注冊表的粗暴和字符串的冗長之間提供一個平衡。就Wire協議而言,這些是不透明的,32位的值,在本文中,tags 通常將是 EXMP。雖然它是一個字符串,但是它只是值 0x504d5845 的助記符。該值,是小尾端的,是ASCII字符串 E X M P。
如果 tag 是ASCII,但它少于四個字符,則就好像剩余的字符是NUL。因此 EXP 對應于 0x505845。
如果 tag 值包含超出ASCII范圍的字節,則它們將以十六進制格式寫入,比如,504d5845。
除非另有說明,否則所有值都是小尾端的。
握手消息的組成為:
標簽值格式允許在只有一小部分數據被驗證后對標簽進行有效的二分搜索。標簽嚴格單調的要求也消除了關于重復標簽的任何歧義。
盡管當前 32 位的長度超出了需要,16位長度存在不足以處理更大的后量子值的風險。
任何消息可以包含一個填充 (PAD) 標記。這些可以用來打敗流量分析。此外,我們可以為客戶端的 hellos 定義一個全局的最小大小以限制放大攻擊。小于最小值的客戶端hello將需要PAD標記來彌補差異。
客戶端握手
客戶端握手的流程如圖1。概念上來說,QUIC中的所有握手都是 0-RTT的,只是它們中的一些會失敗,需要重試。
圖 1. 客戶端握手流程
為了執行0-RTT握手,客戶端需要具有已被驗證為可信的服務器配置。最初,我們假設客戶端不知道任何關于服務器的東西,因此,在可以嘗試握手之前,客戶端將發送“inchoate” 客戶端 hello 消息以從服務器引出服務器配置和真實性證明。在客戶端收到其需要的所有信息前,可能會有幾輪 inchoate 客戶端 hellos,因為服務器可能不愿意向未經驗證的IP地址發送大量的真實性證明。
Client hello 具有消息標記CHLO,并且以其初始形式包含以下標記/值對:
- SNI Server Name Indication (服務器名稱指示)(可選的):服務器的完全限定DNS名稱,規范化為小寫,沒有尾隨周期。國際化的域名需要被編碼為 RFC 5890 中定義的 A-labels。SNI 標簽的值不能是IP地址字面量。
- STK 源地址令牌 (Source-address token)(可選的):服務器先前提供的源地址令牌(如果有)。
- PDMD 證明需求 (Proof demand):描述客戶端可接受的證明類型的標簽列表,按照優先順序。目前只定義了X509。
- CCS 公共證書集 (Common certificate sets)(可選的):一系列 64 位, FNV-1a 散列的客戶端擁有的公共證書集 。(參考關于證書壓縮的小節。)
- VER 版本:單個標簽,反映客戶端在第一個數據傳輸中的每個QUIC數據包中通告的協議版本。如果發生了版本協商,則這個字段被設為客戶端使用的第一個版本。如果包中的版本不等于標簽中的版本,則服務器需要驗證服務器不支持標簽中的版本以防御降級攻擊。
- XLCT 客戶端期望服務器使用的葉證書的64位, FNV-1a 哈希值。證書的完整內容將被加進 HKDF。如果存在緩存的證書,則首個這樣的條目應與此字段的值一致。
(QUIC的其它部分可以定義客戶端和服務器的 hellos 中包含的額外標簽。比如,流的最大個數,擁塞控制參數等等。然而,那些標簽不在本規范中定義。)
作為對客戶端 hello 的響應,服務器將發送一個拒絕消息,或一個服務器 hello。服務器 hello 表示一個成功的握手,并且永遠不會從初始客戶端 hello 產生,因為它不包含足夠的信息來執行握手。拒絕消息包含客戶端可用以在后面執行更好的握手的信息。
拒絕消息具有 REJ 標簽,且包含如下的標 簽/值 對:
- SCFG 服務器配置(Server config)(可選的):包含了服務器的序列化的配置的消息。(在下面描述。)
- STK 源地址令牌 (Source-address token)(可選的):客戶端應該在未來的客戶端 hello 消息中回顯的透明字節串。
- SNO 服務器隨機數 (Server nonce)(可選的):服務器可以設置一個隨機數,客戶端應該在任何未來的(完整的)客戶端 hello 消息中回顯此隨機數。這允許服務器在沒有觸發寄存器的情況下操作,而客戶端在時鐘偏斜的情況下連接。
- STTL 服務器配置有效的持續時間,以秒計,
- ff545243 證書鏈(可選的):服務器的證書鏈。(參考關于證書壓縮的小節。)
- PROF 真實性證明(可選):在 X.509 的情況下,服務器配置的葉子證書的公鑰簽名。當前這個簽名的格式由公鑰的類型固定:
| ECDSA | ECDSA-SHA256 |
簽名通過如下方式計算:
盡管拒絕消息的所有元素是可選的,但是服務器必須允許客戶端進行。比如,如果客戶端不存在源地址令牌,且服務器不希望發送服務器配置給一個未經驗證的 IP 地址,則服務器必須以一個源地址令牌來響應以使客戶端接下來的握手嘗試更加成功。
一些標記以十六進制而不是以ASCII符號指定。這是因為標簽被構造為使得它們將在消息的開始或結束到來。回想一下,標記,作為數字,是以小尾端序寫入線上的。
包含熵的標簽被移動到消息的開始,因為服務器可能不維護狀態,因此可以處理重復的客戶端 hello 兩次。如果拒絕消息分組丟失,并且熵字段跨越分組邊界,則客戶端可能錯誤地組合它們。
大標記(到目前為止是證書鏈)被移動到消息的結尾,使得它們不會延遲可能足夠的其它字段的接收。
服務器配置包含序列化的服務器首選項,并采用具有標簽 SCFG 的握手消息的形式。它包含如下的標 簽/值 對:
- SCID 服務器配置 ID:這個服務器配置的透明的,16字節標識符。
- KEXS 密鑰交換算法:標記列表,以優先順序,指定了服務器支持的密鑰交換算法。定義了以下標記:
| P256 | P-256 |
- AEAD 驗證加密算法:標記列表,以優先順序,指定了服務器支持的 AEAD 基元。定義了以下標記:
| S20P | Salsa20 with Poly1305。(暫時還沒有實施。) |
-
PUBS 公共值的列表,24 位,小尾數長度前綴,與 KEXS 相同的順序。P-256 公共值,如果有的話,被編碼為X9.62格式的未壓縮點。
-
ORBT 軌道(Orbit):一個8字節的不透明值,用于標識觸發寄存器(殘留)。
-
Expiry 到期(Expiry):以 UNIX epoch 秒計數的服務器配置 64 位到期時間。
-
VER 版本:服務器支持的版本標記的列表。底層的 QUIC 包協議有版本協商。服務器支持的版本是簽名的服務器配置的鏡像,以確保沒有降級攻擊的發生。
一旦服務器接收了服務器配置,且已經認證了它并驗證了證書鏈和簽名,它可以通過發送完整的 client hello 來執行一個不是設計為失敗的握手。完整 client hello 包含與初始 client hello 相同的標簽,加上幾個其他的:
-
SCID 服務器配置 ID:客戶端使用的服務器配置 ID。
-
AEAD 驗證加密:被使用的 AEAD 算法的標簽。
-
KEXS 密鑰交換:被使用的密鑰交換算法的標簽。
-
NONC 客戶端隨機數:由 4 字節的時間戳(大尾端,UNIX epoch 秒),8 字節的 服務器軌道,和 20 字節的隨機數組成的 32 字節數。
-
SNO 服務器隨機數(可選的):回顯的服務器隨機數,如果服務器提供了的話。
-
PUBS 公共值:對于給定的密鑰交換算法,客戶端的公共值。
-
CETV 客戶端加密標簽值(可選的):序列化消息,以在 client hello 中指定的 AEAD 算法加密,并且具有以下面 CETV 部分中指定的方式導出的密鑰。此消息將包含進一步的加密的標簽值對,指定客戶端證書,ChannelID 等。
發送了完整的 client hello 之后,客戶端擁有用于連接的非前向安全密鑰,因為它可以計算來自服務器配置的共享值和PUBS中的公共值。(有關密鑰推導的詳細信息,請參見下文。)這些密鑰被稱為初始密鑰(而不是稍后的前向安全密鑰)且客戶端應該用這些密鑰加密未來的包。它還應該配置數據包處理以接受使用這些密鑰以鎖定方式加密的數據包:一旦已經接收到加密分組,則不應接受另外的未加密分組。
在此時,客戶端可以自由地開始向服務器發送應用程序數據。實際上,如果它希望實現0-RTT,則它必須在等待服務器的答復之前開始發送。
數據的重傳發生在握手層下面的層,然而該層必須仍然知道加密的改變。新的分組必須使用初始密鑰來傳輸,但是如果 client hello 需要重傳,則必須以明文的方式重傳。分組發送層必須知道哪個安全級別最初用于發送任何給定分組,并且小心不要使用更高的安全級別,除非對端已經確認擁有這些密鑰(即通過使用該安全級別發送分組)。
服務器將接受或拒絕握手。服務器拒絕 client hello 的情況下,它將發送 REJ 消息,并且使用初始密鑰發送的所有分組必須被認為丟失并且需要在新的初始密鑰下重傳。因此,在 server hello 或 拒絕 待定時,客戶端應該限制未完成的數據量。
在理想的情況下,握手成功,服務器返回一個 server hello 消息。此消息具有標記 SHLO,使用初始密鑰加密,除了為拒絕消息定義的標記/值對之外,還包含以下標記/值對:
- PUBS 客戶端用于密鑰交換算法的臨時公共值。通過手中的臨時公共值,雙方都可以計算前向安全密鑰。(見關于密鑰推導的部分。)服務器可以立即切換為使用前向安全密鑰發送加密數據包。客戶端必須等待收 server hello。(注意:我們正在考慮讓服務器等待,直到它在發送任何自己之前接收到前向安全數據包。如果 server hello 數據包被丟棄,這避免了停頓。)
密鑰推導
密鑰材料由經過散列函數 SHA-256 的基于 HMAC 的密鑰導出函數(HKDF)生成。HKDF(在 RFC 5869 中描述)使用 NIST SP 800-56C 中描述的已批準的兩步密鑰導出過程。
第 1 步:HKDF 提取
密鑰協議的輸出(在Curve25519和P-256的情況下為32字節)是預主密鑰,后者是HKDF-提取 函數的輸入密鑰材料(IKM)。鹽 輸入是客戶端隨機數,后跟服務器隨機數(如果有的話)。HKDF-提取 輸出偽隨機密鑰(PRK),其是主密鑰。 如果使用SHA-256,主密鑰為32字節長。
第 2 步:HKDF 擴展
PRK 輸入是主密鑰。info 輸入(上下文和應用程序特有信息)是以下數據的級聯:
密鑰材料按以下順序分配:
如果任何原語需要少于密鑰材料的整個字節數,則丟棄最后一個字節的剩余部分。
當推導前向安全密鑰時,使用相同的輸入,除了info使用標簽“QUIC前向安全密鑰擴展 (QUIC forward secure key expansion)”。
當推導服務器的初始密鑰時,它們必須是多樣化的,以確保服務器能夠向HKDF提供熵。
第 1 步:HKDF 提取
服務器寫密鑰加上來自找到的輪的服務器寫IV的級聯是用于 HKDF-Extract 函數的輸入密鑰材料(IKM)。鹽輸入是多樣化隨機數。HKDF-Extract 輸出一個偽隨機密鑰(PRK),它是多樣化密鑰。如果使用 SHA-256 的話,多樣化密鑰是 32 字節長的。
第 2 步:HKDF 擴展
PRK 輸入是多樣化密鑰。info 輸入(上下文和應用特有信息)是標簽"QUIC 密鑰多樣化 (QUIC key diversification)"。
密鑰材料按以下順序分配:
客戶端加密的標簽值
client hello 可能包含一個 CETV 標簽,以描述客戶端證書,ChannelIDs 和 client hello 中的其它非公有數據。(那與 TLS 相反,它以明文發送客戶端證書。)
CETV 消息以 client hello 中的 AEAD 序列化和加密。密鑰是以與連接的密鑰相同的方式推導的(參考上面的 密鑰推導),除了 info 使用標簽“QUIC CETV 塊(QUIC CETV block)”。推導中所用的 client hello 消息是無 CETV 標記的 client hello。當隨后推導連接密鑰時,所使用的 client hello 將包含 CETV 標記。
AEAD 隨機數總是 0,它是安全的,這是因為只有一個消息曾以該密鑰加密。
通過對 CETV 密鑰推導中所使用的 HKDF info 輸入簽名,來完成對客戶端證書和 ChannelID 兩者所需的私鑰的擁有。
CETV 消息可以包含如下的標簽:
CIDK ChannelID 密鑰(ChannelID key)(可選的):一個 32字節對,大尾端數字,一起描述一個 (x, y) 對。這是 P-256 曲線上的一個點和一個 ECDSA 公鑰。
CIDS ChannelID 簽名(ChannelID signature)(可選的):一個 32字節對,大尾端數字,一起描述一個 HKDF 輸入的 ECDSA 簽名的 (r, s) 對。
證書壓縮
在 TLS 中,證書鏈是無壓縮傳輸的,并占用了完整握手中的絕大多數字節。在 QUIC 中,我們希望能夠通過壓縮證書來避免一些往返。
證書鏈是一系列的證書,就這一節的目的而言,是透明的字節串。葉子證書總是鏈中的第一個,且從不應該包含根 CA 證書。
當在一個拒絕消息的 CRT\xFF 標簽中序列化一個證書鏈的時候,服務器認為那些信息客戶端已經有了。這些先驗知識可以來自于兩種方式:擁有一般中間證書,或者緩存了之前與相同服務器交互時的證書。
前者表示為 client hello 的 CCS 標簽中的一系列 64-bit FNV-1a 哈希值。如果客戶端和服務器共享了至少一個一般證書集合,則可以簡單地引用它們中存在的證書。
緩存的證書表示 client hello 的 CCRT 標簽中的 64-bit FNV-1a 哈希值。如果任何一張依然在證書鏈中,則它們可由哈希值代替。
任何剩余的證書以一個預共享的由前兩個方法指定的證書和取自于 Alexa top 5000 的證書中的字符串組成的字典經 gzip 壓縮。
具體的表示方式被放置在拒絕消息的CERT標簽中,并具有以下 TLS 表示風格中的 Cert 結構格式:
enum { end_of_list(0), compressed(1), cached(2), common(3) } EntryType;struct {EntryType type;select (type) {case compressed:// nothingcase cached:opaque hash[8];case common:opaque set_hash[8];uint32 index;} } Entry;struct {Entry entries[];uint32 uncompressed_length;opaque gzip_data[]; } Certs;(回憶一下,QUIC 中的數字是小尾端的。)
entries 列表以一個類型為 end_of_list 的 Entry 結束,而不是
TLS 中常見的長度前綴。gzip_data 擴展到值的末尾。
gzip,預共享字典包含類型為 compressed 或 cached 的證書,以相反的順序連接,其后是此處未提供的 ~1500 個字節的公共子字符串。
未來方向
感謝
多謝 Trevor Perrin, Ben Laurie 和 Emilia K?sper 的有價值的反饋。
打賞
原文
總結
- 上一篇: 网络优化实践探索文章
- 下一篇: 读《Android 安全架构深究》