要約

  • HPACK動的テーブルのインデックスは、HTTP/2接続の正しい方向、正しい符号化・復号コンテキスト、正しい処理順序の中でしか意味を持たない。
  • フィールドリストを復元できてもキャッシュヒットの証明にはならない。保存、キー、鮮度、検証、権限、アプリケーション結果は別々に記録する必要がある。

運用画面に「HPACK cache hit」と表示される。送信側は長いフィールドを再送せず、受信側は短い番号から同じオクテット列を復元した。この説明までは正しい。ところが表示名は、過去のレスポンスが保存され、現在のリクエストに再利用されたかのような結論まで持ち込んでいる。

HPACKとHTTPキャッシュは、どちらも反復を利用して負担を減らす。ただし、覚える対象が違う。HPACKは接続内でフィールドリストを再構成するための状態を持つ。HTTPキャッシュは、将来のリクエストに答えるためレスポンスメッセージを保存する。

Roberto Peonは、この境界に直接関わる標準の共著者である。Hervé RuellanとともにRFC 7541を執筆した。IETF Datatrackerの人物ページには、HPACKと最初のHTTP/2仕様であるRFC 7540が掲載されている。また、2012年のQCon San Francisco講演者紹介は、当時のPeonをGoogleのエンジニアでSPDYの共同開発者と説明する。これは経歴の時点資料であり、現在の所属を示すものではない。現行HTTP/2仕様のRFC 9113にも別の著者がいる。

圧縮層が扱うのは順序付きオクテット列

RFC 7541では、ヘッダーフィールドリストは名前と値のペアを順序づけた集合である。同じペアが重複してもよい。圧縮層にとって名前と値は不透明なオクテット列であり、復号後も元の順序が保たれなければならない。

したがって、cache-control: privateを復元できたことは、privateを適用したことではない。HPACKはその意味を判定せず、プライベートキャッシュを作らず、共有キャッシュの保存可否も決めない。HTTP層が復元後にフィールドを解釈する。

HPACKには静的テーブルと動的テーブルがある。静的テーブルは事前定義された読み取り専用の表だ。動的テーブルは空の状態から始まり、フィールドブロックを処理するたびに更新され、重複したエントリーも許す。容量上限を守るため古いエントリーは追い出される。

インデックスは、この動くアドレス空間を指す。新しいエントリーが最も若い動的位置に入り、古いものの位置が変わる。追い出された後に番号だけを残しても、対象は保存されない。別の接続では同じ数字が別のフィールドを示すか、無効になる。インデックスは永続IDではない。

一つの接続でも方向ごとに別の記憶を持つ

RFC 7541は、双方向通信における一つのエンドポイントの符号化用と復号用の動的テーブルを完全に分離する。クライアントがリクエストを符号化する表と、レスポンスを復号する表は同じではない。サーバーも逆向きの二つを持つ。

RFC 9113では、各エンドポイントが一本の接続にHPACKエンコーダーコンテキストとデコーダーコンテキストを持ち、その方向の全フィールドブロックで使う。動的テーブルは各コンテキストの中心的な可変状態だ。

「接続のテーブルにあった」というログだけでは再現できない。どちらの端点か、どの方向か、何番目のブロックより前か、上限と実効状態はいくつかが必要になる。受信側が一回の挿入を見落とせば、その後の同じインデックスを送受信側が別のオクテット列として理解しうる。

破棄する予定のメッセージであっても、フィールドブロックを再構成して復号しなければならない理由もここにある。ブロックが後続ストリームの復号状態を更新するからだ。アプリケーションが一つのストリームを不要と判断しても、圧縮状態の履歴は飛ばせない。

容量制御は順序を持つ交渉である

HTTP/2接続の開始時、動的テーブルの初期最大値は4096バイトである。デコーダーはSETTINGS_HEADER_TABLE_SIZEで許容上限を伝え、エンコーダーはその範囲内で実効容量を選び、HPACKのサイズ更新命令で変更を示す。

4096はプロトコルの初期値であり、すべての実装の実測値ではない。上限縮小はSETTINGSと確認応答の順序にも依存する。縮小が確認された後、必要であれば次のフィールドブロックは適合するサイズ更新から始めなければならない。

挿入時には容量に収まるまで古いエントリーが追い出される。一個のエントリーが最大値より大きければ、テーブルは空になり、そのエントリーは残らない。ここにあるのは保存期間を管理するアーカイブではなく、制限された圧縮辞書である。

状態の食い違いは接続全体に波及する。RFC 9113は、フィールドブロックを復号できない場合、COMPRESSION_ERRORとして接続を終了するよう求める。症状は一つのストリームに現れても、壊れたコンテキストは同じ方向の他のストリームにも使われる。これは圧縮会話を続けられない証拠であって、業務処理の内容を否定する証拠ではない。

HTTPキャッシュはレスポンスの再利用を決める

RFC 9111は、HTTPキャッシュをレスポンスメッセージのローカルな保存場所と、その保存・検索・削除を制御するサブシステムとして定義する。以前のレスポンスで後のリクエストを満たすことが目的だ。

選択には少なくともリクエストメソッドと対象URIを含むキャッシュキーが要る。Varyで指定されたリクエストフィールドも照合に加わりうる。そのうえで鮮度、許可された古いレスポンスの利用、オリジンでの検証、no-storeprivateno-cacheなどを評価する。

HPACKエントリーにはその判断材料がない。etag: "blue"のオクテット列を持っても、対応する表現は保存していない。age: 300を圧縮しても現在年齢は計算しない。vary: accept-languageを復元しても二つのリクエストを比較しない。

cache-control: privateを動的テーブルへ入れる場面を考える。エンコーダーは繰り返しを利用したいだけかもしれない。その挿入はプライベートキャッシュを作らず、共有キャッシュの判断もしない。HTTPキャッシュが完全なメッセージを解釈して初めて意味を適用する。HPACKからエントリーが消えてもレスポンスは残りうるし、どこにもレスポンスを保存しなくてもエントリーは残りうる。

逆も成り立つ。本物のキャッシュヒットから返すフィールドが、リテラル、静的テーブル参照、あるいは動的テーブルが空の新規接続で送られることはある。圧縮参照とレスポンス再利用は互いの必要条件でも十分条件でもない。

復号成功は信頼の判定ではない

有効なインデックスが証明するのは限定的だ。その時点で受信側が互換性のある圧縮状態を持ち、フィールドを復元できた。次にHTTPの名前と値の妥当性、チャネルの性質、中継者による変換、オリジン到達、アプリケーション処理を別々に調べる必要がある。

機密性にも境界がある。RFC 7541は、攻撃者がフィールドを制御し、圧縮後の長さを観測できると、動的テーブル状態を探れると警告する。TLSが内容を暗号化しても長さの手がかりは残る。圧縮率の変化がすべて攻撃ではない一方、短くなったことも安全の証明ではない。

「決してインデックス化しない」リテラル表現は、値を動的テーブルへ入れず、中継者が再符号化する際にもその扱いを保つよう求める。これは具体的な防御だが、推測しやすい値を強い秘密に変えず、長さ情報を消さず、アクセス権限を与えない。

証跡を二つに分ける

圧縮の証跡には、接続ID、端点の役割、方向、ストリーム、フィールドブロック順序を残す。さらに、通知された上限と確認時点、処理前後の実効サイズ、挿入、追い出し、サイズ更新、表現形式、復号結果を記録する。

HTTPの証跡は復元後から始まる。メソッド、URI、ステータス、オリジンまでの経路、解釈済みのフィールド意味を持つ。キャッシュを使ったなら、キー、Vary入力、保存判断、年齢、鮮度、バリデーター、認可関連規則、再検証結果、アプリケーションへ渡した結果を追加する。

パケットキャプチャはHPACK同期を証明してもキャッシュ内部を知らないことがある。キャッシュログは再利用を証明しても回線上の圧縮を示さないことがある。欠けた欄は空欄のままにするべきだ。フィールドの記憶からレスポンス再利用の権限を作ってはならない。

情報源