HTTP/2 HPACK圧縮: RFC 7540とRFC 7541に基づく詳細解説

Tech

本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

HTTP/2 HPACK圧縮: RFC 7540とRFC 7541に基づく詳細解説

背景

HTTP/1.1は、テキストベースのシンプルなプロトコルであり、拡張性に富んでいました。しかし、複数のリソースを同時に取得する際のレイテンシの増大や、ヘッダの冗長性によるオーバーヘッドといった課題を抱えていました。特に、Webページの複雑化とモバイルデバイスの普及に伴い、これらの課題は顕著になりました。

HTTP/2は、これらの課題に対処するために設計され、2015年5月にRFC 7540として標準化されました。HTTP/2の主要な改善点の一つが、ヘッダ圧縮技術であるHPACKです。HPACKは、RFC 7541で詳細が定義されており、HTTP/1.1の課題であったヘッダの冗長性とオーバーヘッドを大幅に削減することを目指しました。

設計目標

HPACK(Header Compression for HTTP/2)の設計目標は以下の通りです[1][2]。

  • ヘッダの効率的な圧縮: 複数のリクエストやレスポンス間で共通するヘッダフィールドを効率的に圧縮し、転送データ量を削減すること。特にCookieやUser-Agentなど、冗長になりがちなヘッダの最適化。

  • セキュリティの確保: CRIME/BREACH攻撃のような圧縮オラクル攻撃(Compression Oracle Attack)に対する耐性を持つこと。これは、圧縮によって秘密情報が推測されるリスクを回避することを意味します。

  • サーバとクライアント間の状態共有: ヘッダ圧縮の効率を高めるため、サーバとクライアント間でヘッダテーブルの状態を共有し、以前に送信されたヘッダフィールドを参照可能にすること。

  • 簡潔なヘッダ表現: ヘッダをビットレベルで効率的に表現し、パースコストを低減すること。

詳細

HPACKは、主に以下の3つのメカニズムを組み合わせてヘッダを圧縮します。

1. 静的テーブル (Static Table)

HPACKは、HTTPで頻繁に使用されるヘッダフィールドや値をあらかじめ定義した「静的テーブル」を持ちます[2]。このテーブルはクライアントとサーバ間で共有されており、接続確立時に特別なネゴシエーションを必要としません。例えば、:method: GET:status: 200といった項目が、それぞれ小さなインデックス番号で表現されます。これにより、これらのヘッダフィールドを毎回文字列として送信するのではなく、わずか数バイトのインデックスで参照できるようになります。

2. 動的テーブル (Dynamic Table)

静的テーブルに加え、HPACKは接続中に動的に更新される「動的テーブル」をクライアントとサーバで共有します[2]。これは、以前に送信されたヘッダフィールドや値の組み合わせを格納し、それらを新しいインデックスで参照できるようにするものです。新しいヘッダフィールドが送信されるたびに、それが動的テーブルに追加され、後続の通信でそのインデックスを利用して圧縮されます。動的テーブルの最大サイズは、HTTP/2のSETTINGSフレーム(SETTINGS_HEADER_TABLE_SIZE)を用いてネゴシエート可能です[1]。

3. ハフマン符号化 (Huffman Coding)

静的テーブルや動的テーブルに存在しないヘッダ名や値は、文字列リテラルとして送信されます。この文字列リテラルをさらに効率的に転送するため、HPACKはハフマン符号化を適用します[2]。HTTPヘッダフィールドで頻繁に出現する文字のシーケンス(例: “content-type”)には短いビット列が割り当てられ、出現頻度の低い文字には長いビット列が割り当てられることで、全体のデータ量を削減します。

HPACKエンコードのフロー

flowchart TD
    A["ヘッダフィールドの入力"] --> B{"静的テーブルに存在するか?"};
    B -- Yes --> C["静的テーブルのインデックスで参照"];
    B -- No --> D{"動的テーブルに存在するか?"};
    D -- Yes --> E["動的テーブルのインデックスで参照"];
    D -- No --> F{"動的テーブルに追加するか?"};
    F -- Yes --> G["動的テーブルに追加"];
    G --> H{"文字列値はハフマン符号化が必要か?"};
    F -- No --> H;
    H -- Yes --> I["ハフマン符号化された文字列リテラル"];
    H -- No --> J["そのままの文字列リテラル"];
    C --> K["エンコード済みヘッダブロックの出力"];
    E --> K;
    I --> K;
    J --> K;
  • A[ヘッダフィールドの入力]: エンコード対象のHTTPヘッダフィールド(例: content-type: application/json)が入力されます。

  • B{静的テーブルに存在するか?}: まず、このヘッダフィールドがHPACKの静的テーブルに定義されているかを確認します。

  • C[静的テーブルのインデックスで参照]: 存在する場合、そのインデックス(例: 2)を使ってヘッダフィールドを表します。

  • D{動的テーブルに存在するか?}: 静的テーブルに存在しない場合、動的テーブルに存在するかを確認します。

  • E[動的テーブルのインデックスで参照]: 存在する場合、そのインデックス(例: 61)を使ってヘッダフィールドを表します。

  • F{動的テーブルに追加するか?}: どちらのテーブルにも存在しない場合、このヘッダフィールドを動的テーブルに追加するかを決定します。特定のコンテキスト(例: Authorizationヘッダなど)では、セキュリティ上の理由から追加しない選択も可能です(Never-Indexed Literals)。

  • G[動的テーブルに追加]: 追加する場合、新しいエントリとして動的テーブルに登録されます。

  • H{文字列値はハフマン符号化が必要か?}: ヘッダ名や値がテーブル参照ではない文字列リテラルとして送信される場合、その文字列をハフマン符号化するかを決定します。

  • I[ハフマン符号化された文字列リテラル]: ハフマン符号化が適用され、より短いビット列に変換されます。

  • J[そのままの文字列リテラル]: ハフマン符号化が適用されない場合、文字列はそのままの形で送信されます。

  • K[エンコード済みヘッダブロックの出力]: 最終的に、インデックス参照または符号化された文字列リテラルとしてヘッダブロックが出力されます。

HTTP/2フレーム構造とHPACK

HTTP/2では、すべての通信がフレームとして送受信されます[1]。ヘッダはHEADERSフレームまたはPUSH_PROMISEフレームの一部として、HPACKエンコードされた「ヘッダブロックフラグメント」として転送されます。

HEADERS Frame Payload (excerpt):
  Pad Length:8           // Paddingのバイト数 (オプション)
  Header Block Fragment:variable // HPACKエンコードされたヘッダデータ
  Padding:variable       // Paddingデータ (オプション)

HPACKエンコードされたヘッダブロックフラグメントは、複数のCONTINUATIONフレームに分割されて送信されることもあります。

HTTP/2接続確立シーケンスとHPACK

HTTP/2の接続確立時、クライアントとサーバはSETTINGSフレームを交換し、特にSETTINGS_HEADER_TABLE_SIZEパラメータを通じてHPACKの動的テーブルの最大サイズをネゴシエートします。

sequenceDiagram
    participant C as Client
    participant S as Server

    C -> S: TCP SYN
    S -> C: TCP SYN-ACK
    C -> S: TCP ACK

    C -> S: TLS ClientHello
    S -> C: TLS ServerHello, Certificate, ServerKeyExchange, ServerHelloDone
    C -> S: TLS ClientKeyExchange, ChangeCipherSpec, Encrypted Handshake Message
    S -> C: TLS ChangeCipherSpec, Encrypted Handshake Message
    Note over C,S: TLSハンドシェイク完了

    C -> S: PRIORITY Frame (Optional)
    C -> S: SETTINGS Frame (SETTINGS_HEADER_TABLE_SIZE=4096, etc.)
    S -> C: SETTINGS Frame (SETTINGS_HEADER_TABLE_SIZE=4096, etc.)
    S -> C: SETTINGS ACK Frame
    C -> S: SETTINGS ACK Frame
    Note over C,S: HTTP/2接続確立、HPACK動的テーブルサイズネゴシエート完了

    C -> S: HEADERS Frame (HPACKエンコードされたリクエストヘッダ)
    S -> C: HEADERS Frame (HPACKエンコードされたレスポンスヘッダ)
  • C->S: SETTINGS Frame: クライアントはSETTINGS_HEADER_TABLE_SIZEを含むSETTINGSフレームを送信し、動的テーブルの初期最大サイズを提案します。デフォルトは4096バイトです。

  • S->C: SETTINGS Frame: サーバも同様にSETTINGSフレームを送信し、自身の動的テーブル設定を伝えます。

  • S->C: SETTINGS ACK Frame: サーバはクライアントのSETTINGSフレームを受信・処理したことを確認します。

  • C->S: SETTINGS ACK Frame: クライアントもサーバのSETTINGSフレームを受信・処理したことを確認します。 このネゴシエーションにより、両端でHPACKの動的テーブルサイズが同期され、ヘッダ圧縮が開始可能になります。

相互運用

HTTP/2 vs HTTP/1.1

  • ヘッダ圧縮: HTTP/1.1はヘッダ圧縮メカニズムを持たず、全てのヘッダをプレーンテキストで送信します。HTTP/2はHPACKにより、静的/動的テーブルとハフマン符号化を用いてヘッダを効率的に圧縮します。これにより、HTTP/2は特に多数のリクエスト/レスポンスを伴うシナリオで、オーバーヘッドを大幅に削減します。

  • 多重化: HTTP/1.1はパイプライン処理が可能ですが、依然としてHead-of-Line Blocking(HOL Blocking)が発生しやすいです。HTTP/2は単一のTCP接続上で複数のストリームを多重化し、HOL Blockingをトランスポート層で緩和します。

  • 二進プロトコル: HTTP/1.1はテキストベースですが、HTTP/2はフレームベースの二進プロトコルです。これにより、パースが高速化され、エラー耐性も向上します。

HTTP/2 vs HTTP/3

  • トランスポート層: HTTP/2はTCP上で動作します。HTTP/3はQUIC(UDPベース)上で動作します。QUICはTCPのHOL Blocking問題を解決し、より高速な接続確立(0-RTT接続)、マルチパス接続、信頼性の高いUDPを実現します[3][4]。

  • ヘッダ圧縮: HTTP/2はHPACKを使用します。HTTP/3はHPACKから派生したQPACK(RFC 9204)を使用します[5]。HPACKはTCPのin-order保証に依存しているため、QUICのような多重化されたUDPストリームでは、一つのヘッダ圧縮ストリーム上の損失が他のストリームのヘッダデコードをブロックする可能性があります(Application-layer HOL Blocking)。QPACKはこの問題を解決するため、動的テーブルの更新を専用のストリームで行い、ヘッダブロックの参照を特定の動的テーブルバージョンに紐付けることで、他のストリームのデコードをブロックしないように設計されています。

  • 0-RTT: HTTP/2は0-RTTを直接サポートしませんが、TLS 1.3の0-RTTモードを利用できます。HTTP/3はQUICのプロトコルレベルで0-RTT接続をサポートしており、ハンドシェイクを削減して高速な接続確立が可能です。

セキュリティ考慮

HPACKとHTTP/2の設計において、いくつかのセキュリティ考慮点がなされています[1][2]。

  • 圧縮オラクル攻撃 (CRIME/BREACH対策): HTTP/1.1で問題となったCRIME/BREACH攻撃は、圧縮率の変化を観測することで秘密情報を推測するものです。HPACKは以下のメカニズムでこれに対処しています。

    • ハフマン符号化の静的辞書: ハフマン辞書は静的であり、攻撃者が動的に辞書を操作することはできません。

    • Never-Indexed Literals: Authorizationヘッダのような機密性の高い情報は、動的テーブルに追加されず、常にリテラルとして送信されるべきとRFC 7541で推奨されています。これにより、同じ値が繰り返し送信されても圧縮率が高まらないため、攻撃者が特定の値を推測する手掛かりを減らします。

    • テーブルサイズ制限: SETTINGS_HEADER_TABLE_SIZEにより、動的テーブルの最大サイズを制限できます。これにより、テーブルへの過剰なエントリ追加によるメモリ枯渇攻撃を防ぎます。

  • リプレイ攻撃: HTTP/2はTLS上で動作することを前提としているため、TLSによってリプレイ攻撃は防止されます。HPACK自体にはリプレイ保護のメカニズムはありません。

  • ダウングレード攻撃: HTTP/2へのダウングレード攻撃は、接続開始時にPRIOR_KNOWLEDGEを使用しない限り、通常TLS ALPNネゴシエーション中に発生します。HTTP/2プロトコル自体がTLSを必須としているため、ダウングレードはHTTPSからHTTP/1.1へのダウングレードとして扱われ、TLSの保護下で行われます。

  • キー更新: TLSセッションのキー更新は、TLSプロトコル自身によって管理されます。HPACKのテーブル状態はTLSセッションとは独立して維持されますが、TLSセッションが終了すれば、そのHPACKテーブルも破棄されます。

  • 0-RTTの再送リスク: HTTP/2はTLS 1.3の0-RTTを直接利用できますが、0-RTTデータはリプレイ攻撃のリスクを伴います。特にべき等ではないリクエスト(例: POST)を0-RTTで送信する場合、サーバはそれらを慎重に処理する必要があります。HTTP/2プロトコル自体は、0-RTTリクエストの処理に関して具体的なガイダンスを提供していませんが、TLS 1.3の勧告に従うべきです。

実装メモ

HPACKを実装する際の注意点およびHTTP/2の一般的な実装考慮事項です。

  • MTU/Path MTU: HTTP/2フレームはTCPパケットにカプセル化されるため、下位層のMTU(Maximum Transmission Unit)やPath MTUを考慮する必要があります。特に大きなHEADERSフレームやDATAフレームを送信する際は、TCPセグメント化が適切に行われるように、フラグメント化を適切に管理する必要があります。

  • HOL blocking回避: HTTP/2はTCPレベルでのHOL blockingは緩和しますが、HPACKの動的テーブルの更新が順不同で届いた場合、アプリケーションレベルのHOL blockingが発生する可能性があります。例えば、参照先のテーブルエントリがまだ追加されていないヘッダブロックはデコードできません。このため、実装はテーブル更新を適切に順序付けて処理する必要があります。HTTP/3のQPACKはこの問題を解決しています。

  • キュー制御と優先度: HTTP/2はストリームごとに優先度を設定できます[1]。これにより、重要なリソース(HTML、CSSなど)のデータがより早く転送されるよう、送信キューを制御する必要があります。HPACKヘッダブロックの送信もこの優先度付けに従うべきです。

  • 動的テーブル管理:

    • サイズ制限: SETTINGS_HEADER_TABLE_SIZEでネゴシエートされた最大サイズを厳守する必要があります。テーブルがサイズを超えた場合、古いエントリを削除して新しいエントリを追加するEvictionポリシーを実装します。

    • エントリの追加と参照: 新しいヘッダを送信する際、それが動的テーブルに追加されるべきかを適切に判断します。また、受信側はテーブルのインデックスを正しくデコードし、テーブルの状態を送信側と同期させる必要があります。

    • ハフマンデコーダ/エンコーダ: ハフマン符号化された文字列の効率的なエンコード/デコードロジックを実装する必要があります。

まとめ

HTTP/2のHPACK圧縮は、HTTP/1.1の主要な課題の一つであったヘッダの冗長性を劇的に改善し、Webパフォーマンスの向上に大きく貢献しました。静的テーブル、動的テーブル、ハフマン符号化の組み合わせにより、データ転送量を削減し、CRIME/BREACH攻撃のようなセキュリティリスクにも対応しています。

しかし、HPACKはTCPの特性に依存しているため、HTTP/3で採用されたQPACKでは、QUICのマルチプレックス環境下でのアプリケーションレベルHOL Blockingを回避する設計が導入されました。ネットワークプロトコルの進化は継続しており、HPACKはその重要な一里塚として、現代のWeb通信基盤を支える技術の一つであり続けています。RFC 7540とRFC 7541に記載された原則は、将来のプロトコル設計においても重要な指針となるでしょう。


参考文献

[1] IETF. (2015, May). RFC 7540: Hypertext Transfer Protocol Version 2 (HTTP/2). https://www.rfc-editor.org/rfc/rfc7540 (閲覧日: 2024年7月30日) [2] IETF. (2015, May). RFC 7541: HPACK – Header Compression for HTTP/2. https://www.rfc-editor.org/rfc/rfc7541 (閲覧日: 2024年7月30日) [3] IETF. (2021, May). RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport. https://www.rfc-editor.org/rfc/rfc9000 (閲覧日: 2024年7月30日) [4] IETF. (2022, June). RFC 9114: HTTP/3. https://www.rfc-editor.org/rfc/rfc9114 (閲覧日: 2024年7月30日) [5] IETF. (2022, June). RFC 9204: QPACK – Header Compression for HTTP/3. https://www.rfc-editor.org/rfc/rfc9204 (閲覧日: 2024年7月30日)

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました