That was QUIC.
昨年一昨年とQUICのハンドシェークについて書いてまいりました。
QUICの標準化はまだ続いており、三年間も同じような内容でアドベントカレンダーを書くことになろうとは思いもよりませんでした。実は2018年の記事が投稿されたその日に、QUICのドラフトが更新され、ハンドシェーク仕様が変更になり、なんとも賞味期間が短い記事となっていました。今年はどうでしょうか。数週間は大丈夫だと思います。QUICは未だ標準化の道半ばです。quic-transportの方は若干終わりの光明がうっすらと差してきた感があります。この記事は執筆時点での最新draft-24をベースに、筆者が説明しやすい部分だけを大いに端折って記述するといった内容になります。ほとんどの人にとってQUICのハンドシェークなど一生知らなくても良いものでしょうからアドベントカレンダーの内容にはうってつけです。Z Labに入社してもQUICの仕事は現在ありませんのでご注意ください。
- https://tools.ietf.org/html/draft-ietf-quic-transport-24
- https://tools.ietf.org/html/draft-ietf-quic-tls-24
- https://tools.ietf.org/html/rfc8446
2018年の記事は、一から書き直したのですが、今年は2018年版をベースに加筆修正しました。2017と2018の間の変化ほど大きな変化はありません。
TLSv1.3--
まず最初にTLSv1.3について話しておかなければなりません。QUICはTLSv1.3でハンドシェークをしますが、TLSv1.3そのままではなく、一部変更を加えたものになります:
- TLSレコードレイヤープロトコルを使わない
- EndOfEarlyDataメッセージを使わない
変更を加えたというか不要なものを取り去ったといういうべきか。
TLSv1.3の暗号化はTLSレコードレイヤープロトコルで行われています。QUICでは、TLSレコードレイヤープロトコルを使わず、TLSスタックから直接鍵を受け取り、QUICパケットペイロードを暗号化します。QUICは、Initial、Handshake、0-RTT、Shortのパケット種別が存在し、それぞれ暗号化する鍵が異なります。Initialパケット以外の暗号化鍵についてはTLSスタックから受け取る鍵をそのまま使います。
- Initialパケット: https://tools.ietf.org/html/draft-ietf-quic-tls-24#section-5.2 で示す方法で秘密を生成し、鍵を導出。ClientHello、ServerHelloを運ぶパケット。
- Handshakeパケット: client_handshake_traffic_secret (サーバーの場合はserver_handshake_traffic_secret)から導出する鍵で暗号化。ClientHello、ServerHello以外のハンドシェークTLSメッセージを運ぶパケット。
- 0-RTTパケット: client_early_traffic_secretから導出する鍵で暗号化。0RTTデータ(early data)を運ぶパケット。
- Shortパケット: client_application_traffic_secret (サーバーの場合はserver_application_traffic_secret)で暗号化。1RTTデータを運ぶパケット。
これら鍵の導出スケジュールは https://tools.ietf.org/html/rfc8446#section-7.1 に記載されています。運ぶTLSメッセージによって鍵が異なることから、スケジュールにそって、TLSスタックは厳密なタイミングでQUICスタックに鍵を教えてやる必要があります。
quic_transport_parameters TLS拡張で接続の設定を送信します。拡張が有効になるのはハンドシェーク完了後なので内容については触れません。
QUICのパケット番号と暗号化
QUICには4種類のパケット(この他にもありますが暗号化されないのでここでは割愛)があり、暗号化鍵が違います:
- Initial
- Handshake
- 0-RTT
- Short
もう一つ、以下のパケット番号の空間も以下のグループ毎に独立しています:
- Initial
- Handshake
- 0-RTT, Short
それぞれのグループでパケット番号は0から始まります。パケット番号とはTCPのシーケンス番号のようなものですが、重複は許さず、同一パケット番号空間のパケットはパケット番号で一意に識別できます。これはQUICがパケットを再送するのではなく、パケットのペイロードにエンコードした各フレーム単位に再送をするためです。
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-24#section-5.4) 。
パケット番号は、可変長整数でエンコード (https://tools.ietf.org/html/draft-ietf-quic-transport-24#section-16) するので最初の2ビットを見ないとバイト数がわからないのですが、パケット番号暗号化は、この先頭2ビットも含めて暗号化するので、最初のバイトを正しく復号しないとパケット番号のバイト数がわからないという仕様になっています。暗号化前のパケット番号部分は、QUICパケットペイロード暗号化のAEADのAdditional Dataになっているので、パケット番号を正しく復号できないとペイロードも復号化できません。暗号化の順番は、ペイロード、パケット番号の順で、復号はその逆です。
同じパケット番号空間に属するもののみACKすることができます。Initialパケットをacknowledgeするには、InitialパケットにACKを載せて送信しなければなりません。ハンドシェーク初期では、InitialとHandshakeそれぞれでACKだけを載せた小さいQUICパケットが必要になる場合がありますが、QUICでは一つのUDPパケットに複数のQUICパケットを詰め込んで送信することができるので、UDPパケット数を減らすことは可能です。
パケットサイズの制限
Amplification attack対策でハンドシェーク初期のパケットサイズには制限があります。
クライアントが最初に送信するInitialパケットは完全なClientHelloまたはその一部を含んでいますが、通常百バイトちょっとですが、少なくとも1200バイトになるようにパッディングをつけて送信しなければなりません。サーバーは、クライアントがアドレスを偽造していないと確かめるまでは、自身が受信したバイト数の三倍を超えるデータを送信してはならないとされています(これはMUST NOT, https://tools.ietf.org/html/draft-ietf-quic-transport-24#section-8.1 )。サーバーは、クライアントからのHandshakeパケットを正常に復号し処理できれば、クライアントがアドレスを偽造していないと信じることになっています。サーバーが何も送信できなくなる状態になることを防ぐため、クライアントはHandshakeパケットを送ることができるまで、Initialパケットを含むUDPパケットは少なくとも1200バイトになるようにパディングします。
以前はInitialパケットは完全なClientHelloを含んでいないといけないとされていましたが、この制限はなくなりました。理由は色々あったようですが、OpenSSLなどメジャーなTLS実装はクライアント認証した時、クライアントの証明書をsession ticketに含めるため、session resumptionした時のInitialパケットが、PMTUDなしに送信できるQUICパケットのサイズ(IPv4で1252バイト)を超えてしまうという現実的な問題がありました。
Connection ID
Connection IDはQUICコネクションを識別するIDですが、1接続につき、送信元Connection ID (Source Connection ID, SCID)と送信先Connection ID (Destination Connection ID, DCID)の二対を使用します。Initial、Handshake、0-RTT (いわゆるlong packet)は、SCID、DCID両方エンコードしますが、Shortパケットは、DCIDのみになります。long packetは、Connection IDの長さもエンコードするのですが、Shortパケットはエンコードしません。これはハンドシェークを通過した受信者なら知っているという前提です。Connection IDの内容は実装、運用者が好きにできるので、長さをエンコードするという方法もあるようです。
クライアントは、ランダムに生成する最小8バイトの予想不可能なDCIDと、自身を特定するSCIDをInitialパケットに載せてサーバーに送信します。サーバーは、自身を特定するSCIDを生成して、クライアントInitialパケットに書いているSCID (=クライアントSCID)をDCIDとして、パケットを送ります。クライアントは、サーバーから受け取ったパケットのSCIDをサーバーのSCID(つまり、クライアントがパケット送信時にDCIDとして使用する)として以後使用します。クライアントがランダムに生成するDCIDはInitialパケットを暗号化する鍵の導出に使います。
やっとハンドシェーク
さてやっとQUICのハンドシェークにやってきました。1RTTハンドシェークを順を追って見ていきましょう。
クライアントはInitialパケットを暗号化する鍵を生成します。この鍵は、draftに書いてあるsaltと、クライアントがランダムに選択するDCIDを元に導出します。
TLSスタックが生成するClientHello TLSメッセージをInitialパケットに載せてサーバーへ送信します。QUICパケットのペイロードはフレーム単位でエンコードされているのですが、TLSメッセージは、CRYPTOフレームでエンコードします。
Initialパケットを受け取ったサーバーは、パケットに書いてあるDCIDを使ってInitialパケットを暗号化、復号化する鍵をそれぞれ生成します。Initialパケットを復号してクライアントからのTLSメッセージをTLSスタックに渡します。
TLSスタックはServerHello TLSメッセージを生成するので、サーバーはInitialパケットで、先ほど生成した鍵で暗号化して送信します。続いてTLSスタックはserver_handshake_traffic_secretとclient_handshake_traffic_secretをQUICに通知し、残りのハンドシェークのTLSメッセージを生成します。その後、server_application_traffic_secret, client_application_traffic_secretをQUICに通知します。これらメッセージは、Handshakeパケットで送信します。client_application_traffic_secretを得ているのでサーバーはこの時点からクライアントのShortパケットを復号化できるのですが、ハンドシェークが終わってから使うようにとドラフトには書かれています。サーバーはserver_application_traffic_secretを使ってこの時点から0.5RTTのShortパケットを送信することが可能です。ただしハンドシェークが終了していないので認証が終わっていません。
クライアントはサーバーのInitialパケットを受信、復号後、TLSスタックに受信したTLSメッセージを渡します。TLSスタックはserver_handshake_traffic_secret, client_handshake_traffic_secretをQUICに通知します。これでクライアントはサーバーのHandshakeパケットを復号できるようになります。続いてやってくる(順番は前後する可能性はありますが)サーバーのHandshakeパケットを復号し、TLSメッセージをTLSスタックへ渡します。このTLSメッセージには証明書が含まれていて、通常複数のパケットになっていることが多いと思います。その後、TLSスタックはserver_application_traffic_secret, client_application_traffic_secretをQUICに通知します。TLSスタックは残りのハンドシェークTLSメッセージ(e.g., ClientFinished)と生成します。クライアントは、TLSメッセージをHandshakeパケットに載せてサーバーに送信します。クライアント側のハンドシェークは、再送を除くとこれで終了です。クライアントはserver_application_traffic_secretとclient_application_traffic_secretを使ってShortパケットを送受信することができます。
サーバーはクライアントからのHandshakeパケットを受信して復号し、TLSスタックへTLSメッセージを渡します。サーバー側のハンドシェークはこれで終了です。server_application_traffic_secretとclient_application_traffic_secretを使ってShortパケットを送受信することができます。
NewSessionTicketは、サーバーはハンドシェーク終了後に、サーバーがShortパケットでクライアントへ送信することになります。
0RTT
0RTTは、クライアントが、ClientHello生成後にTLSスタックが通知するclient_early_traffic_secretを使って、Initialパケットの後に続けて0-RTTパケットを送信する以外はほぼ同じです。サーバーは、クライアントのInitialパケットをTLSスタックに渡すと、TLSスタックが0RTTデータを許容すると判断した場合はclient_early_traffic_secretをQUICに通知するので、0-RTTパケットを復号化できます。
トークンベースのアドレスバリデーション
QUICはトークンベースなアドレスバリデーションをステートレスに行うことができます。
サーバーが、確かにクライアントがアドレスを所有していると確認した時にだけ何かしらリソースを割り当てたいと思っている時に有用です。
例えば、クライアントの最初のInitialパケットは完全なClientHelloを含まなくてもよくなったのでサーバーはInitialパケットの内容をバッファリングする必要が出てきます。サーバーはアドレスバリデーションを使って、バリデーションが成功した場合だけバッファリングを行う、といったようなことができます。
アドレスバリデーションを行うには、Retryパケットというのを使います。これは暗号化されていないパケットです。クライアントの最初のInitialパケットを受け取った時に、サーバーは、Retryトークンと呼ばれるトークンを生成しRetryパケットに含めてクライアントに送信します。Retryパケットを受け取ったクライアントは、Retryトークンを含め、Initialパケットをもう一度送ります。サーバーはRetryトークンの内容をみてバリデーションを行います。サーバーは、アドレスバリデーションに必要な状態をRetryパケットに入れておくことができるので、サーバー自体はステートレスであり続けることができるようになっています。
終わりに
三年目のQUICハンドシェークは如何でしたでしょうか。果たして四年目はあるのでしょうか。
来年はオリンピックイヤー、競技場も完成して大いに盛り上がることでしょう。
それではHappy New Year、良いお年を。
付録
TLSとQUICハンドシェーク 2018からの変更点
- 鍵導出のHKDFのラベルは"quic"ではなく、TLSv1.3と同様に"tls13"になりました。
- 0-RTT Protected packetは0-RTT packetに名称変更になりました。
- クライアントが最初に送信するInitialパケットが完全なClientHelloを含まなくてもよくなりました。
- アドレスバリデーションについて雑に追加しました。
- OpenSSL実装依存のEOEDの記述を削除、および、鍵の通知タイミングの修正。