跟堅哥學QUIC系列:加密和傳輸握手


大家知道 TCP 建立連接需要 3 次握手,這需要 1.5-RTT,如果再加上 TLS 的握手時間,總共需要 3-RTT,耗時將近 200-300 ms。隨着互聯網的高速發展,用戶對於性能體驗要求越來越高,TCP 連接握手帶來的長時延顯然是不可接受的。QUIC 因此提出一個新的建立連接機制,把傳輸和加密握手合並成一個,以最小化延遲(1-RTT)建立連接。后續的連接(repeat connections)還可以通過上一次連接時緩存的信息(TLS 1.3 Diffie-Hellman 公鑰、傳輸參數、NEW_TOKEN 幀生成的令牌,等)直接在一個經過認證且加密的通道傳輸數據(0-RTT),這極大減小了建立連接的時延。

 

下面我們將基於IETF QUIC 協議草案詳細了解下 QUIC 是如何做到能快速建立連接(1-RTT/0-RTT)又能保證安全性。

QUIC 加密握手提供以下屬性:

  • 認證密鑰交換,其中
    • 服務端總是經過身份驗證
    • 客戶端可以選擇性進行身份驗證
    • 每個連接都會產生不同並且不相關的密鑰
    • 密鑰材料(keying material)可用於 0-RTT 和 1-RTT 數據包的保護
  • 兩個端點(both endpoints)傳輸參數的認證值,以及服務端傳輸參數的保密保護
  • 應用協議的認證協商(TLS 使用 ALPN)

下面顯示了一個簡化的握手以及相關數據包和幀的交換過程。用 “*” 表示在握手過程中可以進行應用數據交換。一旦握手完成,端點就可以交換應用數據。

Client Server Initial (CRYPTO) 0-RTT (*) ----------> Initial (CRYPTO) Handshake (CRYPTO) <---------- 1-RTT (*) Handshake (CRYPTO) 1-RTT (*) ----------> <---------- 1-RTT (HANDSHAKE_DONE,*) 1-RTT (*) <=========> 1-RTT (*)

注意:

  • CRYPTO 幀可以在不同的數據包編號空間(packet number spaces)中發送。CRYPTO 幀使用偏移量(offsets)來確保加密握手數據的有序傳遞的在每個包編號空間(packet number spaces)都是從零開始。
  • 端點需要顯式地協商應用協議(TLS 使用 ALPN)。這避免了對所使用的協議存在分歧的情況。

在QUIC中,數據包編號分為3個空間:

  • 初始(Initial)空間:所有初始包都在這個空間中。
  • 握手(Handshake)空間:所有握手包都在此空間中。
  • 應用數據(Application data)空間:所有 0-RTT 和 1-RTT 加密的數據包都在這個空間中。

握手流程示例

QUIC 在握手前會先進行地址驗證(Address Validation),確保請求包里面的源地址不是偽造的。

一旦地址驗證交換完成,就可以使用加密握手來獲取加密密鑰。加密握手通過初始(Initial)和握手(Handshake)包進行傳輸。

下圖展示了 1-RTT 握手的示例。每行顯示一個 QUIC 包(packet),首先顯示包類型(type)和包編號(number),然后是幀(frames)。例如,第一個包是 Initial 類型,包編號為 0,並且包含一個攜帶 ClientHello(縮寫:CH) 的 CRYPTO 幀。

多個 QUIC 數據包(即便是不同的類型)也可以合並成一個單獨的 UDP 數據報(datagram)。因此,下圖所示的 1-RTT 握手可以由 4 個UDP數據報(datagrams)組成。如果受協議固有的限制(如擁塞控制(congestion control)和反放大(anti-amplification))也可以使用更多的數據報。

Client Server Initial[0]: CRYPTO[CH] -> Initial[0]: CRYPTO[SH] ACK[0] Handshake[0]: CRYPTO[EE, CERT, CV, FIN] <- 1-RTT[0]: STREAM[1, "..."] Initial[1]: ACK[0] Handshake[0]: CRYPTO[FIN], ACK[0] 1-RTT[0]: STREAM[0, "..."], ACK[0] -> Handshake[1]: ACK[0] <- 1-RTT[1]: HANDSHAKE_DONE, STREAM[3, "..."], ACK[0]

 

下圖展示了一個 0-RTT 握手的連接示例:

Client Server Initial[0]: CRYPTO[CH] 0-RTT[0]: STREAM[0, "..."] -> Initial[0]: CRYPTO[SH] ACK[0] Handshake[0] CRYPTO[EE, FIN] <- 1-RTT[0]: STREAM[1, "..."] ACK[0] Initial[1]: ACK[0] Handshake[0]: CRYPTO[FIN], ACK[0] 1-RTT[1]: STREAM[0, "..."] ACK[0] -> Handshake[1]: ACK[0] <- 1-RTT[1]: HANDSHAKE_DONE, STREAM[3, "..."], ACK[1]

注意:服務端在 1-RTT 包中確認了(ACK)客戶端發送 的 0-RTT 數據包,而客戶端在相同的包編號空間(packet number space)中發送 1-RTT 數據包。

 

QUIC 使用連接ID(而不是 ip + port)來確保數據包的路由一致性。如果用戶的 IP 發生變化時,比如從移動蜂窩 4G 網絡切換到 WiFi,IP 地址會改變。而使用一個唯一的連接ID 可以確保用戶的 IP 變化時業務請求依然能夠被繼續處理,不用重新建連,可以繼續使用當前連接ID 路由數據包,因此 QUIC 可以通過這個特性支持連接遷移。下面我們將了解 QUIC 在握手時是如何協商連接ID,以及如何驗證連接ID。

協商連接ID

QUIC 的數據包(packets)長報頭(long header)包含兩個連接ID:目標連接ID(Destination Connection ID)由數據包的接收者選擇並用於提供一致的路由,源連接ID(Source Connection ID)用於對端(peer)響應時使用的目標連接ID(Destination Connection ID)。

在握手過程中,帶有長報頭的包用於建立兩端(both endpoints)使用的連接ID。在處理第一個初始數據包(Initial packet)之后,每個端點使用其接收到的源連接ID(Source Connection ID)字段的值設置為后續數據包中的目標連接ID(Destination Connection ID)字段。

當客戶端發送了一個初始包(Initial packet),而該客戶端之前沒有從服務端接收過初始數據包(Initial packet)或重試包(Retry packet),則客戶端將用一個不可預測的值(長度至少為8字節)填充到目標連接ID字段。在從服務端接收到數據包之前,客戶端必須對該連接中的所有數據包使用相同的目標連接ID值。

當第一次從服務端接收到初始(Initial)或重試(Retry)數據包時,客戶端使用服務端提供的源連接ID作為后續數據包(包括任何 0-RTT 數據包)的目標連接ID。這意味着在建立連接的過程中,客戶端可能需要兩次更改它的目標連接ID字段:一次用於響應重試(Retry),一次用於響應來自服務端的初始數據包(Initial)。一旦客戶端從服務端接收到有效的初始數據包,客戶端必須丟棄它后續接收到的具有不同源連接ID的數據包。

服務端必須根據第一個接收到的初始數據包(Initial packet)的源連接ID,設置為用於發送數據包的目標連接ID。后續只有當接收到 NEW_CONNECTION_ID 幀時,才允許對目標連接ID進行更改。如果后續初始數據包包含不同的源連接ID,則必須將其丟棄。這樣可以避免由於無狀態(stateless)處理具有不同源連接ID的多個初始數據包而導致的不可預測結果。

端點可以在連接的生命周期內更改發送的目標連接ID,特別是在響應連接遷移(connection migration)時。

驗證連接ID

QUIC 通過在傳輸參數(transport parameters)中包含相應的值來驗證每個端點在握手過程中所選擇的連接ID。這可以確保用於握手的所有連接ID也通過加密握手進行身份驗證。

每個端點都包含源連接ID字段的值,該字段來自它發送的第一個初始(Initial)數據包的 initial_source_connection_id 傳輸參數中。服務端在 original_destination_connection_id 傳輸參數中包含它從客戶端接收到的第一個初始(Initial)數據包的目標連接ID字段。如果服務端發送了一個重試(Retry)數據包,這是指在發送重試(Retry)數據包之前接收到的第一個初始(Initial)數據包。如果發送重試(Retry)數據包,服務端還會在 retry_source_connection_id 傳輸參數中包含來自重試數據包的源連接ID字段。

對端(peer)為這些傳輸參數提供的值必須與端點發送的初始(Initial)數據包的目標(Destination)或源(Source)連接ID字段中的值匹配。在傳輸參數中包含連接ID值並對其進行驗證,可確保攻擊者不會在握手過程中通過注入帶有攻擊者選擇的連接ID的數據包來影響連接ID的選擇。

需要將端點沒有傳輸參數 initial_source_connection_id 或服務端中沒有傳輸參數 original_destination_connection_id 視為類型為TRANSPORT_PARAMETER_ERROR的連接錯誤。

端點必須將以下情況視為TRANSPORT_PARAMETER_ERROR 或 PROTOCOL_VIOLATION類型的連接錯誤:

  • 接收到服務端的重試(Retry)數據包后,缺少傳輸參數retry_source_connection_id
  • 當未收到重試(Retry)數據包時,存在retry_source_connection_id傳輸參數
  • 從對端(peer)接收的傳輸參數的值與在初始包(Initial packets)對應的目標(Destination)或源(Source)連接ID字段中的值不匹配。

下圖顯示了完整握手中使用的連接ID(DCID = Destination Connection ID,SCID = Source Connection ID)。顯示了初始(Initial)包的交換,以及隨后交換的 1-RTT 數據包,其中包括在握手過程中建立的連接ID。

Client                                                  Server

Initial: DCID=S1, SCID=C1 ->
                                  <- Initial: DCID=C1, SCID=S3 ... 1-RTT: DCID=S3 -> <- 1-RTT: DCID=C1

下圖顯示了一個包含重試(Retry)包的握手。

Client                                                  Server

Initial: DCID=S1, SCID=C1 ->
                                    <- Retry: DCID=C1, SCID=S2 Initial: DCID=S2, SCID=C1 -> <- Initial: DCID=C1, SCID=S3 ... 1-RTT: DCID=S3 -> <- 1-RTT: DCID=C1

在這兩種情況下,客戶端將傳輸參數initial_source_connection_id的值設置為 C1。

當握手不包括重試(Retry)時,服務端將original_destination_connection_id設置為 S1,將initial_source_connection_id設置為 S3。在這種情況下,服務端不包括retry_source_connection_id傳輸參數。客戶端接收到服務端的初始(Initial)包后,驗證 original_destination_connection_id 是否與之前所設置的 DCID(目標連接ID)一致,然后將 DCID 更新為 S3。

當握手包括重試(Retry)時,服務端將original_destination_connection_id設置為 S1,retry_source_connection_id設置為 S2,initial_source_connection_id設置為 S3。客戶端對應的分別將 DCID 更新為 S3 和 S3。

傳輸參數(Transport Parameters)

在建立連接期間,兩個端點都對其傳輸參數進行了身份驗證聲明。端點需要遵守每個參數定義的限制,每個參數的描述包括它的處理規則。

傳輸參數是由每個端點單方面作出的聲明。每個端點可以選擇傳輸參數的值,而不依賴於其對端(peer)選擇的值。

QUIC 在加密握手中包含編碼后(encoded)的傳輸參數。一旦握手完成,對端(peer)聲明的傳輸參數就可用了。每個端點驗證其對端(peer)提供的值。

端點必須將以下情況視為TRANSPORT_PARAMETER_ERROR類型的連接錯誤:

  • 接收到的含有無效值的傳輸參數
  • 接收到重復的傳輸參數

應用層協議協商(Application Layer Protocol Negotiation, ALPN)允許客戶端在建立連接期間提供多個應用協議。客戶端在握手過程中的傳輸參數包含了它的所支持的所有應用協議。應用協議可以為傳輸參數推薦值,例如初始流量控制限制(initial flow control limits)。然而,對傳輸參數的值設置約束的應用協議可能使客戶端無法在這些約束沖突時提供多個應用協議。

0-RTT 的傳輸參數值

使用 0-RTT 取決於客戶端和服務端使用的協議參數是否是之前的連接協商過的。要啟用 0-RTT,端點將存儲服務端傳輸參數的值,並將其應用於在后續連接中發送到該對端(peer)的任何 0-RTT 數據包。這個信息與應用協議或加密握手所需的信息一起存儲。

存儲的傳輸參數將應用於新連接,直到握手完成並且客戶端開始發送 1-RTT 數據包。一旦握手完成,客戶端將使用握手中建立的傳輸參數。並非所有傳輸參數都會被存儲,因為有些參數不適用於后續的連接,或者它們對 0-RTT 的使用沒有影響。

新傳輸參數的定義必須指定存儲 0-RTT 的傳輸參數是強制的(mandatory)、可選的(optional)、或者是禁止的(prohibited)。客戶端不需要存儲它無法處理的傳輸參數。

客戶端不能存儲以下參數的值:

  • ack_delay_exponent
  • max_ack_delay
  • initial_source_connection_id
  • original_destination_connection_id
  • preferred_address
  • retry_source_connection_id
  • stateless_reset_token

這些不能存儲的參數值,客戶端必須在 0-RTT 握手中使用服務端的新值,如果服務端沒有提供新值,則使用默認值。

嘗試發送 0-RTT 數據的客戶端必須記住服務端使用的所有其他能存儲的傳輸參數,並且服務端能夠處理這些參數。服務端可以記住這些傳輸參數,或者在票證(ticket)中存儲的參數值的完整性保護(integrity-protected)副本,並在接受 0-RTT 數據時恢復信息。服務端使用傳輸參數來確定是否接受 0-RTT 數據。

如果服務端接受 0-RTT 數據,則服務端不得減少任何限制或更改任何可能被客戶端與其 0-RTT 數據沖突的值。特別是,接受 0-RTT 數據的服務端不得將以下參數設置為小於之前記住(remembered)的參數值。

  • active_connection_id_limit
  • initial_max_data
  • initial_max_stream_data_bidi_local
  • initial_max_stream_data_bidi_remote
  • initial_max_stream_data_uni
  • initial_max_streams_bidi
  • initial_max_streams_uni

忽略或設置某些傳輸參數的零(zero)值可能會導致啟用 0-RTT 數據,但不可用。對於 0-RTT,允許發送應用數據的傳輸參數的子集應設置為非零(non-zero)值。這包括:

  • initial_max_data
  • initial_max_streams_bidi 和 initial_max_stream_data_bidi_remote, 或者initial_max_streams_uni 和 initial_max_stream_data_uni.

服務端可以存儲和恢復之前發送的 max_idle_timeoutmax_udp_payload_sizedisable_active_migration的參數值。如果選擇較小的值,則拒絕 0-RTT。在接受 0-RTT 數據的同時降低這些參數的值可能會降低連接的性能。具體地說,與直接拒絕 0-RTT 數據相比,降低max_udp_payload_size可能會導致丟包,從而導致性能更差。

如果傳輸參數的還原值不被支持,服務端必須拒絕 0-RTT 數據。

當以 0-RTT 數據包發送幀(frames)時,客戶端必須只使用之前記住(remembered)的傳輸參數。重要的是,它不能使用從服務端獲取到的新傳輸參數更新參數值,也不能從接收到的 1-RTT 數據包中的更新值。握手傳輸參數的更新值僅適用於 1-RTT 數據包。例如,記住(remembered)的傳輸參數的流量控制限制(flow control limits)適用於所有 0-RTT 包,即使這些值通過握手或在 1-RTT 包中被增加了。服務端可能會將 0-RTT 中更新傳輸參數視為PROTOCOL_VIOLATION類型的連接錯誤。

新傳輸參數

新的傳輸參數可用於協商新的協議行為。端點必須忽略它不支持的傳輸參數。因此,缺少傳輸參數將禁用使用該參數協商的任何可選(optional)協議功能。

客戶端不理解的傳輸參數可以丟棄並在后續連接上嘗試 0-RTT。但是,如果客戶端添加了對丟棄的傳輸參數的支持,那么在嘗試 0-RTT 時,可能會違反傳輸參數所建立的約束。新的傳輸參數可以通過設置最保守(most conservative)的默認值來避免這個問題。

加密消息緩沖

QUIC 實現(Implementations)需要維護無序接收的加密數據的緩沖區(buffer)。由於CRYPTO幀沒有流量控制(flow control),端點可能會強制其對端(peer)緩沖無限量(unbounded)的數據。

QUIC 實現(Implementations)必須支持緩沖在無序 CRYPTO幀中接收的至少 4096 字節的數據。端點可以選擇允許在握手期間緩沖更多的數據。握手過程中的更大限制可能允許交換更大的密鑰(keys)或憑據(credentials)。端點的緩沖區大小不需要在連接的生命周期內保持不變。

無法在握手期間緩沖CRYPTO幀可能導致連接失敗。如果在握手過程中超過了端點的緩沖區,則可以臨時擴展其緩沖區以完成握手。如果端點不擴展其緩沖區,則必須使用CRYPTO_BUFFER_EXCEEDED錯誤代碼關閉連接。

一旦握手完成,如果一個端點無法緩沖CRYPTO幀中的所有數據,它可能會丟棄該CRYPTO幀和將來接收到的所有CRYPTO幀,或者可能使用CRYPTO_BUFFER_EXCEEDED錯誤代碼關閉連接。必須確認(ACK)包含丟棄的CRYPTO幀的數據包,因為即使丟棄了CRYPTO幀,該數據包也已被接收和處理。

參考鏈接:

https://medium.com/@chester.yw.chu/http-3-%E5%82%B3%E8%BC%B8%E5%8D%94%E8%AD%B0-quic-%E7%B0%A1%E4%BB%8B-5f8806d6c8cde

https://quicwg.org/base-drafts/draft-ietf-quic-transport.html#name-cryptographic-and-transportport.html#name-cryptographic-and-transport

Even faster connection establishment with QUIC 0-RTT resumption


QUIC 的全稱是 Quick UDP Internet Connections protocol,由 Google 設計提出,目前由 IETF 工作組推動進展,其設計的目標是替代 TCP 成為 HTTP/3 的數據傳輸層協議。熹樂科技在物聯網(IoT)和邊緣計算(Edge Computing)場景也一直在打造底層基於 QUIC 通訊協議的邊緣計算微服務框架YoMo,長時間關注 QUIC 協議的發展,本系列文章總結了學習 QUIC 協議時的知識點。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM