HTTP/3の解説を書いた (2020/06/01)

@flano_yukiがHTTP/3について書きました (無料です)。

2020/06/01 時点です。TODOも多くありますが、図表含め今後加筆されます。

http3-note

https://github.com/flano-yuki/http3-note にPDFを置きました

公開形式は、そのうちなんとかするかも

内容

  • 1. はじめに(HTTP/3と概要)
    • 1.1 はじめのはじめに
    • 1.2 HTTPのセマンティクスとバージョンの話
    • 1.3 HTTP/3の概要
    • 1.4 HTTP/3 と呼ばれるまでの道のり
    • 1.4.1 Google QUICの実験
    • 1.4.2 HTTP over QUIC、標準化の開始
    • 1.4.3 HTTP/3への改称
    • 1.5 標準化動向を追うために
  • 2 QUICについて
    • 2.1 QUIC、はじめに
    • 2.2 QUICの概要
    • 2.3 QUICコネクションとQUICパケットの基礎
    • 2.4 フレームについて
    • 2.5 ストリームについて
    • 2.6 コネクションの確立
    • 2.7 コネクションのクローズ
    • (TODO) 2.8 負荷分散・トラフィックのオペレーション
    • 2.9 その他 (FEC, Multipath, LB)
    • 2.9.1 Forward Error Correction(FEC)
    • 2.9.2 MP-QUIC
    • (TODO) 2.9.3 QUIC-LB
    • 2.10 応用例
    • TODO
  • 3 HTTP/3について
    • 3.1 HTTP/3のはじめに
    • 3.2 HTTP/3対応の通知とコネクションの開始
    • 3.3 QUICストリームの利用
    • 3.4 HTTP/3 フレーム
    • 3.5 HTTPメッセージの送受信
    • 3.5.1 HTTPメッセージを送るまで
    • 3.5.2 HTTPメッセージの送受信
    • 3.5.3 CONNECTメソッド
    • 3.6 ヘッダ圧縮 QPACK
    • 3.6.1 ハフマン符号
    • 3.6.2 静的テーブル、動的テーブル
    • 3.6.3 ヘッダ表現
    • 3.7 サーバプッシュ
    • 3.8 優先度制御
    • 3.9 コネクションの終了
    • 3.10 拡張性
  • 4 HTTP/3 拡張仕様と応用
    • 4.1 HTTP/3 拡張仕様と応用、はじめに
    • 4.2 HTTP/2の拡張フレーム
    • 4.3 DATAGRAMフレーム
    • 4.4 WebTransport
    • 4.4.1 背景と概要
    • 4.4.2 WebTransport over QUIC
    • (TODO) 4.3.3 WebTransport over HTTP/3
    • (TODO) 4.5 MASQUE
    • (TODO) 4.6 RIPT
    • (TODO) 4.7 HTTP over multicast QUIC

CONNECT-UDP HTTPメソッドの仕様

MASQUEプロトコルの標準化の流れで、GoogleのDavid Schinazi氏から「The CONNECT-UDP HTTP Method」という提案仕様が提出されている。

MASQUEは、HTTP/3でプロキシサーバに接続した後にそのコネクションをトンネルとして使用し、VPNのように任意のデータをやりとりするプロトコルです。通信を観測する第三者からは、ただのHTTP通信を行っているようにしか見えません。

f:id:ASnoKaze:20200506005033p:plain

このMASQUEは現在IETFでワーキンググループを作成している最中であり、まだまだ議論が始まったばかりの仕様になります。

CONNECT-UDP HTTPメソッド

CONNECT-UDP HTTPメソッドは、HTTPの接続をUDPでプロキシするのに使用されるメソッドです。

クライアントは以下のようなリクエストを送信します (HTTP/2以降は疑似ヘッダで表現される)

:method CONNECT-UDP
:authority server.example.com:443
Datagram-Flow-Id 2

(Datagram-Flow-Idヘッダについては後述する)

このリクエストを受け取ったプロキシは接続先のサーバに対してUDPソケットを開き、クライアントに200番台のレスポンスを返す。それ以降プロキシはう受け取ったデータをUDPでターゲットサーバに転送します

なお、HTTP/3を使用している場合は「HTTP/3 DATAGRAM Frame」を使用できます。HTTP通信は同一ストリーム上ではデータは送った順番通りに処理される必要があります。HTTP/3 DATAGRAM Frameは、パケットが欠損したりパケットの順番が入れ替わっても届いた順番で処理していいデータを運ぶ用のフレームです。

以前解説した、QUICレイヤのDATAGRAM Frameとは異なり、HTTP/3レイヤで送信されるフレームです。
asnokaze.hatenablog.com

フォーマットも微妙に異なっており、Flow Identifierという識別子を持ちます。CONNECT-UDPリクエストを送信する際に、Datagram-Flow-Idヘッダを送信しますが、利用するHTTP/3 DATAGRAM FrameのFlow Identifierと同じ値を入れて、紐付けを行います。
f:id:ASnoKaze:20200506004620p:plain

これによりHead-of-line blockingを防ぐことができます。

f:id:ASnoKaze:20200506012157p:plain

そのた

(間違ってる箇所があったらすみません)

サポートしてないwell-known URIs に対するステータスコードを確認する提案仕様

Webの仕様ではWell-know URIとして特別なPathを指定するものがあります。

例えば、現在議論が進められている「A Well-Known URL for Changing Passwords」では .well-known/change-password をパスワード変更用のページにリダイレクトする特別なパスとして定義しています。

実際にtwitterFacebookではすでに対応しています

サポートしていないwell-known URIs に対するリクエストには200番台以外のステータスコードを返さなければなりません。しかし、実際にはリソースが存在しないのに200番台のレスポンスを返すサーバがいるようです。


そのために 200番台を絶対に返さない well-known URIs を定義し、そういったサーバを検出できるようにするのが「Detecting the reliability of HTTP status codes」です

Detecting the reliability of HTTP status codes

後述の通り、「Detecting the reliability of HTTP status codes」では必ず200番台のレスポンスを返さないwell-known URIsを定義します。

具体的には下記のとおりです

.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200

このパスに対してリクエストを送ることで、サーバが返すステータスコードの信頼性を確認できます。

これらの仕様は、W3CのWICGで管理されているドキュメントです。これから、まだまだ変更される可能性があります。

Compact TLS 1.3の提案仕様

TLS1.3の仕様を書いたEric Rescorla氏らより「Compact TLS 1.3」という提案仕様が提案されており、すでにWG Draftとなっています。

このCompact TLS (cTLS)は、よりコンパクトなTLSを定義しデータの通信量を削減しています(リソースなどが制限されたデバイスでも利用できるようにというモチベーションもあります)。TLS1.3自体とisomorphic(同型)でありやり取りされるメッセージの意味自体は同じですがメッセージのフォーマットは異なっており、cTLSとTLS1.3自体と相互通信はできません。

しかし、cTLS, TLS1.3両方に対応することはできるので、将来的にそういうサーバが出てくれば問題なく通信できるようになりそうです。

アプローチ

主なアプローチは

  • TLS1.3の冗長なフィールド、廃止されたフィールドを取り除きます (TLS1.2との互換性の都合形だけ入ってたフィールドなど)
  • 可変長整数を使い、整数値のフィールドを削減します
  • 不要な値の省略
  • デフォルト値を定義可能にするテンプレート (profile)

結果として、下記の通りデータ転送量を削減できます。
f:id:ASnoKaze:20200430000115p:plain

いくつか説明していきます

冗長, 不要なフィールドの廃止

TLS1.2との互換性のために確保されてた領域をクリーンアップしています

Record Layer

         struct {
             ContentType type;
             opaque fragment[TLSPlaintext.length];
         } TLSPlaintext;

ClientHello

         struct {
             Random random;
             CipherSuite cipher_suites<1..V>;
             Extension extensions<1..V>;
         } ClientHello;

ServerHello

         struct {
             Random random;
             CipherSuite cipher_suite;
             Extension extensions<1..V>;
         } ServerHello;

テンプレート(Profile)

ProfileはJSON形式で記述され、暗号関連のパラメータ・TLS拡張・証明書などが事前定義されており。ハンドシェイク時はこれらの値を使う場合は省略されます。また、randomの長さなども短く設定することもできます。

このProfileはクライアントとサーバで事前に共有されている必要があります。(標準Profileを定義するかや、ハンドシェイク中に使用しているProfileを相手に通知する方法などは現在議論中です。)

Profile例

   {
     "version": 772,
     "cipherSuite": "TLS_AES_128_CCM_8_SHA256",
     "dhGroup": "X25519",
     "signatureAlgorithm": "ECDSA_P256_SHA256",
     "randomSize": 8,
     "finishedSize": 8,
     "clientHelloExtensions": {
       "server_name": "000e00000b6578616d706c652e636f6d",
     },
     "certificateRequestExtensions": {
       "signature_algorithms": "00020403"
     },
     "knownCertificates": {
       "61": "3082...",
       "62": "3082..."
     }
   }

Cookieのセキュリティを改善する Scheming Cookiesについて

Chromeが「Cookie の SameSite=Lax をデフォルト化」を進めたことは記憶に新しい。
asnokaze.hatenablog.com

Cookieの改善は引き続き議論されており、Cookieの扱いでスキーム(http://やhttps://)を考慮に入れることが検討されている。

Incrementally Better Cookies

Cookieのセキュリティ改善を精力的に行っているGoogleのMike West氏は、Secure属性の利用が30%、"__Secure-"プレフィックスの利用が0.18%ほどにとどまっていると述べており(リンク)、セキュリティ改善のためにCookieの扱いを段階的に変更していくことを考えている。

同氏がIETFに提出している「Incrementally Better Cookies」では、Cookieを次のように段階的に改善することを提案している。

  1. 「SameSite = Lax」をデフォルトにする
  2. 「SameSite = None」にするにはHTTPSから配信される必要がある
  3. same-siteはスキームを考慮にいれるようにする (Schemeful Same-Site)
  4. Cookieはスキームを尊重する必要がある (Scheming Cookies)
  5. 非セキュアなスキームのCookieは、セッションの最後に削除する
  6. セッションの定義を厳しくする

すでに、最初の2つは実施されている。これに続く、Schemeful Same-Siteや、Scheming Cookiesなどについて簡単に書いていく。

ただし、同氏がBlink-devで「Reducing web compatibility risk.」述べているように、昨今の状況を鑑みてWebの互換性を壊しうる変更はすぐにブラウザには入らないだろう。

Schemeful Same-Site

Same-Site の判定は、eTLD+1である。

例えば下記はsame-siteである。

また、https:// と http:// は考慮に入らないが、Schemeful Same-Siteではスキームを考慮にいれるという変更である。

Same-site Cookieの場合は、https://example.comでセットされたcookieは.http://www.example.com へのリクエストでは付加されません。逆もそうなります。

Scheming Cookies

(same-site属性なしでも) スキームを考慮するようにします。Scheme-Bound Cookies とも呼ばれます。

ブラウザがCookieを保存する際に、スキーム情報も保存します。ブラウザは今までの条件に加えスキームも一致している場合のみ、Cookieを送信します。

非セキュアなCookieの失効

次の段階として、非セキュアなスキームでセットされたCookieをセッション終了時に失効することも検討されている。

時刻同期プロトコル Roughtime の標準化

Rougtime」という時刻同期プロトコルの標準化が進められている。

すでに、GoogleやCloudflareがRoughtimeサーバを公開している

  • roughtime.cloudflare.com
  • roughtime.sandbox.google.com

もともとは、2016年頃にGoogleのAdam Langley氏が原案を公開しており、サーバの実装が進められた。その後、時刻同期プロトコルを扱うIETFのNTP WGに仕様が提出されました。そして2020年1月に、「Rougtime」としてWG Draftになり標準化が進められています。

Roughtimeプロトコルの特徴

Roughtimeは以下の特徴を持つ

  • クライアントからリクエストを送信し、サーバから署名を付けてレスポンス(時刻+証明書+署名)を返す
  • 複数サーバを利用することで、暗号的に不正なサーバを暗号的に検知できる
  • UDPTCPに対応。UDPでは、アンプ攻撃対策が入ってる。
  • サーバにおける署名処理は、複数クライアントからのリクエストを一気に署名できるようになっている
  • バッチ的に処理を行うため、高精度の時刻同期ではない

f:id:ASnoKaze:20200412230149p:plain:w480
(図はリクエスト/レスポンス内に含まれるタグを一部咲楽しています)

Rougtimeでは、クライアントからリクエストを送り、サーバから署名のついたレスポンスが返されます。サーバの署名がついてますので、クライアントはサーバを認証できます。

クライアントはリクエストにノンス(NONC)をつけて送ります。これに対してサーバ側はレスポンス(時刻+証明書+ノンスに対する署名 SIG)を返します。これにより、クライアントからのリクエストを受信したあとに、レスポンスが生成されたことを暗号的に保証することができます。

クライアントはサーバAからのレスポンスを、乱数と結合しハッシュを撮った値を付きのサーバBへのノンスとして送ります。こうすることで、「サーバAからの時刻 < サーバBからの時刻」ということことが暗号的に保証されます。こうすることで、暗号的に不正な値を返すサーバを見つけることができます。

各サーバは事前に長期鍵として公開鍵を公開している。その鍵で一時鍵を署名して、その一時鍵を用いてクライアントからのリクエストに対して署名処理をする。

例えば、Cloudflareでは長期鍵はこのように公開されています。

   address:       roughtime.cloudflare.com
   port:          2002
   long-term key: gD63hSj3ScS+wuOeGrubXlq35N1c5Lby/S+T7MNTjxo=

メッセージ

リクエストおよび、レスポンスのメッセージは下記の構造を持ちます。タグというキー・バリューのペアが複数含まれます。

最初にタグの数と、メッセージの中で1~N番目のタグがどこから始まるか示すオフセット、32bitのタグ(ASCIIでNONCやVERのように表現されます)およびタグの値が繰り返されます。

f:id:ASnoKaze:20200412231215p:plain

リクエス

リクエストは以下のタグを持ちます

  • NONC: ノンス
  • VER: プロトコルバージョン
  • PAD: UDPの場合は、アンプ攻撃を防ぐためリクエストメッセージが1024バイトを超えるようにパディングをします
レスポンス

レスポンスには、時刻を示すMIDP、証明書を示すCERT、署名値SIGとマークルツリー(後述) ROOT INDEX PATHなどである。

署名対象はSREPであり、ここに時刻やマークルツリーのROOTの値の値が含まれている。複数のリクエストを一度に処理する場合、このPREPが同じ値になる。

Response

  • SREP
    • ROOT: リクエストとの署名値を含むマークルツリーのルート値
    • MIDP: 処理の瞬間のタイムスタンプ
    • RADI: MIDPの制度を示す値 MIDP - RADI < MIDP < MIDP + RADI になる。
    • NONC: リクエストのノンス
    • LEAP: うるう秒情報
  • SIG: 署名 (signature contextとSREPかDELEを結合したものを、署名したもの)
  • CERT: サーバーの長期鍵で署名された公開鍵証明書
  • DELE: サーバーがSREPタグに署名するために使用する委任された公開鍵証明書が含まれています。
  • MINT: PUBKが信頼される最低時間(MIDP)
  • MAXT: PUBKが新楽される最高時間(MIDP)
  • PUBK: SREPタグの署名に使用される一時鍵
  • INDEX: マークルツリー内のNONCの位置
  • PATH: マークルツリー内のNONCからROOTへのパス (各ノードの32バイトハッシュ値の羅列)
  • VER: バージョン

署名のバッチ処理

Routhtimeでは、サーバは複数のリクエストのノンスに対して一度に署名処理を行います。これは、先述のマークルツリーを利用します。

ツリーの各リーフには、複数のクライアントから送られてきたノンス(NONC)が含まれます。各リーフは左から順にindexを持ちます。

親は、左のノードと右のノードを結合しハッシュを取った値が含まれています。

順々にハッシュを取ってルートが計算されると、ルートの値と時刻情報を合わせて署名を行います。その署名結果を複数のリクエストに対するレスポンスに含めます。

f:id:ASnoKaze:20200412235758p:plain

クライアントは署名値を検証するのにマークルツリー全体の情報は必要なく、自身の送ったノンス(NONC)のindex, ROOT, およびPATH(ROOTまでハッシュを計算するのに必要なノードの値)のみが必要です。

例えば、index3のPATHは赤く囲ったノードになります。

TLS ClientHelloの暗号化(ECHO)に関するメモ

広域盗聴行為(RFC7258)への対策として、通信の暗号化が行われています。

TLS1.3からは、サーバ側証明書は暗号化されるようになり、観測者が得られる情報はIPアドレスやSNIから得られるホスト名などになりました。

このSNIの情報も暗号化するために、さまざな議論が行われました。その中で出てきた提案として「Encrypted Server Name Indication for TLS 1.3」という提案仕様があります。

この仕様はすでに、TLS WGでWG Draftとなっており第6版まで出てます。版が進む中で幾つかの変更があり、HTTPSSVCレコードの利用や、encrypting the entire ClientHello (ECHO)といった仕組みなどが入っています。

簡単に現在のDraftをつまみ食いする。

なお、HTTPSSVCについては以前書いたとおり。こちらは、Chromeなどで対応が進んでいる。
HTTPSで接続するための追加情報を格納するHTTPSSVCレコード - ASnoKaze blog

構成

本仕様では、以下の2種類の構成が考えられている。

それぞれドメイン名として、経路上からはSNIとしてpublic.example.comが設定されているように見える。 暗号化されたSNIとしてprivate.example.orgが送られる。そんなイメージである。

f:id:ASnoKaze:20200409024404p:plain

それぞれが同居しているShared Modeと、分離しているSplit Modeの2つの構成です。

public.example.comの部分が、大手検索会社や大手CDN会社のドメイン名になり、多くのサイトの前段となれば、パプリックなドメインとプライベートなドメインを紐付けることは困難となる。

ECHO

Draft 6から、ECHOという仕組みになりました。これは、ClientHelloをまるごと暗号化する仕組みです。これによってALPNなどの領域も暗号化されるようになります。このClientHelloの暗号化は、暗号化したClientHello(ClientHelloInner)を含むClientHello(ClientHelloOuter)で送る形で転送されます。

f:id:ASnoKaze:20200409020447p:plain

ClientHelloOuterはちゃんとしたClientHelloですが、TLS拡張としてencrypted_client_hello拡張を持ちます。

encrypted_client_hello拡張には、暗号化されたClientHello及びそれを復号するの必要な情報が格納されています。

サーバは、ClientHelloInnerを復号し受理するか、もしくはClientHelloOuterを使用できます。サーバによってClientHelloOuterが選択され処理された場合、ClientHelloOuterに対するサーバ証明書がクライアントに送信されます、後述のpublic_nameとしてvalidな証明書か検証する必要があります。

暗号化

ClientHelloの暗号化方法についても簡単に触れます。

クライアントがClientHelloを暗号化するために、ECHOConfigというメタ情報を使用します。このECHOConfigは任意の方法でクライアントに伝えられますが、例えばDNSのHTTPSSVCレコードを用いる事が言及されています。複数のECHOConfigをクライアントに渡し、使用できる設定が使用されます。

    struct {
        opaque public_name<1..2^16-1>;

        HpkePublicKey public_key;
        HkpeKemId kem_id;
        CipherSuite cipher_suites<2..2^16-2>;

        uint16 maximum_name_length;
        Extension extensions<0..2^16-1>;
    } ECHOConfigContents;

    struct {
        uint16 version;
        uint16 length;
        select (ECHOConfig.version) {
          case 0xff03: ECHOConfigContents;
        }
    } ECHOConfig;

ECHOConfigは、version, length, およびECHOConfigContentsからなります。versionは0xff03が使用されます。ECHOConfigContentsに実際に暗号化するための鍵とサイファースイートなどが入っています。

暗号化に関しては、HPKEとあるようにIETFのCFRGで議論されている「Hybrid Public Key Encryption」という仕組みが用いられている。

ということで、ECHOConfigからゴニョゴニョされて暗号化されたClientHelloが導出される。。。

pkR = HPKE.KEM.Unmarshal(ECHOConfig.public_key)
enc, context = SetupBaseS(pkR, "tls13-echo")
echo_nonce = context.Export("tls13-echo-nonce", 16)
echo_hrr_key = context.Export("tls13-echo-hrr-key", 16)
encrypted_ch = context.Seal("", ClientHelloInner)

その他

ちょっと調べきれてないトピックとして、以下がある。引き続きお勉強します

  • ClientHelloInnerが持つecho_nonce拡張
  • HelloRetryRequest
  • HPKE