HTTPヘッダー圧縮

HPACK (HTTP)

[129] HPACK は、HTTP/2ヘッダーで採用されている圧縮形式です。 RFC 7541 で定義されています。

仕様書

ヘッダーブロック

[5] ヘッダーリスト (header list) は、 ヘッダー欄の順序付きリストです。 >>1

[28] 重複するヘッダー欄を含むことができます。 HTTP/2 header block に含まれるヘッダー欄の完全なリストは、 ヘッダーリストです。 >>1

[7] ヘッダーブロック (header block) は、 ヘッダー欄表現を連結したもので、復号すると完全なヘッダーリストが得られるものです。 >>1

[37] ヘッダーブロックの先頭には動的表サイズ更新があるかもしれません。

  1. ?
    1. 動的表サイズ更新
    2. ?
      1. 動的表サイズ更新
  2. *
    1. ヘッダー欄表現

[11] 符号化器 (encoder) は、入力のヘッダーリストを符号化してヘッダーブロックとして出力するものです。 フレームに含めるデータの生成に用います。 符号化器は、次のようにします。

  1. 必要があれば、
    1. 1個または2個の動的表サイズ更新を出力します。
  2. ヘッダーリスト内のヘッダー欄を順に >>1
    1. 符号化してヘッダー欄表現を得ます。
    2. 得られたヘッダー欄表現を出力します。

[49] 復号器 (decoder) は、入力のヘッダーブロックを復号してヘッダーリストとして出力するものです。 復号エラーを返すこともあります。 フレームに含まれるデータの解釈に用います。 復号器は、次のようにします。

  1. 空のヘッダーリストを用意します。
  2. 入力の先頭から順に >>1
    1. 動的表サイズ更新なら、
      1. 適切に処理します。
      2. 復号エラーなら、停止します。
    2. ヘッダー欄表現なら、
      1. 復号してヘッダー欄を得ます。
      2. 復号エラーなら、停止します。
      3. ヘッダーリストの末尾にヘッダー欄を追加します。
  3. ヘッダーリストを返します。

[113] なおヘッダーブロックの各内容は、先頭ビットにより次のように判断できます。

  • 1 なら、索引表現 (>>19)
  • 01 なら、索引付けするリテラル表現 (>>26)
  • 001 なら、動的表サイズ更新 (>>40)
  • 0001 なら、決して索引付けしないリテラル表現 (>>24)
  • 0000 なら、索引付けしないリテラル表現 (>>23)

[110] HTTPヘッダーの順序は、意味があります。HPACK では同名のヘッダーも含めて順序を保持しなければなりません >>1

ヘッダー欄

[2] ヘッダー欄 (header field) は、名前と値の組です。 いずれも不透明なオクテット列として扱います。 >>1

[8] HTTP/2 では任意のオクテット列が認められているわけではありませんが、 HPACK では緩めになっています。不正なオクテット列なら、 HTTP/2 としての処理でエラー (奇形) となります。
[27] HTTPヘッダーだけでなく、疑似ヘッダーも含まれます。 HTTP/2 では疑似ヘッダーが他の HTTPヘッダーの前に来なければならないことになっていますが、 HPACK としては制約はありません。

[6] ヘッダー欄表現 (header field representation) は、 ヘッダー欄を索引表現またはリテラル表現によって符号化したものです。 >>1

  1. |
    1. 索引表現
    2. リテラル表現 (索引付けする)
    3. リテラル表現 (索引付けしない)
    4. リテラル表現 (決して索引付けしない)

[9] 符号化器は、ヘッダー欄をいずれかの方法でヘッダー欄表現として符号化しなければなりません。 その選択の方法は実装に任されていますが、ヘッダー名や値の性質によって、 セキュリティー上問題の無いように決定する必要があります。

>>85 参照。

[125] 索引付けを行うリテラル表現により符号化する場合は、符号化に加えて、 必要なら eviction を行ってから >>1ヘッダー欄動的表の先頭に追加 (できれば) します >>1

[29] 復号器は、ヘッダー欄表現を次のようにしなければなりません

  1. [30] 索引表現なら、
    1. 復号エラーが検出されたら、停止します。
    2. そうでなければ、索引で参照されている項目のヘッダー欄を返します >>1
  2. [31] リテラル表現なら、
    1. 復号エラーが検出されたら、停止します。
    2. [32] そうでなければ、
      1. リテラル表現からヘッダー欄を得ます >>1
      2. [33] 索引付けを行うリテラル表現なら、
        1. 必要なら、 eviction を行います >>1
        2. 得られたヘッダー欄動的表の先頭に追加 (できれば) します >>1
      3. 得られたヘッダー欄を返します。

[114] 中間器の実装に用いる符号化器復号器は、 通常の名前と値の組に加えて、決して索引付けしないリテラル表現であることを保存しなければなりません (>>86)。

[115] 中間器は、 HPACK接続動的表への依存性のため、 すべてのヘッダーを無変更で転送する場合であっても必ずヘッダーブロックを一旦復号してから改めて符号化しなければなりません。

索引表現

[19] 索引表現 (indexed representation) は、静的表または動的表上の項目を参照する形でヘッダー欄を表しています。 >>1

[64] 索引表現は、最初の1ビットが 1 でその後の7ビットを接頭辞とする整数 (>>50) で構成され、この整数索引 (>>17) を表します。 >>1

[111] 索引 (>>17) / 整数 (>>50) は、復号エラーとなることがあります。
width
8
  1. 1 1
  2. 7 索引

リテラル表現

[20] リテラル表現 (literal representation) は、 名前と値の組によりヘッダー欄を表します。 名前は、文字列リテラルまたは索引により記述します。 値は、文字列リテラルにより記述します。 >>1

[21] 次の3通りがあります >>1

[26] 索引付けを行うリテラル表現では、次のようにします >>1

  1. [69] 最初の2ビットは 01 とします。
  2. [66] ヘッダー欄の名前を索引によって表現するなら、
    1. [70] 次の6ビットを接頭辞とする整数によってその索引を示します。
  3. [67] ヘッダー欄の名前を文字列リテラルによって表現するなら、
    1. [72] 次の6ビットをすべて 0 とし、その後に名前を文字列リテラルとして指定します。
  4. [68] その後にヘッダー欄の値を文字列リテラルとして指定します。

width
16:
  1. 1 0
  2. 1 1
  3. 6 索引
  4. 24... 値
width
16:
  1. 1 0
  2. 1 1
  3. 6 0
  4. 8 名前
  5. 16... 値

[71] 索引付けを行わないリテラル表現では、次のようにします >>1

  1. [74] 最初の3ビットは 000 とします。
  2. [87] 次の1ビットは転送時に索引付けしても構わないなら 0、 絶対に索引付けしてはならないなら 1 とします。
  3. [75] ヘッダー欄の名前を索引によって表現するなら、
    1. [76] 次の4ビットを接頭辞とする整数によってその索引を示します。
  4. [77] ヘッダー欄の名前を文字列リテラルによって表現するなら、
    1. [78] 次の4ビットをすべて 0 とし、その後に名前を文字列リテラルとして指定します。
  5. [79] その後にヘッダー欄の値を文字列リテラルとして指定します。

width
16:
  1. 1 0
  2. 1 0
  3. 1 0
  4. 1 絶対
  5. 4 索引
  6. 24... 値
width
16:
  1. 1 0
  2. 1 0
  3. 1 0
  4. 1 絶対
  5. 4 0
  6. 8 名前
  7. 16... 値
[116] 索引 (>>17) / 文字列リテラル (>>56) は、復号エラーとなることがあります。

[86] 中間器は、転送時に絶対に索引付けしない (0001) 表現で符号化されたヘッダー欄は、 絶対に索引付けしない表現のまま符号化しなければなりません >>1

[25] これは圧縮することで危険になるヘッダー欄を保護するためのものです (>>97)。 >>1
[131] ヘッダー名を索引により表現するかリテラルにより表現するかを変えて良いのかは定かではありません。

表現方法の選択

[148] どの表現方法を選択するかは実装に委ねられています。

[149] Firefox は、文字列を表す際に、常にハフマン符号化するように見えます。

[151] Chrome は、文字列を表す際に、短ければそのまま、長ければハフマン符号化するように見えます。

[150] Firefox は、 cookie ヘッダーを送るときに値が短ければ決して索引付けしないとし、 長ければ索引付けするように見えます。短いかどうかをどう判断しているのかはよくわかりません。 :path ヘッダーは索引付けしないで送信するようです。 その他常に送信するヘッダーは、索引付けするようです。

[152] Chrome は、 :path は索引付けしないとし、 それ以外で常に送信するヘッダーは索引付けするように見えます。

[153] Firefoxauthorization ヘッダーを決して索引付けしないとするようです。 Chrome は索引付けするようです。

[118] 符号化器復号器は、表を持ちます。

[119] (table) は、項目の順序を持った列です。項目 (entry) は、 索引によって識別され、ヘッダー欄の名前と値を表しています。

[120] 表は、静的表動的表で構成されます。

[4] 静的表 (static table) >>103 は、 頻出ヘッダー欄を集めたものです。静的表は読み取り専用で、 あらゆる場面で共通であり、いつでも使うことができます。 >>1

[135] 静的表は、主要 Webサイトの用例と HTTP/2疑似ヘッダーから作成されました >>1

[3] 動的表 (dynamic table) は、 特定の接続で重出する (かもしれない) ヘッダー欄を追加するものです。 初期状態は空ですが、符号化復号により追加 (や削除) されてゆきます。 >>1

[15] 動的表は、重複する (同名同値の) 項目を含むことがあります。 復号器はこれをエラーとしてはなりません>>1

[40] 動的表は、最大サイズを持ちます (>>10)。

[14] 動的表は、接続ごとに異なるものを使用します >>73

[122] 動的表は、双方向通信の符号化 (送信/圧縮) 用と復号 (受信/展開) 用で異なるものを使用します >>1, >>73

[123] RFC 7541 は、それぞれ符号化文脈 (encoding context) /復号文脈 (decoding context) >>1 と言っています。 RFC 7540 は(おそらく)同じものを圧縮文脈 (compression context) /展開文脈 (decompression context) >>73 と呼んでいます。
[12] あるエンドポイントの符号化用の動的表と、 peer の復号用の動的表が (遅延を別にすれば) 同じとなります。 逆も同様です。

[128] 実装によっては、より多くの情報を動的表の項目に付加し、符号化時に参照するかもしれません (>>93)。

索引

[17] 表中の項目は、索引 (index) により参照されます。

[13] 動的表静的表は、どちらも共通の索引番地空間 (index address space) 上にあります。 1 以上静的表の大きさ以下索引は、 静的表上の項目を表しています。それよりも大きな索引は、 動的表上の項目を表しています。 >>1

[121] 動的表は、 FIFO であり、最初・最新の項目ほど小さな索引を持ち、 最古の項目ほど大きな索引を持ちます。 >>1

[126] つまり追加により動的表の項目の索引は変わっていきます。
[157] 索引付けする項目は索引への参照を含んでいることがあります。 参照を解決してから索引付けする必要があります。

[18]の長さのよりも大きな索引は、 復号エラーとしなければなりません >>1

[65] 索引表現における値 0 は、復号エラーとしなければなりません >>1

[109] リテラル表現では、索引の値 0 は索引を持たない場合のために使われます。 索引として 0 が用いられることはありません。

[145] HTTP/2 における動的表の理論上最大サイズは 232-1 です (>>144)。 ヘッダーは名前も値も空の時 (実際には HTTP/2 では不可) 最小サイズ32 (>>35) なので、動的表の最大項目数は 227。これに静的表の個数を加えたのが索引の最大値です。 従って24ビット整数では表せませんが、32ビット整数なら十分です。

サイズ制限

[34] 動的表サイズ (size) は、各項目のサイズのです。 >>1

[35] 項目のサイズ (size) は、名前のオクテット長と値のオクテット長と 32 のです。 >>1

[36] Huffman符号化は適用しない状態の長さです。 >>1

[10] 動的表には、メモリー消費の制約ということで最大サイズが設けられています。 符号化器は、動的表サイズを最大サイズ以下としなければなりません >>1復号器は、動的表サイズを最大サイズより大きくすることはありません。

動的表サイズ更新

[16] 最大サイズに達すると削除 (eviction>>44) が発生するため、 古い項目は符号化復号に使えなくなります。そのため、 符号化器が最大サイズを変更したら復号器に通知する必要があります。 動的表の最大サイズの変更は、動的表サイズ更新 (dynamic table size update) により通知されます。 動的表サイズ更新は、変更の後の最初のヘッダーブロックの最初になければなりません>>1

[38] 最初以外に動的表サイズ更新が出現したらどうするべきなのかは不明です。 復号エラーでしょうか。

[41] HTTP/2 では、変更の後とは設定acknowledgment の後に当たります。 >>1

[42] 2つのヘッダーブロックの転送の間に最大サイズの変更が複数回行われた時は、 最小サイズを動的表サイズ更新で通知しなければなりません。 更に最後のサイズも動的表サイズ更新で通知しなければなりません。 動的表サイズ更新は、この高々2つとなります。 >>1

[39] 何度変更されたとしても、最小と最後の変更 (と eviction) さえ実行すれば、 両者の動的表と最大サイズは同じ状態になります。
[127] 2個以上受信した時どう処理するべきかは不明です。

[147] FirefoxChrome も、動的表サイズ更新がどの位置に何個あってもエラーとはしないようです。

[43] 0 を設定し、元に戻すことで、動的表の内容を消去できます。 >>1

[80] 動的表サイズ更新は、先頭の3ビットが 001 で、 その後5ビットを接頭辞とする整数によって新しい最大サイズを表します >>1

width
16
  1. 1 0
  2. 1 0
  3. 0 1
  4. 5 最大サイズ

設定 SETTINGS_HEADER_TABLE_SIZE

[81] 動的表の新しい最大サイズは、プロトコルによる上限以下でなければなりません >>1

[82] HTTP/2 では、復号側から受信し、符号化側が acknowledge した最新の設定 SETTINGS_HEADER_TABLE_SIZE の値がこの上限です。 >>1

[83] 上限を超える値は、復号エラーとしなければなりません>>1

[124] 最大値の初期値はプロトコルの上限と思われます。

[132] HTTP/2設定 SETTINGS_HEADER_TABLE_SIZE (0x1) は、受信者側がヘッダーブロック復号に使う表の最大サイズをバイト単位で送信者側に通知するものです。 >>84

[134] 符号化器は、本設定で指定された値以下のサイズの表を使うことができます。 >>84

[133]設定の初期値は、 4096 バイトです。 >>84

[144] HPACK索引HPACK 整数なので、任意の大きさの値を使えます。 つまり HPACK として動的表のサイズはいくらでも大きくできます。 HTTP/2SETTINGS フレームにより指定できる値の最大値は 232-1 で、本設定固有の上限は設けられていないようです。

eviction

[44] 最大サイズが減少した時は、動的表のサイズが最大サイズ以下となるまで、 末尾から順に削除 (eviction) します。 >>1

[45] 新しい項目を追加する前には、動的表に新しい項目のサイズを足しても最大サイズ以下となるか、 が空となるまで、末尾から順に削除 (eviction) します。 >>1

[46] 新しい項目が最大サイズ以下なら、動的表に追加します。 >>1

[47] 最大サイズを超えるなら、動的表が空になります。これはエラーではありません。 >>1

[48] なお、新しい項目は、それによって動的表から削除される項目の名前を参照していることがありますから、 注意が必要です。 >>1

文字列リテラル (オクテット列)

[56] オクテット列 (string of octets) は、ヘッダー欄の名前や値に使います。 >>1

[112] RFC では「literal」、「string literal」、「string of octets」 など表記が一定しませんが、いずれにせよ、バイト列を表すものです。

[57] 文字列リテラルは、オクテット列として直接、 またはHuffman符号により符号化します。 >>1

[58] 文字列リテラルは、次の欄で構成されます。 >>1

[59] H
Huffman符号化されているかどうかを示す1ビットのフラグです。 0 は、Huffman符号化されていない生のオクテット列を表します。 1 は、Huffman符号化されたオクテット列を表します。 >>1
[60] 文字列長 (String Length)
文字列リテラルを符号化するために使ったオクテット数を7ビット接頭辞整数 (>>50) として符号化したものです。 >>1
[61] 文字列データ (String Data)
文字列リテラルの符号化されたデータです。生のオクテット列または Huffman符号化されたオクテット列です。 >>1

width
16
  1. 1 H
  2. 7 文字列長
  3. 24... 文字列データ
[117] 整数 (>>50) は、復号エラーとなることがあります。

[146] 理論上は整数が任意の長さを扱えますから、文字列も任意の長さとできます。 実際にはメモリー容量や実装の都合などの制約を受けます。例えば 232-1 や 231-1 が文字列長の上限かもしれません。

ハフマン符号化

[62] Huffman符号化の場合、 RFC 7541 の表 >>105 によるHuffman符号によって符号化し、 各オクテットに対応する符号をビット単位で順に連結したものです。 >>1

[108] この符号化の表 >>105 は、0-255 のオクテットと 256 = EOS (end-of-string) の257種類の記号 (symbol) に対して、5-30ビットの符号を割り当てるものです。 >>1

[106] この符号化の表は、大量の HTTPヘッダーの標本から統計的に得られたものです。 これは正準Huffman符号 (canonical Huffman code) でどの記号も異なる長さを持つようにしたものです。 >>1

[107] オクテット境界でちょうど終わらない場合、次のオクテット境界まで詰め (padding) を挿入します。 文字列リテラルの一部と誤解されないよう、 EOS (end-of-string) 記号に対応する符号の最上位側数ビットを使います。 >>1

[63] Huffman符号化復号時には、末尾の不完全な符号は、 詰めとみなして捨てます。8ビット以上の詰めは、復号エラーとしなければなりませんEOS 記号に対応する符号の最上位側数ビットでない詰めも、 復号エラーとしなければなりませんEOS 記号を含む文字列リテラルは、復号エラーとしなければなりません>>1

[102] 静的Huffman符号の符号化表を使うことで情報漏洩が生じるものの、 攻撃者がこの情報漏洩から意味のある情報を得ることはできないとの研究があります。 その他現時点で静的Huffman符号への攻撃は知られていません。 >>1

整数

[50] 整数 (integer) は、索引と文字列長に使います。 >>1

[51] 整数は、オクテット中のどこからでもはじめられますが、 必ずオクテットの最後で終わります。 >>1

[52] 整数は、開始オクテットのうちの開始以後の部分である接頭辞 (prefix) と、 その後の0個以上オクテットの列で構成されます。 接頭辞のビット数 N は、文脈によります。 接頭辞の後に続くオクテットは、 MSB が1のものが続いた後、 MSB が0のもので終わります。 >>1

  1. |
    1. 接頭辞 (0 を含む)
    2. =
      1. 接頭辞 (すべて1)
      2. *
        1. MSB=1
      3. MSB=0

[55] 整数符号化は、次のように行います >>1

  1. 値が 2N-1 未満なら、
    1. N ビットで表現したものを、接頭辞として出力します。
  2. それ以外なら、
    1. N 個の 1 のビットの列を、接頭辞として出力します。
    2. 値から 2N-1 を引きます。
    3. 値が 128 以上である間、繰り返し実行します。
      1. 値を128で割った余りを下位7ビットで符号化し、 MSB を1としたオクテットを出力します。
      2. 値を128で割ります。
    4. 値を符号化したオクテットを出力します。

[53] 整数復号は、次のように行います >>1

  1. 結果を、接頭辞 N ビットを復号した値に設定します。
  2. 結果が 2N-1 なら、
    1. M を、0に設定します。
    2. 次の処理を実行します。
      1. B を、次のオクテットに設定します。
      2. 結果に、 B の下位7ビットの値に 2M を掛けた値を足します。
      3. M に、7 を足します。
      4. BMSB が 1 なら、繰り返します。そうでなければ、次に進みます。
  3. 結果を返します。

[54] 本方式の整数は無限に大きな値を表現できますし、 無駄に多くのオクテットを消費したり、桁溢れさせたりするかもしれません。 実装は (使われる文脈毎に適当な) 値やオクテット長の制限を超えたら、 復号エラーとしなければなりません>>1

復号エラー

[130] HTTP/2 ヘッダーブロック復号エラーは、接続エラー COMPRESSION_ERROR としなければなりません >>73

[91] 誤り符号 COMPRESSION_ERROR (0x9) は、接続ヘッダー圧縮文脈を維持できないことを示します >>90

[103] ヘッダー圧縮は、無駄な処理をさせるために濫用できます。 エンドポイントは利用状況を監視して制限するべきです接続エラー ENHANCE_YOUR_CALM としても構いません。 >>104

[142] Chrome は仕様通りのようです。 Firefoxストリームエラー PROTOCOL_ERROR とするようです。

[143] ChromeFirefox も、 HEADERS の復号エラーを検出するより前に次の CONTINUATION のエラーを検査するように見えます。

セキュリティー

[85] CRIME攻撃のように (暗号化されていても) 長さから内容を推定することを難しくするため、 HPACK では個々の文字レベルではなく、文字列全体を表に入れて参照する形を採っています。 しかしこれで完全に攻撃を防げるわけではありませんから、注意が必要なことには変わりありません。 >>1

[88] 機密データと攻撃者が何らかの形で注入可能なデータが同じ表に基づき圧縮されると、 攻撃が可能となります。同一の要求応答に含まれるヘッダーの場合はもちろん、 HTTP/2 では同一の接続で同じ表を使いますから、 同じ接続を共有する他の要求応答でさえあれば、 攻撃できてしまうかもしれません。

[89] 例えば、次のような状況が存在し得ます。

[93] 複数の出自を持つヘッダーを圧縮する場合は、動的表の項目を出自ごとに分け、 他の項目は使わないようにするのが理想的です。 >>1

[94] 圧縮性能の向上のため、一部は共通にしても構いません >>1

[95] 例えば WebブラウザーAccept-Encoding の値を共通にできます >>1WebブラウザーAccept-Encoding ヘッダーに指定する値はどの起源でも同じでしょうから、 おそらく問題とならないはずです。

[96] 同じヘッダーに多数の異なる値を指定して推定を試みているらしき状況なら、 それ以後そのヘッダー動的表を用いないようにしても良さそうです。 (ただ単に動的表から削除するだけでは、別の方法で簡単に再追加されてしまうかもしれませんから、 不十分です。) 値が短い場合ほど、すぐに動的表を使わないようにするべきかもしれません。 >>1

[97] また、繊細なヘッダーは決して索引付けしないことにする (常に文字列リテラルで表現する) ことにもできます。 >>1

  • [98] 短い値は攻撃に弱いので、索引付けしないことにするといいかもしれません。
  • [99] CookieAuthorization のように繊細な情報を含むヘッダーは、索引付けしないことにするといいかもしれません。

[100] 逆に、分かったとしてもあまり価値が無いヘッダーは、索引付けした方がいいかもしれません。 >>1

[101] 例えば User-Agent はどのサーバーにも同じ値で送信するのが普通なので、 その値を特定しようとする企てにはさして意味がありませんから、索引付けしても良さそうです。>>1

[112] HTTP/2 ヘッダーリストの機密データと攻撃者が制御できるデータを (同じ圧縮辞書で) 一緒に圧縮してはなりません。データの出所が不明なら、 圧縮してはなりません>>111

歴史

[154] 1025071 – Don't index :path field in hpack ( 版) <https://bugzilla.mozilla.org/show_bug.cgi?id=1025071>

[155] Issue 524041 - chromium - Reject line-folding in HPACK - An open-source project to help move the web forward. - Google Project Hosting ( 版) <https://code.google.com/p/chromium/issues/detail?id=524041>

HTTP/2