QUICをゆっくり解説(19):バージョン・ネゴシエーション
2022年07月21日 木曜日
CONTENTS
前回は「QUICバージョン2」について説明しました。アップグレードの例として、クライアントとサーバ共に、バージョン1もバージョン2もサポートしている場合を考えます。互換性の観点からクライアントは、バージョン1でコネクションを張ろうと試みるでしょう。サーバはハンドシェイクの際に、バージョン2をサポートしていることをクライアントへ伝える必要があります。今回は、このようなバージョンを交渉する仕組みについて説明します。
バージョン・ネゴシエーションの復習
忘れている方も多いとは思いますが、実はすでに「ネゴせよ」でバージョン・ネゴシエーションについて説明しています。バージョン・ネゴシエーションに使われるVersion Negotiationパケットは、ロングヘッダパケットであり、以下のような構造をしています。
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|X X X X X X X|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Version (32) = 0 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| DCID Len (8) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Connection ID (0..2040) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SCID Len (8) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Connection ID (0..2040) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Supported Version 1 (32) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [Supported Version 2 (32)] |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [Supported Version N (32)] |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
バージョンには0という値を入れて、Version Negotiationパケットであることを示します。サーバは、サポートしているバージョンをSupported Versionに列挙してクライアントに返します。
例を使ってバージョン・ネゴシエーションを復習しましょう。クライアントはVa、VbおよびVc、サーバはVaとVcをサポートしているとします。小文字のアルファベットは、aが一番低いバージョンで、b、cとバージョンが高くなるという意味です。
クライアントは、VbでInitialパケットを送るとします。外野から見れば、バージョンはVcが選ばれるべきですね。以下の図をご覧ください。
Version=Vb ------------------------------------------>
<---------------------- VN: Supported Version={Va, Vc}
Version=Vc ------------------------------------------>
<------------------------------------------ Version=Vc
- クライアントはVbで通信を始めます
- サーバはVbをサポートしていないので、Version Negotiationパケット(図中 VN)を返します。Supported Version はVaとVcです
- クライアントはVcを選んで、再びInitialパケットを送信します
- サーバもVcでハンドシェイクを開始します
これまでのバージョン・ネゴシエーションの問題点
これまでのバージョン・ネゴシエーションの問題点は、Version Negotiationパケットが暗号的に守られてないことです。第三者が、悪意のあるVersion Negotiationパケットを送ると、選ばれるべきでないVaを選択させることができます。これをダウングレード攻撃と言います。
ダウングレード攻撃の例を以下に示します。
Version=Vb ------------------------------------------>
<------ VN: Supported Version={Va} 第三者による注入
Version=Va ------------------------------------------>
<------------------------------------------ Version=Va
- クライアントはVbで通信を始めます
- 悪意のある第三者が、サーバからのVersion
Negotiationパケットを改竄したり、新たに作ったりして、偽のVersion
Negotiationパケットをクライアントへ届けます - クライアントはVaを選んで、再びInitialパケットを送信します
- サーバもVaでハンドシェイクを開始します
Vaに脆弱性が見つかったのでVbやVcが策定されたのであれば、クライアントとサーバは脆弱なバージョンでコネクションを張ったことになります。
Version Information
ダウングレード攻撃を防止するために現在標準化中なのが、Version Informationトランスポート・パラメータです。トランスポート・パラメータについては「QUICビットとトランスポート・パラメータ」を参照してください。
Version Informationトランスポート・パラメータは以下のような構造をしています。
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Chosen Version (32) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Other Version 1 (32) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [Other Version 2 (32)] |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [Other Version N (32)] |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Chosenバージョンは、サーバやクライアントが選んだバージョンです。サーバのアップグレードなどの特殊な場合を除き、これはロングヘッダパケットのバージョンの値と一致します。
Other Versionは、以下のような意味です。
- クライアントが送る場合:Initialパケットで選択されたバージョンと「互換」であるバージョンを優先順位の高い順に列挙します。Chosen Versionの値は必ず含まれます(含んだ方がサーバの実装が簡単になります)。互換の意味については後述します
- サーバが送る場合:その時点でデプロイされているバージョンを列挙します。特別な場合を除き、Chosen Versionの値が含まれます
この記事では、Version Informationを以下のように表記することにします。
VI = Chosen Version, {Other Version 1, Other Version 2, Other Version N}
Version Informationを使ったバージョン・ネゴシエーションの例を以下に示します。互換の意味はまだ説明していませんが、VbとVcは互換であり、Vaはどれとも互換でないとします。
Version=Vb VI=Vb,{Vb,Vc}----------------------------->
<----------------------- VN: Supported Version={Va,Vc}
Version=Vc VI=Vb,{Vb,Vc} ---------------------------->
<---------------------------- Version=Vc VI=Vc,{Va,Vc}
クライアントは以下の2つの検証を義務付けられています。
- Version Negotiationパケットが改竄されてないことを調べるために、Other Versionを検査します。具体的には、Other Versionの値が、Version Negotiationに入っていたとして、バージョンを選びます。その値と、最初のVersion Negotiationパケットから選んだ値が一致するか検査します
- ハンドシェイクが完了した後、Chosenバージョンと交渉の結果が一致するか検査します
クライアントからのVersion Informationは、ClientHelloに入っており、暗号的に守られていません。一方で、サーバからのそれは、EncryptedExtensionsに入っており暗号的に保護されています。すなわち、サーバからの2番目のパケットに入っているVersion Informationは信用できます。Supported VersionとOther Versionの中身は、通常一致しています。よって、選択の結果が一致すれば、Version Negotiationも改竄されていなかったと推測できます。
ダウングレード攻撃を防ぐ例も見てみましょう。
Version=Vb VI=Vb,{Vb,Vc}----------------------------->
<------ VN: Supported Version={Va} 第三者による注入
Version=Va VI=Va,{Va} ------------------------------->
<---------------------------- Version=Va VI=Vc,{Va,Vc}
- クライアントは、Supported VersionからVaを選んでいます
- クライアントは、Version InformationからはVcを選ぶでしょう
このように、不一致が検知できるのです。
互換バージョン・ネゴシエーション
これまでのバージョン・ネゴシエーションは、Version Negotiationパケットを使うので、ハンドシェイクに1往復余計な手間がかかっています。この方式を「非互換バージョン・ネゴシエーション」と言います。
バージョン・ネゴシエーションの文脈で、VaがVbと「互換」という場合、クライアントからのVaのInitialパケットを受け取ったサーバが、それを処理してVbに変換できることを意味します。変換できない場合は、「非互換」です。
非互換の場合も、QUICの不変条件として、バージョン・フィールドだけは構文解析できますので、Version Negotiationパケットを返すことができます。一方で、互換の場合は、この余分な1往復を省略できます。
以下に、VaとVbが互換の場合の例を示します。
Version=Va VI=Va,{Vb,Va}----------------------------->
<---------------------------- Version=Vb VI=Vb,{Vb,Va}
- クライアントは、互換性を高めるためにVaを選択していますが、Version InformationでVbの方が優先だとサーバへ伝えます
- サーバは、Vbを選んでハンドシェイクを開始します
互換バージョン・ネゴシエーションの場合は、2つの検証の内、2番目だけをクライアントが実行する必要があります。以下に再掲します。
- ハンドシェイクが完了した後、Chosenバージョンと交渉の結果が一致するか検査します
QUICバージョン2は、バージョン1と互換ですので、バージョン2へのアップグレードには互換バージョン・ネゴシエーションが利用できます。
合わせ技
非互換バージョン・ネゴシエーションと互換バージョン・ネゴシエーションが組み合わさって実行されることがあります。
たとえば、VaとVbが互換、VcとVdが互換で、VaとVcは非互換だとしましょう。クライアントはすべてのバージョンをサポートしていますが、サーバはVcとVdのみをサポートしています。この場合、以下のようなバージョンの折衝が起こり得ます。
Version=Va VI=Va,{Vb,Va}----------------------------->
<----------------------- VN: Supported Version={Vd,Vc}
Version=Vc VI=Vc,{Vc,Vd} ---------------------------->
<---------------------------- Version=Vd VI=Vd,{Vd,Vc}
- クライアントは、Vaを選び、互換であるVbの方が優先だとサーバに伝えます
- サーバはVaを知らないので、サポートしてるVdとVcをVersion
Negotiationパケットに入れて返します - クライアントは、Vcを選び、VdよりもVcの方が優先だとサーバに伝えます
- サーバは、Vdを選んでハンドシェイクを始めます
クライアントは2番目の検証として、交渉の結果がVdであることを確かめます。これは直感的に理解できるでしょう。
一方、1番目の検証はどうなるでしょうか? サーバから送られてきたOther Versionには、VdとVcが入っていて、これが Version Negotiationで送られたとすると、(実際そうしたように)クライアントはVcを選びます。これはクライアントが2番目のパケットを送ったときに選んだVcと一致するので、Version Negotiationパケットは改竄されてないと判定します。Vdではなく、Vcとなるのを検査するのは、直感的でなく、この仕様の最も理解し難い部分です。
トランスポート・パラメータの性質の変更
QUICバージョン1では、トランスポート・パラメータは静的であるように設計されています。すなわち、設定などから値はあらかじめ決まっていて、ハンドシェイクの途中で変わることがありません。
一方で、サーバが送るVersion Informationは、クライアントのVersion Informationを見て決める必要があります。QUICバージョン2以降では、Version Informationの実装が必須となっていますので、トランスポート・パラメータが静的であるという性質は失われました。実際、QUICバージョン2を実装するには、トランスポート・パラメータに関するAPIを変更しなければならないでしょう。
おまけ
前々回説明したQUICビットを乱数化する仕様は承認されたので、しばらくするとRFCとなって発行されます。
今回で、硬直化に関連した話題は終了です。QUICに関する面白いテーマが見付かったら、また記事を書こうかと思います。