HPACK:HTTP/2 ヘッダ圧縮と Huffman 符号化
毎リクエストで繰り返す巨大なヘッダを、テーブル番号参照と Huffman でほぼゼロに縮める仕組みを内側から理解できます。CRIME を避けた圧縮設計の意図まで押さえられます。
- 1.HPACK は61件の静的テーブルとコネクション単位の動的テーブルを用い、過去に送ったヘッダを文字列ではなくインデックス番号で参照して冗長な再送を消す。
- 2.動的テーブルは先頭挿入・末尾退避の FIFO で増分更新され、エンコーダとデコーダが同じ命令列を同順で適用することで同期する。
- 3.リテラルは静的 Huffman 表で符号化される。TLS や SPDY のヘッダ圧縮が招いた CRIME を避けるため、HPACK は秘密情報を圧縮文脈から外せるよう「圧縮しない」リテラル表現を備える。
なぜヘッダ圧縮が要るのか
HTTP/1.1 では各リクエストのヘッダが毎回ほぼ丸ごとテキストで送られていました。user-agent や cookie のように長く、しかもリクエスト間でほとんど変わらないヘッダを何百回も繰り返すのは純粋な無駄です。HTTP/2 は1本のコネクションに多数のストリームを多重化するため、この冗長性がさらに増幅されます。HPACK(RFC 7541)はヘッダフィールドをインデックス番号と短い符号に置き換え、典型的なリクエストヘッダを数バイト級まで縮める専用の圧縮層です。HTTP のバージョン間の位置づけは /network/http-versions/ を参照してください。
2 種類のテーブルとインデックス空間
HPACK の中心は2つのテーブルです。両者は1つの連続したインデックス空間として扱われます。
- 静的テーブル: RFC が定める固定の61エントリ。
:method: GET、:status: 200、accept-encoding: gzip, deflateなど定番のヘッダ(名前のみ、または名前+値)が番号で引けます。コネクション間で不変なので合意済みです。 - 動的テーブル: コネクションごとに構築される可変表。エンコーダがこのコネクションで実際に送ったヘッダを蓄積し、次回以降は番号参照で送れるようにします。
インデックスは静的テーブルが 1 から始まり、その続き番号で動的テーブルが並びます。あるヘッダフィールドが既にテーブルにあれば、エンコーダはそのインデックス1個を送るだけで名前と値の両方を表現できます。
| 表現形式 | 送る内容 | 動的テーブルへの挿入 |
|---|---|---|
| Indexed Header Field | テーブルの番号1個(名前+値) | しない |
| Literal w/ Incremental Indexing | 名前(番号 or 文字列)+値の文字列 | する |
| Literal without Indexing | 名前+値の文字列 | しない |
| Literal Never Indexed | 名前+値の文字列(中継も非索引) | しない |
動的テーブルの増分更新
動的テーブルは FIFO(先入れ先出し)で振る舞います。新しいエントリは先頭(インデックスの若い側)に挿入され、容量(SETTINGS_HEADER_TABLE_SIZE でデコーダが上限を通知)を超えると末尾の古いエントリから退避します。各エントリのサイズは「名前のバイト数+値のバイト数+32」のオーバーヘッド込みで計算されます。
重要なのは、テーブルがヘッダブロックの処理そのものによって暗黙に更新される点です。エンコーダが「Incremental Indexing 付きリテラル」を送ると、デコーダはそのフィールドを復号すると同時に自分の動的テーブルにも同じエントリを挿入します。エンコーダとデコーダは同じ命令列を同じ順で適用することでテーブルを同期させます。
エンコーダ側テーブル デコーダ側テーブル
[送信] cookie: ab... ──────▶ 受信して復号
↓ 自分のテーブル先頭に挿入 ↓ 同じく先頭に挿入
[62] cookie: ab... [62] cookie: ab... ← 同期が取れる
次回は「インデックス62」だけ送れば cookie 全体を表現できる
HPACK の同期は「ヘッダと同じ経路をテーブル更新が厳密な順序で流れる」前提に立ちます。HTTP/2 は単一 TCP 上で全フレームが順序到着するため成立しますが、QUIC のストリーム独立配送ではこの前提が壊れます。だから HTTP/3 は HPACK を使えず QPACK へ再設計されました。詳細は /network/qpack-header-compression/ にあります。
Huffman 符号化:文字列リテラルを縮める層
テーブルに無いヘッダはリテラル(生の文字列)として送る必要があります。HPACK はこのとき静的 Huffman 符号を任意で適用できます。RFC 7541 付録は、HTTP ヘッダに現れる文字の出現頻度を大量のトラフィックから集計して作った固定の符号表を1つ定義しています。動的に頻度を測って表を作り直す適応 Huffman ではなく、表が固定なので符号表の送受信が不要で、状態を持たず復号できます。
仕組みは Huffman 符号の原則どおりで、頻出文字(/、:、英小文字など)に短いビット列を、稀な文字に長いビット列を割り当てます。各リテラルの先頭には Huffman 適用の有無を示す H ビットがあり、エンコーダは符号化後が短くなる場合だけ適用します。末尾はオクテット境界に合うよう、対応する符号の最長プレフィックスである全 1 ビット(EOS 符号の接頭)でパディングします。逆に、復号側が末尾以外で全 1 のパディング列に出会ったり、明示の EOS 符号を受け取ったりした場合はエラー扱いです。
HPACK の圧縮は独立した2層です。第1層は「テーブルによる重複排除」(同じヘッダを番号1個に畳む)、第2層は「Huffman による文字列短縮」(リテラルの各文字を短いビットに)。前者がヘッダ間の冗長性を、後者が文字列内の冗長性を削ります。両者は直交し、リテラルを動的テーブルに載せつつ Huffman で縮める、といった併用が普通です。
CRIME を避ける設計:Never Indexed
ヘッダ圧縮にはセキュリティ上の落とし穴があります。HTTP/1.1 時代に TLS や SPDY が試みたヘッダ全体の gzip 圧縮は、CRIME 攻撃で破られました。原理はこうです。攻撃者がリクエストの一部(パスやヘッダ値)を少しずつ操作でき、かつ圧縮後の長さを観測できるとき、推測した文字列が秘密値(例:セッション Cookie)と一致すると重複として圧縮され、暗号文が短くなる。この長さの差を手掛かりに、秘密値を1文字ずつ総当たりで復元できてしまいます。
HPACK はこの教訓を設計に織り込んでいます。
- そもそも辞書共有が文字列単位ではなくヘッダフィールド単位である。CRIME は同一圧縮文脈内で「攻撃者制御文字列」と「秘密文字列」が部分一致すると縮む点を突きますが、HPACK の動的テーブルは部分文字列ではなく完全なフィールド(名前+値)単位でしか参照を共有しません。攻撃者が値を1文字ずつ変えても、フィールド全体が一致しない限りインデックス参照は成立せず、長さの漏れが起きにくくなります。
- Literal Never Indexed 表現を用意し、秘密性の高いヘッダを動的テーブルへ載せないよう明示できる。この形式は当該フィールドを動的テーブルに挿入しないだけでなく、経路上の中継(プロキシ)にも再索引化を禁じることを伝えます。これにより、機微なヘッダを圧縮の共有文脈から外し、長さベースのサイドチャネルを断てます。
HEIST は被害者のブラウザ上で発生させたクロスオリジン応答のサイズを TCP/TLS の挙動から推定し、BREACH 系の圧縮サイドチャネルをネットワーク盗聴なしに成立させる手法です。HPACK 自体はリクエストヘッダの圧縮であり、機微値を Never Indexed にすればヘッダ経由の漏えいは抑えられます。ただし応答本文の圧縮(gzip など)に秘密と攻撃者制御値が混在する状況は HPACK の管轄外で、依然リスクが残る点に注意が必要です。
圧縮率を引き出す実装上の判断
エンコーダには裁量があります。動的テーブルを積極利用すれば圧縮率は上がりますが、テーブル容量は有限なので、何を載せ何を退避させるかが効きます。実務的な指針は次のとおりです。
- 値が変動しない定番ヘッダ(
:method、accept-encodingなど)は静的テーブルで番号参照に畳む。 cookieのように長くリクエスト間で安定するヘッダは動的テーブルに載せて2回目以降を番号1個にする。- 認証トークンなど秘密性が高く一度しか使わない値は Never Indexed にして圧縮文脈から隔離する。
- リテラルは符号化後が縮む場合のみ Huffman を適用する(短い ASCII 値などは適用しない方が短いことがある)。
「HPACK の二層構造」は、テーブル参照(重複排除)と Huffman(文字列短縮)の独立した2段。「CRIME 対策」は、フィールド単位の辞書共有と Literal Never Indexed による秘密値の圧縮文脈からの隔離。「HTTP/3 で使えない理由」は、動的テーブルが更新順序に依存し QUIC のストリーム独立配送で同期が壊れるから。この3点が頻出です。
まとめ
HPACK は、テーブルによる重複排除と Huffman による文字列短縮という直交した2層で HTTP/2 のヘッダを縮めます。静的テーブルは合意済みの定番を、動的テーブルはコネクション固有の繰り返しを番号参照に畳み、FIFO の増分更新でエンコーダとデコーダが命令列を同順に適用して同期します。同時に、TLS や SPDY のヘッダ圧縮が招いた CRIME の教訓からフィールド単位の辞書共有と Literal Never Indexed を備え、秘密値を圧縮サイドチャネルから守れる設計になっています。この順序依存の同期モデルこそが HTTP/3 で QPACK へ作り替えられた理由であり、/network/qpack-header-compression/ と /network/http-versions/ を合わせて読むと、ヘッダ圧縮の進化が一本の線でつながります。
ネットワーク Article
HPACK:HTTP/2 ヘッダ圧縮と Huffman 符号化を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
HPACK
比較で見る軸
難易度: advanced / カテゴリ: ネットワーク / タグ数: 5
導入後に効く点
動的テーブルは先頭挿入・末尾退避の FIFO で増分更新され、エンコーダとデコーダが同じ命令列を同順で適用することで同期する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- ネットワーク
- タグ数
- 5
判断チェックリスト
- 自社の用途が「HPACK / HTTP/2」に近いか確認する。
- 強みである「HPACK は61件の静的テーブルとコネクション単位の動的テーブルを用い、過去に送ったヘッダを文字列ではなくインデックス番号で参照して冗長な再送を消す。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。