QUIC brown fox jumps over the lazy dog
昨年のアドベントカレンダーに、 https://qiita.com/tatsuhiro-t/items/2c4e40923c5e359ca235 を書きました。あれから1年、QUICは未だ標準化策定中でハンドシェークの方式も変わりました。今後も変わり続けることでしょう。この記事は執筆時点での最新のdraft-16をベースに、説明が面倒な部分は適度に端折ってQUICのハンドシェークについて書きたいと思います。Z Labに入社してもQUICの仕事は現在ありませんのでご注意ください。
- https://tools.ietf.org/html/draft-ietf-quic-transport-16
- https://tools.ietf.org/html/draft-ietf-quic-tls-16
- https://tools.ietf.org/html/rfc8446
QUIC風味のTLSv1.3
まず最初にTLSv1.3について話しておかなければなりません。QUICはTLSv1.3でハンドシェークをしますが、TLSv1.3そのままではなく、一部変更を加えたものになります:
- TLSレコードレイヤープロトコルを使わない
- EndOfEarlyDataメッセージを使わない
- 鍵導出では、HKDF-Expand-Labelのラベルには
tls13
ではなく、quic
を使用する
TLSv1.3の暗号化はTLSレコードレイヤープロトコルで行われています。QUICでは、TLSスタックから鍵を受け取り、QUICパケットペイロードを暗号化します。鍵については、HKDF-Expand-Labelのラベルをtls13
ではなくquic
にする他は導出方法は同じです。QUICは、Initial、Handshake、0RTT Protected、Shortのパケット種別が存在し、それぞれ暗号化する鍵が異なります。Initialパケット以外の暗号化鍵についてはTLSスタックから受け取る鍵をそのまま使います。
- Initialパケット: https://tools.ietf.org/html/draft-ietf-quic-tls-16#section-5.2 で示す方法で秘密を生成し、鍵を導出。ClientHello、ServerHelloを運ぶパケット。
- Handshakeパケット: client_handshake_traffic_secret (サーバーの場合はserver_handshake_traffic_secret)から導出する鍵で暗号化。ClientHello、ServerHello以外のハンドシェークTLSメッセージを運ぶパケット。
- 0RTT Protectedパケット: client_early_traffic_secretから導出する鍵で暗号化。0RTTデータを運ぶパケット。
- Shortパケット: client_application_traffic_secret (サーバーの場合はserver_application_traffic_secret)で暗号化。1RTTデータを運ぶパケット。
これら鍵の導出スケジュールは https://tools.ietf.org/html/rfc8446#section-7.1 に記載されています。運ぶTLSメッセージによって鍵が異なることから、スケジュールにそって、TLSスタックは厳密なタイミングでQUICスタックに鍵を教えてやる必要があります。
EndOfEarlyData (EOED)メッセージ削除によって鍵スケージュールが一部変更になりますが(EOED待たなくてもサーバーは、client_handshake_traffic_secretを得ることができる)、TLSスタック内で解決できるので問題ないでしょう。
これはTLSv1.3への改変ではありませんが、quic_transport_parameters TLS拡張で接続の設定を送信します。拡張が有効になるのはハンドシェーク完了後なので内容については触れません。
QUICのパケット番号と暗号化
UICには4種類のパケットがあり、暗号化鍵が違います:
- Initial
- Handshake
- 0RTT Protected
- Short
もう一つ、以下のパケット番号の空間も以下のグループ毎に独立しています:
- Initial
- Handshake
- 0RTT Protected, Short
それぞれのグループでパケット番号は0から始まります。
QUICでは、QUICパケットのペイロードを暗号化し、QUICパケットヘッダーは平文なのですが、パケット番号の部分だけ暗号化します。鍵は、Initialパケットはdraftに書いてある方式で、それ以外TLSスタックから得た秘密(client_handsake_traffic_secret, etc)からHKDF-Expand-Labelを使って導出します。暗号化スイートですが、ネゴシエートした暗号化スイートに依存して決まり、AES系の場合はAES-CTR、ChaCha20系の場合はChaCha20を使います (https://tools.ietf.org/html/draft-ietf-quic-tls-16#section-5.4) 。
パケット番号は、可変長整数でエンコード (https://tools.ietf.org/html/draft-ietf-quic-transport-16#section-16) するので最初の2ビットを見ないとバイト数がわからないのですが、パケット番号暗号化は、この先頭2ビットも含めて暗号化するので、最初のバイトを正しく復号しないとパケット番号のバイト数がわからないという仕様になっています。暗号化前のパケット番号部分は、QUICパケットペイロード暗号化のAEADのAdditional Dataになっているので、パケット番号を正しく復号できないとペイロードも復号化できません。暗号化の順番は、ペイロード、パケット番号の順で、復号はその逆です。
同じパケット番号空間に属するもののみACKすることができます。Initialパケットをacknowledgeするには、InitialパケットにACKを載せて送信しなければなりません。ハンドシェーク初期では、ACKだけを載せた小さいQUICパケットが必要になるのですが、QUICでは一つのUDPパケットに複数のQUICパケットを詰め込んで送信することができるので、UDPパケット数を減らすことは可能です。
パケットサイズの制限
Amplification attack対策でハンドシェーク初期のパケットサイズには制限があります。
クライアントが最初に送信するInitialパケットは完全なClientHelloを含んでいますが、通常百バイトちょっとですが、最小1200バイトになるようにパッディングをつけて送信しなければなりません。サーバーは、クライアントがアドレスを偽造していないと確かめるまでは、自身が受信したバイト数の三倍を超えるデータを送信してはならないとされています(これはMUST NOT)。サーバーは、クライアントからのHandshakeパケットを正常に復号し処理できれば、クライアントがアドレスを偽造していないと信じることになっています。サーバーが何も送信できなくなる状態になることを防ぐため、クライアントはHandshakeパケットを送ることができるまで、全てのInitialパケットは最小1200バイトになるようにパディングします。
Connection ID
Connection IDはQUICコネクションを識別するIDですが、昨年は1つでしたが、現在、1接続につき、二つになっています。送信元Connection ID (Source Connection ID, SCID)と送信先Connection ID (Destination Connection ID, DCID)です。Initial、Handshake、0RTT Protected (いわゆるlong packet)は、SCID、DCID両方エンコードしますが、Shortパケットは、DCIDのみになります。
クライアントは、ランダムに生成するDCIDと、自身を特定するSCIDをInitialパケットに載せてサーバーに送信します。サーバーは、自身を特定するSCIDを生成して、クライアントInitialパケットに書いているSCID (=クライアントSCID)をDCIDとして、パケットを送ります。クライアントは、サーバーから受け取ったパケットのSCIDをサーバーのSCIDとして以後使用します。クライアントがランダムに生成するDCIDはInitialパケットを暗号化する鍵の導出に使います。
やっとハンドシェーク
さてやっとQUICのハンドシェークにやってきました。1RTTハンドシェークを順を追って見ていきましょう。
クライアントはInitialパケットを暗号化する鍵を生成します。この鍵は、draftに書いてあるsaltと、クライアントがランダムに選択するDCIDを元に導出します。
TLSスタックが生成するClientHello TLSメッセージをInitialパケットに乗せてサーバーへ送信します。QUICパケットのペイロードはフレーム単位でエンコードされているのですが、TLSメッセージは、CRYPTOフレームでエンコードします。
Initialパケットを受け取ったサーバーは、パケットに書いてあるDCIDを使ってInitialパケットを暗号化、復号化する鍵をそれぞれ生成します。パケットを復号してクライアントからのTLSメッセージをTLSスタックに渡します。
TLSスタックはServerHello TLSメッセージを生成するので、サーバーはInitialパケットで、先ほど生成した鍵で暗号化して送信します。続いてTLSスタックはserver_handshake_traffic_secretとclient_handshake_traffic_secretをQUICに通知し、残りのハンドシェークのTLSメッセージを生成します。その後、server_application_traffic_secretをQUICに通知します。これらメッセージは、Handshakeパケットで送信します。
クライアントはサーバーのInitialパケットを受信、復号後、TLSスタックに受信したTLSメッセージを渡します。TLSスタックはserver_handshake_traffic_secretをQUICに通知します。これでクライアントはサーバーのHandshakeパケットを復号できるようになります。続いてやってくる(順番は前後する可能性はありますが)サーバーのHandshakeパケットを復号し、TLSメッセージをTLSスタックへ渡します。このTLSメッセージには証明書が含まれていて、通常複数のパケットになっていることが多いと思います。TLSスタックは、QUICにserver_application_traffic_secret、client_handshake_secretを通知して、残りのハンドシェークTLSメッセージを生成します。その後、client_application_traffic_secretをQUICに通知します。クライアントは、TLSメッセージをHandshakeパケットに載せてサーバーに送信します。クライアント側のハンドシェークは、再送を除くとこれで終了です。server_application_traffic_secretとclient_application_traffic_secretを使ってShortパケットを送受信することができます。
サーバーはクライアントからのHandshakeパケットを受信して復号し、TLSスタックへTLSメッセージを渡します。TLSスタックは、client_application_traffic_secretをQUICに通知します。サーバー側のハンドシェークはこれで終了です。すでに得ているserver_application_traffic_secretとclient_application_traffic_secretを使ってShortパケットを送受信することができます。
NewSessionTicketは、サーバーはハンドシェーク終了後に、サーバーがShortパケットでクライアントへ送信することになります。
0RTT
0RTTは、クライアントが、ClientHello生成後にTLSスタックが通知するclient_early_traffic_secretを使って、Initialパケットの後に続けて0RTT Protectedパケットを送信する以外はほぼ同じです。サーバーは、クライアントのInitialパケットをTLSスタックに渡すと、TLSスタックが0RTTデータを許容すると判断した場合はclient_early_traffic_secretをQUICに通知するので、0RTT Protectedパケットを復号化できます。
トークンベースのアドレスバリデーション
QUICはトークンベースなアドレスバリデーションをステートレスに行うことができます。もはや何を言っているのかわからないと思いますが、締め切りが近いので内容は触れません。
終わりに
締め切りが間近のため、駆け足になりました。締め切りに追われる作家、クリエーターの気持ちを味わっていただければ幸いです。それではHappy New Year、良いお年を。