Edited at
Z LabDay 23

TLSとQUICハンドシェーク

More than 1 year has passed since last update.

TLSと関係の深いQUICのハンドシェークについて紹介します。QUICの仕様は執筆時点の最新版draft-08を元にしています。TLSv1.3の仕様は、draft-22を元にしています。


QUIC

QUICはGoogleのQUICプロトコル(gQUIC)を元に、IETF QUIC WGで標準化が進んでいるプロトコルです。IETF版QUICはgQUICを元に開発されているのですが、すでにまったく別物になっていて互換性はありません。QUICはUDPの上で設計されています。

QUICは以下のような特徴が有ります。


  • 低遅延の接続確立

  • ストリーム間のHead-of-line (HOL) blockingに影響されない論理多重化

  • 認証付き暗号化ヘッダーとペイロード

暗号化通信を行う場合は、TCP+TLSの組み合わせが多いと思います。この場合、TCPのハンドシェークの後、TLSのハンドシェークを行うため、ハンドシェークを2回行う必要があります。QUICハンドシェークは暗号化も含んでいるため一度ですみ無駄がありません。フルハンドシェークで1-RTT、resumptionで0-RTTでデータ送信が可能です。

QUICはTLSv1.3のハンドシェークを使用しています。

QUICの概要は https://tools.ietf.org/html/draft-ietf-quic-transport-08#section-3 を読むといいでしょう。


ハンドシェークを行うストリーム

QUICはストリーム多重化を行います。ストリーム0は、常に開いているという仕様になっていて、ハンドシェークのデータは、ストリーム0でやりとりすることになっています。0-RTTデータ以外は、すべてストリーム0でやりとりされています。


パケット

ハンドシェークで使うQUICのパケットは以下のような種類があります:


  • Initial

  • Retry

  • Handshake

  • 0-RTT Protected

これらハンドシェークで使うパケットについて詳しく知りたい場合は、https://tools.ietf.org/html/draft-ietf-quic-transport-08#section-5.1 を読むといいでしょう。


Initialパケット

Initialパケットはクライアントが最初にサーバーに向けて送信するパケットであり、TLSv1.3 Client Helloが含まれています。Initialパケットは完全なClient Helloを含んでいなければならず、Client Helloを分割して複数のパケットで送信するということはできません。


Retryパケット

これはstateless retryを行うためのパケットで、Initialパケットを受信したサーバーがクライアントに向けて送信するものです。このパケットはHelloRetryRequestを含んでいます。stateless retryについては割愛します。


Handshakeパケット

ハンドシェークを行うためのパケットで、InitialやRetryパケットで運ぶ特別なハンドシェークメッセージ以外を運ぶためのものです。フルハンドシェークの場合、サーバーはInitialパケットを受け取った後、Server Hello、その他のハンドシェークメッセージを含んだHandshakeパケットをクライアントへ向けて送信します。クライアントはサーバーからのHandshakeパケットをすべて受け取った後、client Finishedを含むHandshakeパケットをサーバーへ送ります。これらTLSはハンドシェークメッセージは複数のHandshakeパケットに分けて送信することが可能です。

TLSv1.3と同じようにclient Finishedをサーバーが受信した時点でハンドシェーク完了になります。


0-RTT Protectedパケット

0-RTTアプリケーションデータを送信するためパケットです。クライアントはInitialパケットの後、続いて0-RTT Protectedパケットを送信します。


パケット保護

QUICのパケットはパケットヘッダーを除くペイロードは暗号化されています。一部例外がありますがここでは触れません。

Initial、Retry、Handshakeパケットは、固定のsaltとクライアントが選んだConnection IDを元に生成した秘密を元に鍵を生成して、AEAD_AES_128_GCMで暗号化します。固定のsaltはQUICのバージョン毎に異なる値を使うことになっていて、暗号化の目的は、一つのQUICバージョンしか知らない通信経路上のミドルボックスが別のQUICバージョンのパケットの内容を変更できないようにするためです。

0-RTT Protectedパケットは、TLSv1.3のearly_exporter_master_secretから導出した秘密を元に鍵を生成して、暗号化します。暗号化スイートは、TLSv1.3で0-RTTを送るときの暗号化スイートと同じです。このようにQUICでは0-RTTデータは、QUIC独自の暗号化キーとパケットで送ることになっていますが、0-RTT Protectedパケットを送信したいクライアントは、TLSv1.3で0-RTTデータを送るときの約束ごとは守らなければなりません。early_dataやpre_shared_key extensionが必要です。end_of_early_dataメッセージも送信しなくてはなりません。このあたりはTLSv1.3の実装を改変なくそのまま使おうとする工夫でしょう。

サーバーが0-RTT Protectedパケットに含まれるデータに対して応答を行う場合は、別の種類のパケットを使います。詳細は割愛しますが、Shortヘッダーを使うパケットを使います。ハンドシェーク後に使うパケットなのですが、exporter_master_secretから導出した秘密を元に鍵を生成して、暗号化します。

秘密や鍵の生成方法については https://tools.ietf.org/html/draft-ietf-quic-tls-08#section-5 に詳しく書かれています。


実例

QUICの実装であるngtcp2クライアントとサーバーでハンドシェークした実例を見てみましょう。フルハンドシェークしたときのクライアントからみたパケットの送受信です:

t=0.000484 TX Initial(0x7f) CID=0xd415c827f34bf88a PKN=4122534781 V=0xff000008

STREAM(0x12) FIN=0 LEN=1 OFF=0
stream_id=0x0 fin=0 offset=0 data_length=316
; BIDI
PADDING(0x00)
length=899
t=0.001975 RX Handshake(0x7d) CID=0x937a5e85c1ba591e PKN=1285008477 V=0xff000008
STREAM(0x12) FIN=0 LEN=1 OFF=0
stream_id=0x0 fin=0 offset=0 data_length=1204
; BIDI
; TransportParameter received in EncryptedExtensions
; negotiated_version=0xff000008
; supported_version[0]=0xff000008
; initial_max_stream_data=262144
; initial_max_data=1048576
; initial_max_stream_id_bidi=0x190
; initial_max_stream_id_uni=0x0
; idle_timeout=30
; omit_connection_id=0
; max_packet_size=65527
; stateless_reset_token=a5b389a23dfd4a76f65795d708d70ff4
; ack_delay_exponent=3
t=0.002447 RX Handshake(0x7d) CID=0x937a5e85c1ba591e PKN=1285008478 V=0xff000008
STREAM(0x16) FIN=0 LEN=1 OFF=1
stream_id=0x0 fin=0 offset=1204 data_length=351
; BIDI
; Negotiated cipher suite is TLS13-AES-128-GCM-SHA256
; Negotiated ALPN is hq-08
t=0.002818 QUIC handshake has completed
t=0.002898 TX Handshake(0x7d) CID=0x937a5e85c1ba591e PKN=4122534782 V=0xff000008
STREAM(0x16) FIN=0 LEN=1 OFF=1
stream_id=0x0 fin=0 offset=316 data_length=64
; BIDI

TXは送信、RXは受信です。CIDはConnection ID、PKNはバケット番号です。VはQUICのバージョン0です。


  1. クライアントがInitialパケットを送信

  2. サーバーが2個のHandshakeパケットを送信

  3. クライアントがHandshakeパケットを送信

というように4個のパケットが送受信されています。サーバーが2個のパケットを送信しているのは1個のパケットに収まりきらなかったためです。最後のパケット送信をもってハンドシェーク完了となります。

次に0-RTTでデータを送る場合の例をみましょう:

t=0.000826 TX Initial(0x7f) CID=0xd215616fdb826fe6 PKN=659735510 V=0xff000008

STREAM(0x12) FIN=0 LEN=1 OFF=0
stream_id=0x0 fin=0 offset=0 data_length=549
; BIDI
PADDING(0x00)
length=666
t=0.000894 TX 0-RTT Protected(0x7c) CID=0xd215616fdb826fe6 PKN=659735511 V=0xff000008
STREAM(0x13) FIN=1 LEN=1 OFF=0
stream_id=0x4 fin=1 offset=0 data_length=16
; BIDI
t=0.001837 RX Handshake(0x7d) CID=0xe8ebe108c625444b PKN=3801386834 V=0xff000008
STREAM(0x12) FIN=0 LEN=1 OFF=0
stream_id=0x0 fin=0 offset=0 data_length=339
; BIDI
; TransportParameter received in EncryptedExtensions
; negotiated_version=0xff000008
; supported_version[0]=0xff000008
; initial_max_stream_data=262144
; initial_max_data=1048576
; initial_max_stream_id_bidi=0x190
; initial_max_stream_id_uni=0x0
; idle_timeout=30
; omit_connection_id=0
; max_packet_size=65527
; stateless_reset_token=31e692d7f4f04a5a61bfd89e9ae083a0
; ack_delay_exponent=3
; Negotiated cipher suite is TLS13-AES-128-GCM-SHA256
; Negotiated ALPN is hq-08
t=0.002423 QUIC handshake has completed
t=0.002477 RX Short 01(0x1f) CID=0xe8ebe108c625444b PKN=3801386835
STREAM(0x13) FIN=1 LEN=1 OFF=0
stream_id=0x4 fin=1 offset=0 data_length=177
; BIDI
ordered STREAM data stream_id=0x4
00000000 3c 68 74 6d 6c 3e 3c 62 6f 64 79 3e 3c 68 31 3e |<html><body><h1>|
00000010 49 74 20 77 6f 72 6b 73 21 3c 2f 68 31 3e 0a 3c |It works!</h1>.<|
00000020 70 3e 54 68 69 73 20 69 73 20 74 68 65 20 64 65 |p>This is the de|
00000030 66 61 75 6c 74 20 77 65 62 20 70 61 67 65 20 66 |fault web page f|
00000040 6f 72 20 74 68 69 73 20 73 65 72 76 65 72 2e 3c |or this server.<|
00000050 2f 70 3e 0a 3c 70 3e 54 68 65 20 77 65 62 20 73 |/p>.<p>The web s|
00000060 65 72 76 65 72 20 73 6f 66 74 77 61 72 65 20 69 |erver software i|
00000070 73 20 72 75 6e 6e 69 6e 67 20 62 75 74 20 6e 6f |s running but no|
00000080 20 63 6f 6e 74 65 6e 74 20 68 61 73 20 62 65 65 | content has bee|
00000090 6e 20 61 64 64 65 64 2c 20 79 65 74 2e 3c 2f 70 |n added, yet.</p|
000000a0 3e 0a 3c 2f 62 6f 64 79 3e 3c 2f 68 74 6d 6c 3e |>.</body></html>|
000000b0 0a |.|
000000b1
t=0.002876 TX Handshake(0x7d) CID=0xe8ebe108c625444b PKN=659735512 V=0xff000008
STREAM(0x16) FIN=0 LEN=1 OFF=1
stream_id=0x0 fin=0 offset=549 data_length=84
; BIDI

送受信されたパケットを見てみましょう:


  1. クライアントがInitialパケットを送信

  2. クライアントが0-RTT Protectedパケットを送信

  3. サーバーがHandshakeパケットを送信

  4. サーバーがShortパケットを送信

  5. クライアントがHandshakeパケットを送信

2でクライアントが送信した0-RTT Protectedパケットは、HTTPリクエストを含んでいました。それに対する応答が、4でサーバーが送信したShortパケットに含まれるテキストです。上図では、16進ダンプされた内容が表示されていることが確認できると思います。


終わりに

TLSと関係の深いQUICハンドシェークについて紹介しました。TLSv1.3のハンドシェークを再利用できるように設計されていることが解ると思います。