QUICをゆっくり解説(19):バージョン・ネゴシエーション | IIJ Engineers Blog

QUICをゆっくり解説(19):バージョン・ネゴシエーション

2022年07月21日 木曜日


【この記事を書いた人】
山本 和彦

Haskellコミュニティでは、ネットワーク関連を担当。 4児の父であり、家庭では子供たちと、ジョギング、サッカー、スキー、釣り、クワガタ採集をして過ごす。

「QUICをゆっくり解説(19):バージョン・ネゴシエーション」のイメージ

前回は「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に関する面白いテーマが見付かったら、また記事を書こうかと思います。

こちらの記事もおすすめ

QUICを実装した経験を持つ IIJ技術研究所の 山本 和彦 が、経験者目線でQUICを解説する連載記事です

すべての記事をみる

山本 和彦

2022年07月21日 木曜日

Haskellコミュニティでは、ネットワーク関連を担当。 4児の父であり、家庭では子供たちと、ジョギング、サッカー、スキー、釣り、クワガタ採集をして過ごす。

Related
関連記事