「Advisory Content-Length for HTTP」と自転車置場の話し

プロトコルの標準化の場では、自転車置き場(Bikeshed)の話がたまに出てきます。そのやり取りが面白かったので、今回新しく出た提案仕様自体の解説にあわせて紹介します。

Advisory Content-Length for HTTP

IETFでHTTP WGの議長も務める Mark Nottingham氏から「Advisory Content-Length for HTTP」という提案仕様が提出されています。

ここで背景としている問題は次のとおりである。HTTPにおけるContent-Lengthヘッダーは、2つの意味で使用されています。1つめは、HTTP/1.1におけるメッセージの終端を示すため。2つめは、メッセージボディ(内容)の長さを示すために使用されます。

HTTP/1.1に置いてメッセージの終端を示すのに使用されていますが、Transfer-Encodingヘッダと合わせて使用しプロキシとバックエンドのサーバで終端の解釈を異なるようにする攻撃があったりします (HTTP Request Smuggling攻撃)。

HTTP/2では、DATAフレーム長を持ってしてメッセージの終端を示すため、Content-Lengthは終端のためには使用されません。ただし、HTTPボディの中身の長さはContent-Lengthと一致していることが要求されます。

このように、プロトコルバージョンや場所によって使い方や解釈が異なる上に、実装にも揺れがあったりします。

そこで、メッセージの終端のためではなく、コンテンツの長さを示す新しいHTTPヘッダを定義しようというのがこの提案仕様です。

初版となる現在のdraft 00では、Bikeshed-Length という名前で仮の名前で定義しています。(ヘッダ値はStructured Field Values for HTTPの定義に則る)

Bikeshed-Length = sh-item

ここで、Bikeshedという単語が出てきました。

自転車置き場(Bikeshed)

自転車置き場(Bikeshed)の解説はこちらのサイトが詳しいです。
0xcc.net

かいつまんで簡単に説明すると、誰しもが口を出しやすい議題、つまり自転車置き場の色をどうするかといった議論です。このような議論は、難しい議論や、重要な議論よりも紛糾してしまうという話です。

そのような紛糾を避けるために、「これはbikeshedの議論だね」とそのような紛糾を諌め、本質的な議論に目を目けるという事がよくあります。

プロトコルの標準化では、やはりパラメータ名・ヘッダ名がこれらに該当しがちです。そのため、提案者であるMark Nottingham氏は、ヘッダ名の議論は置いておいて、この提案の内容の話に集中するためにBikeshed-Lengthと記述したのでしょう。

反応

このBikeshed-Lengthに対して他の人の反応も面白かったので紹介します。

QUIC WGの議長を務めるLucas Pardue氏は次のようにツイートしています
「自転車置き場の色を考慮する必要があるだけでなく、長さにわたって熟考しています」(Google翻訳, tweet url)


また、この自転車置き場(Bikeshed)というフレーズを最初に使ったPoul-Henning Kamp氏もコメントしています
「私は提案されたヘッダー名は完全に素晴らしいと思うし、それを変更する理由はないと思う:-)」(Google翻訳, 投稿URL)

WebTransport over HTTP/2 の仕様について

目次

背景と仕様

以前取り上げたように、WebSocketに変わるWebTransportという新しい双方向メッセージングプロトコルの標準化および実装が進められている。
asnokaze.hatenablog.com

JavaScript APIに関してはW3C側で(仕様リンク)で策定されているが、プロトコルIETFで策定が進められている。

使用するトランスポートプロトコルによって仕様が分かれており、以下の3つが存在する。

このうち、QUICやHTTP/3が使用できない場合のフォールバック先として用意される「WebTransport over HTTP/2」の仕様が先日投稿された。

提出された「WebTransport using HTTP/2」の著者はFacebookAlan Frindell氏の他、GoogleのVictor Vasiliev氏、AppleのTommy Pauly氏らが名を連ねている。

彼らは元々 HTTP/2上で任意の双方向メッセージを行うための提案仕様を書いていたので、WebTransport over HTTP/2へと向かうのも頷ける。

WebTransport over HTTP/2 (Http2Transport)

特徴

現在の仕様では、WebTransport over HTTP/2は以下の機能を持つと述べています

  • HTTP/2上でWebTransportセッションが確立したあと、クライアントとサーバのどちらからでもWebTransport用ストリームをオープンできる。
  • WebTransport用ストリームでヘッダー圧縮、優先順位付け、フロー制御 の機能が利用できる
  • 中継者がいてもWebTransport用ストリームのデータを正しい相手に配送できる
  • WebTransport用ストリームをグループ化できる

WebSocket over HTTP/2とは異なり、セッションが確立したあとにサーバ側からWebTransport用ストリームをオープンできる。そのデータをただしくクライアントに配送するための工夫が入っている。

ただ、WebTransportでは優先度処理がどうなるかはまだ変わるところがあると思われる(WICG/Web-Transport)

概要

WebTransport over HTTP/2の仕様では、以下について言及している

  • WebTransport over HTTP/2のネゴシエーション方法 (SETTINGSパラメータ)
  • WebTransport Connect StreamsとWebTransport Streams
  • WTHEADERS拡張フレーム

それぞれ説明していく。最後に通信例を示す。

WebTransport over HTTP/2のネゴシエーション

WebTransport over HTTP/2を使うために、以下の2つのSETTINGSパラメータを投げます。

  • SETTINGS_ENABLE_CONNECT_PROTOCOL
  • SETTINGS_ENABLE_WEBTRANSPORT

それぞれ 1 の場合に、有効であることを示します。クライアントからSETTINGSフレームを送信し、サーバからも同様に有効であることを示すSETTINGSパラメータを受け取った場合のみWebTransport over HTTP/2の機能が使用できます。

WebTransport Connect StreamsとWebTransport Streams

WebTransport over HTTP/2では2種類のストリームを使用します。それぞれストリームですので、ストリームIDを持ちます。

  • WebTransport Connect Streams: クライアントからCONNECTメソッドのHEADERSフレームで開かれるストリーム。グループ化のために使用される
  • WebTransport Streams: WTHEADERSフレームを使用することでクライアント/サーバのどちらからでも開くことができるストリーム。実際にデータをやりとりするのに使用される。WebTransport Connect Streamsに関連付けられる。

実際にデータをやり取りするWebTransport Streamsは、必ずWebTransport Connect Streamsに関連付けられます。WebTransport Connect Streamsがクローズすると、関連するWebTransport Streamsもクローズされます。一つのWebTransport Connect Streamsに複数のWebTransport Streamsを関連付けることもできます。

中継者(Proxy)がいると、サーバ側からオープンしたWebTransport Streamsのデータを、どのクライアントに配送すべきか判断するためこのような仕組みになっています。WebTransport Streamsを、クライアントからオープンしたWebTransport Connect Streamsに必ず関連付けることで、その配送が分かります。

ちょっと仕組みが分かりづらいですが、後述の通信例を見るとわかりやすいかと思います。

WTHEADERSフレーム

WTHEADERSフレームは、先述のWebTransport Streamsをオープンするのに使用する拡張フレームです。クライアントおよびサーバのどちらからでも送信することができます。

HTTP/2のHEADERSフレームと似ていますが、WebTransport Connect Streamsと関連付けるために Connect Stream ID というフィールドを持ちます。その他は、HEADERSフレームと同様です。
f:id:ASnoKaze:20200311012400p:plain

ただし、優先度の依存は同じWebTransport Connect Streamsに関連付けられたストリーム間でしか考慮されません。

CONTINUATIONフレームが続く可能性があるのも同様です。

通信例

ネゴシエーション

f:id:ASnoKaze:20200311013359p:plain

まず、クライアントとサーバはSETTINGSフレームでお互いにWebTransport over HTTP/2が有効なことを確認します。

その後、クライアントからCONNECTメソッドのHEADERSフレームを送信します。このストリームがWebTransport Connect Streamsです。このとき、:protocol疑似ヘッダにwebtransportを指定します。

:status 200が返ってきたらってきたらWebTransport Connect Streamsの準備はOKです。

WebTransport Streams

WebTransport Connect Streamsの準備ができたら、WTHEADERSフレームを用いてデータ通信用のWebTransport Streamsを開きます。下記は、サーバ側からWebTransport Streamsを開く例です。CONNECT_STREAMとして、先に開いたWebTransport Connect StreamsのストリームIDを指定しています。

相手から同様にWTHEADERSフレームで:status 200が返ってきたら、DATAフレームによる双方向通信ができるようになります。

f:id:ASnoKaze:20200311014213p:plain

中間装置(Proxy)の例

中間装置(Proxy)がいる構成における、配送について簡単に補足します。
WebTransport Connect Streamsが赤、WebTransport Streams青です。

f:id:ASnoKaze:20200311014913p:plain

中間装置とサーバは一つのコネクションに集約することができますす。

先述の通り、WebTransport Streamsはサーバからオープンできます。中間装置(Proxy)は、サーバからオープンしたストリームのデータをどのクライアントに配送すべきかマッピングできる必要があります。

WebTransport Streamsは必ずWebTransport Connect Streamsと関連付けられているため、そのWebTransport Connect Streamsをオープンしたクライアントにデータを配送することができます。

その他

CORSのように、Originヘッダを使って正規のオリジンからの通信のみを許可するようですが、仕様中の例には含まれてない?

UUID version 6の提案仕様

[2021/04/28追記] 新しく提案仕様が出たので、新しく記事を書きました
asnokaze.hatenablog.com


先日IETFに提出された「UUID Format Update」という提案仕様の中でUUID version 6のフォーマットが言及されている。このDraftはUUIDを定義しているRFC4122を更新するものです。

ただ、まだIndividual Draftのdraft 00であり、この提案自体どうなるかは全くわからない点に注意が必要。

なお、UUIDv6自体はこの提案を行っているBrad G. Peabody氏が以前から取り組んでいた模様で、4年以上まえから案は存在していたようだ(URL)。またそのタイミングでサンプル実装も公開している。

UUIDv6

この提案仕様の中で、UUIDをデータベースのプライマリキーとして使用する際の課題をあげている。時系列順に並び替えるために他のデータが必要なことや、B-treeにデータを挿入する際に局所性がなくランダムな書き込みになってしまうことが挙げられている。
(UUIDv1はタイムスタンプが付くが、MACアドレスを使うので問題がある)

UUIDv6では、特殊な操作なく単純なソートで時系列順にでき、利用時にアクセスの局所性を持つ様になっている。(ULIDと似ている)

UUIDv6の構造

他のUUIDバージョンと同様128bitからなる。前半の64bit部分がタイムスタンプの機能、後半64bit部分がランダム性を持つことになる。

前半64bitの前方から

  • 48bit: タイムスタンプ
  • 4bit: バージョン(互換性を保つためこの位置となっている)
  • 12bit: タイムスタンプ続き

タイムスタンプ(60bit)は、RFC4122と同様「1582年10月15日(カトリック教会におけるグレゴリオ暦の実施日)0時0分からの経過時間を100ナノ秒単位で計測した数値。」(引用: wikipedia UUID)

後半64bit(既存のUUID RFC4122 と同様)

  • 16bit: clock seq
  • 48bit: node

となっている。ただし、この後半部分はアプリケーションやサーバ固有の識別子、乱数などで埋めることが許可されています。

Base64 Text / Base32 Text

(この記事では、Base64 TextとBase32 Textについての記述を追記予定です。)

終わりに

議論は古くからありますが、individual draftの初版が出たことで、それにたいするフィードバックが始まっています。
来月開催される、Dispatch WGで取り上げられるかもしれません。
https://mailarchive.ietf.org/arch/browse/ietf/?q=uuid

Retry-Afterのスコープを指定するRetry-Scopeヘッダ

一時的にサービスが使用できないことを示す503レスポンスを返す際に、Retry-Afterレスポンスヘッダを付けることで待ってほしい時間を伝えることができます。

Retry-Afterレスポンスヘッダは下記の用に秒で指定できます。

Retry-After: 120

しかし、この503ステータスやRetry-Afterヘッダは元々送ったリクエストに対してのものです。

サービスが一時的に使用できない場合は、その他のURLに対してもリクエストを送るのは待ってほしい場合が多いでしょう。そこで、Retry-Scopeヘッダという話が出てきました。

Retry-Scopeヘッダ

以前、「HTTP/1.1 (RFC 7230 〜 7235) の改訂作業がはじまる」で書いたように、HTTPセマンティクスの再改定作業が進められています。その中で、上記のようなRetry-Afterヘッダのスコープの議論がありました(Github Issue)。

この議論の後、「Retry-Scope header field」という提案仕様が書かれています(MLで共有されていますが、正式にinternet-draftとしては提出されていません)。

このRetry-Scopeヘッダでは、Retry-Afterヘッダを適応するスコープを指定します。503レスポンスヘッダを返したURL以外にも、このスコープへのリクエストを待ってもらうことができます。

Retry-Scopeは下記のように使用できます。

   Retry-Scope: /books
   Retry-Scope: https://api.example/

最後に

この議論はまだ始まったばかりですので、仕様として標準化されるかどうか、どのような形になるかはまだまだ分からないところが多いです。

RFC8701 TLS GREASEで予約される値

TLSの拡張性を維持するために、未知の拡張仕様を実装が正しく無視するように「Applying Generate Random Extensions And Sustain Extensibility (GREASE) to TLS Extensibility」という仕様が長らく議論されてきました。

2020年1月に無事RFC8701になったので、その話を簡単に紹介する。実際にいくつかの実装がすでに使っている。

詳細な背景については、大津先生の記事を参照ください
錆びついたTLSを滑らかに、GoogleによるGREASE試験 - ぼちぼち日記

なお、GREASEの仕組み自体はTLS以外にも他のプロトコルでも導入が検討されてたりします(HTTP2の例)。

GREASE

TLS1.3の標準化をすすめる中で新しいTLS拡張などを定義しました。

未知のTLS拡張は無視されなければなりませんが、正しく処理せず接続を拒否する実装などがありました。

そのため、未知のTLS拡張などを正しく無視するのを確認する用の番号群を定義するのがGREASEです。

RFC8701では ALPN, cipher suitesや、TLS拡張, namedGroups, 署名アルゴリズム, バージョン番号などにGREASE用の値が確保されます。

クライアントはそれらの値を含めてネゴシエーションしていいことになっています。ただし、その値をサーバが選んでしまった場合は切断しなければなりません。

一方、サーバはGREASE用の値を特別扱いせず未知の値として無視しなければなりません。

予約されてる値

cipher suitesおよびALPN識別子

  • {0x0A,0x0A}
  • {0x1A,0x1A}
  • {0x2A,0x2A}
  • {0x3A,0x3A}
  • {0x4A,0x4A}
  • {0x5A,0x5A}
  • {0x6A,0x6A}
  • {0x7A,0x7A}
  • {0x8A,0x8A}
  • {0x9A,0x9A}
  • {0xAA,0xAA}
  • {0xBA,0xBA}
  • {0xCA,0xCA}
  • {0xDA,0xDA}
  • {0xEA,0xEA}
  • {0xFA,0xFA}

拡張機能、namedGroups、署名アルゴリズム、およびバージョン

  • 0x0A0A
  • 0x1A1A
  • 0x2A2A
  • 0x3A3A
  • 0x4A4A
  • 0x5A5A
  • 0x6A6A
  • 0x7A7A
  • 0x8A8A
  • 0x9A9A
  • 0xAAAA
  • 0xBABA
  • 0xCACA
  • 0xDADA
  • 0xEAEA
  • 0xFAFA

PskKeyExchangeModes

  • 0x0B
  • 0x2A
  • 0x49
  • 0x68
  • 0x87
  • 0xA6
  • 0xC5
  • 0xE4

オリジン全体にポリシーを適応するOrigin PolicyをChromeで試す

目次

Chromeがオリジン全体にポリシーを適応する「Origin Policy」という仕様をサポートしてるようなので、簡単に試してみる。(現在は Chrome Canaryのみの模様)

おそらく仕様及び実装も変わりうるので注意が必要です(極力仕様の変更があったら、記事を更新するようにします)

Origin Policy

Content-Security-PolicyやFeature-Policyといった、ブラウザがページを表示する際に機能を制限したりセキュリティ上の制限を与える機能があります。これらの機能はレスポンスヘッダに該当のヘッダを付けることでポリシーを適応させることができます。

そのため、これらの機能をもれなく適応するにはレスポンスの都度きちんとこれらのヘッダーを付加しなければなりません。

また、CSPやFeature-Policyのヘッダは1キロバイトになるほど多いものもあります。HTTP/2やHTTP/3の場合はヘッダ圧縮がありますが動的ヘッダテーブルを圧迫してしまい、効率を悪くしてしまいます。

そこで、CSPやFeature-Policyのポリシーをオリジン全体に適応可能にするのがOrigin Policyです

有効にさせる方法

ブラウザにOrigin Policyを適応するために、まずSec-Origin-Policyレスポンスヘッダで適応するポリシーを通知します。なお、httpsでないとダメです。

Sec-Origin-Policy: policy=example-policy

ブラウザはこのレスポンスヘッダを見て、該当するポリシーファイルを参照しに行きます。
https://<ドメイン名>/.well-know/origin-policy/example-policy」

ポリシーファイルは以下のように、featuresとcontent_securityにそれぞれのポリシーを記述するだけです。ディレクティブ等の詳細はそちら側の仕様を参照

{
  "features": {
    "policy": "camera 'none', geolocation 'none', 'sync-xhr' 'none'"
  },
  "content_security": {
    "policies": ["script-src 'self' 'unsafe-inline', img-src 'none'"],
    "policies_report_only": ["img-src 'none'"]
  }
}

Chromiumのテストファイルを見ると例が書いてあるので参考になると思います
https://cs.chromium.org/chromium/src/third_party/blink/web_tests/external/wpt/.well-known/origin-policy/?g=0

試してみる

上記で解説したレスポンスヘッダ及びポリシーファイルを、https://asnokaze.com に設定しました。

Origin Policyによってジオロケーションへのアクセスを禁止していますので、コンソールからジオロケーションにアクセスしようとすると、ちゃんとブロックされることが確認できました。
f:id:ASnoKaze:20200126020918p:plain

なお、正しくポリシーファイルを設置しないとページの表示ができなくなってしまいます
(ページタイトルが"元のポリシーエラー"になってるのは修正中のようです)
f:id:ASnoKaze:20200126021436p:plain

https://asnokaze.com のオリジン ポリシーに沿ってブロックしました。
サイト https://asnokaze.com は、すべてのリクエストにオリジン ポリシーの適用を求めていますが、このポリシーは現在適用することができません。

WebTransport over QUICのサンプルサーバを試してみる

2022/01/04追記: この記事はすでに古いです。WebTransport over QUICは標準化せずに、WebTransport over HTTP/3を標準化する方向です。Chromeの実装もそちらに移っています。


"WebTransport over QUIC"の標準化及び、Chromiumでの実装が進んでいます。

今回はクライントを書いて、Chromiumに同梱されているquic_transport_simple_serverと通信してみました。環境は Ubuntu 18.04です。

WebTransport over QUICについては以前書いたブログを参照してください
asnokaze.hatenablog.com

quic_transport_simple_server のビルド

Chromiumのビルド手順通り勧めます (Google社員向けはスルーしましょう)
https://chromium.googlesource.com/chromium/src/+/master/docs/linux/build_instructions.md

  • Install
  • Get the code
    • Install additional build dependencies
    • Run the hooks
  • Setting up the build

フェッチに時間がかかります。
最後にビルドする際に、chromeのビルドではなく、サンプルサーバのみをビルドします

autoninja -C out/Default chrome #この部分を
ninja -C out/Default quic_transport_simple_server #こうする

証明書・鍵の作成

サーバを起動するのにPKCS#8形式の鍵が必要です

openssl req -new -key server.key > server.csr
openssl req -new -key server.key > server.csr
openssl x509 -days 3650 -req -signkey server.key < server.csr > server.crt
openssl pkcs8 -topk8 -inform PEM -outform DER -nocrypt -in server.key -out server.p8

起動

ビルドができていれば、実行ファイルがありますので実行します

~/chromium/src/out/Default$ ./quic_transport_simple_server \
    --accepted_origins="" \
    --key_file="/path_to/server.p8" \
    --certificate_file="/path_to//server.crt" \
    --allow_unknown_root_cert --port="10000" 
  • --accepted_origins: client indicationで受け付けるオリジン名(空を指定)
  • --key_file: 鍵
  • --certificate_file: 証明書
  • --port: ポート

クライアントサイド

WebTransport over QUICでは、QUIC DATAGRAMフレームのサポートが必須です。実際にはトランスポートパラメータでmax_datagram_frame_sizeを指定する必要があります。

今回はDATAGRAMフレームをサポートしてる、PythonのQUICライブラリ「aioquic」を利用します。
(exampleとして、DATAGRAM検証用プロトコル"siduck"実装が含まれてるのも選んだ理由です)

インストール方法は「aioquicのgithubリポジトリ」を参照。

./examples/siduck_client.py を雑にいじって、WebTransport over QUIC クライアントを作成します。
ソースはこちら https://gist.github.com/flano-yuki/3ab550393138066b19d2ff4d44e0cb47

~/work/aioquic$ python3 examples/quic_transport.py  localhost 10000 -k -q out -l ./ssllog
2020-01-13 21:04:51,881 INFO quic [229cca53a4662860] ALPN negotiated protocol wq-vvv-01
2020-01-13 21:04:51,882 INFO client Sent Client Indication
2020-01-13 21:04:51,883 INFO client Say Hello
2020-01-13 21:04:51,885 INFO client Echo is received: 
2020-01-13 21:04:51,885 INFO client b'hello world'

(-k はデバックログをファイルに出力、-lは復号用の鍵ファイルをファイルに出力)

quic_transport_simple_serverにデータを送って、Echoを受信することに成功しました。

修正ポイント

ポイントは

  • ALPNとして"wq-vvv-01"を指定する
  • Client Indicationを送信する(OriginとPATHをそれぞれ指定)
    • PATHとして/echoを指定すると、サーバはechoモードで動作します (ソース)
        b  = b'\x00\x00' + b'\x00\x17' + b'https://example.com:443'
        b += b'\x00\x01' + b'\x00\x05' + b'/echo'
        self._quic.send_stream_data(2, b, True)
  • クライアントからデータが送れるストリームでデータを送り、Echoされたデータを受信する
  • (いくつか試したが、DATAGARAMフレームにはEchoを返してくれない...? ソース的には未実装?)

終わりに

とりあえず、通信できるところまでうまく行ったので、ちゃんとクライアントを実装したい。quic-go もDATAGARAMフレーム実装が進んでるので、そっちでも試したい (サーバ・クライアント両方)