QUICをゆっくり解説(11):ヘッダの保護
2021年10月14日 木曜日
CONTENTS
QUICと「TCP上のTLS 1.3」との決定的な違いに、トランスポート層に関連する情報が保護されているか否かが挙げられます。TLS 1.3では情報が格納されるTCPヘッダは保護できません。一方QUICでは、トランスポート層に関する情報の多くがペイロードに暗号化されて格納されます。またヘッダの一部のフィールドも保護されます。今回は、ヘッダの保護について解説します。
TCP Fast Openのジレンマ
「ハンドシェイク」の記事で説明したように、QUICと「TCP上のTLS 1.3」を比較すると、前者の方がハンドシェイクが1RTT分早く終わります(下図)。2回目以降のハンドシェイクで0-RTTを使う場合も、前者の方が1RTT分早く終わります。これはTCPに対するQUICの優位な点と言えるでしょう。
TCPに詳しい方は、「TCPにはTCP Fast Open(TFO)がある」と主張したくなるかもしれません。TFOだと2回目以降のコネクションでは、クライアントからのSYNパケットにアプリケーションデータを格納できます。確かにTFOを使えば、0-RTTの際には、RTTの回数に差はなくなります。
しかし、残念ながらTFOはあまり使われていません。なぜなら、以下のような中間装置が存在するからです。
- 知らないTCPオプションを削除する
- SYNパケットにデータが乗っていた場合は、パケットを落とす
TFOはTCPオプションを使うので、このような中間装置があると利用できなくなります。そして、このような中間装置が置き換わる速度は残念ながら遅いと言わざるを得ません。
このように、パケットの中身を見て何かをする中間装置があると、プロトコルの進化の妨げになります。進化を担保するには、中間装置がパケットを解析できないようにすることが望まれます。
QUICのヘッダ保護
TCPではヘッダの中にあったざまざまな情報をQUICではペイロードに入れて暗号化することで、中間装置がパケットを解析できないようにしています。しかし、QUICのヘッダの中には、まだ保護したいフィールドが残されています。それはパケット番号です。
パケット番号はパケットごとに単調に増加します。マイグレーションしても、パケット番号がリセットされることはありません。もし仮にパケット番号が保護されてないと、マイグレーションの際にコネクションIDを変えても、パケット番号から同一のコネクションだと関連づけられるかもしれません。
QUICのヘッダ保護とは、ヘッダにある主にパケット番号に関連したフィールドの値を、パケットごとに変わるマスクとXORを取り、結果の値で置き換えることです。元に戻すには、同じマスクで XOR を取ればよいですね。
保護されるフィールド
保護の対象となるロングヘッダパケットのヘッダは、以下のような共通部分を持っています。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+
|1|1|T T|R R|P P|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Version (32) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| DCID Len (8) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Connection ID (0..160) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SCID Len (8) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Connection ID (0..160) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Packet Number (8/16/24/32) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Protected Payload (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
これらのフィールドの内、保護されるのは以下のとおりです。
- 1バイト目のフラグの中の下位4ビット (“RRPP”の部分)
- RR は予約ビット
- PP はパケット番号の長さ
- パケット番号(Packet Number)
なおTTは、InitialやHandshakeといったパケットの型なので、保護はできません。
ショートヘッダパケットのヘッダは、以下のような構造を持っています。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+
|0|1|S|R|R|K|P P|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Connection ID (0..160) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Packet Number (8/16/24/32) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Protected Payload (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
これらのフィールドの内、保護されるのは以下のとおりです。
- 1バイト目のフラグの中の下位5ビット (“RRKPP”の部分)
- RR は予約ビット
- K はペイロードを守る鍵のフェーズ
- PP はパケット番号の長さ
- パケット番号(Packet Number)
中間装置は、「予約ビットが未知の値であれば、パケットを落とす」といったポリシーを実装できませんね。そのため、予約ビットを活用したオプションを追加する際に、中間装置が妨げになることを防げるでしょう。
マスクの生成
パケットごとに異なるマスクはどうやって生成するのでしょうか? パケットごとに異なると言えばパケット番号ですが、ペイロードを暗号化するときのnonceとして使われていますし、自分を守るのに自分を使うのは無理がありそうです。
Fastlyの奥一穂さんは、暗号化されたペイロードが利用できることに気付きました。彼のアイディアを基に、マスクは以下のように生成されます。
- クライアントからのInitialパケットにある終点コネクションIDからヘッダ保護用の鍵を生成する。このヘッダ保護用の鍵は、どの種類のパケットにも共通に使われ、1-RTT用の鍵が更新されても同じ鍵を使い続ける。
- あるパケットに対し、暗号化されたペイロードの一部を切り出し、ヘッダ保護用の鍵と共にマスクを生成する。
もし、ある通信に対してInitialパケットの情報を保存するような中間装置があれば、ヘッダの保護は解かれてしまいます。しかし、中間装置が知っているのは、IPアドレス、ポート番号、そしてヘッダ保護の鍵なので、必要であればマイグレーションすることで追跡から逃れられるでしょう。