HTTP/3で接続してVPNとして使うMASQUEプロトコルの提案仕様

[2021/05/16 追記] 議論が進み、MASQUEを実現する仕様が幾つか出てます


GoogleのDavid Schinazi氏が「The MASQUE Protocol」という仕様を提出している。

初版のdraftであり、議論の呼び水としての立ち位置が強いがまずは読む。

MASQUE Protocol

MASQUEはMultiplexed Application Substrate over QUIC Encryptionの略称です。この提案仕様では、例えばHTTP/3のQUICコネクションを確立したあとに、そのQUICコネクションをVPNとして使う例を上げています。もちろん、他のプロトコルも通信可能だと思います。

このMASQUEプロトコルの大きな特徴としては、第三者がMASQUEプロトコルの使用に気づかないところである。

  • クライアント・サーバ間の通信を見てもMASQUEプロトコルを喋ってることがわからない
  • 三者がサーバに通信を試みてもただのWebサーバにしか見えない (MASQUEプロトコルを喋るための鍵がないため)

もちろんHTTP/2 over TCPにフォールバックも可能だがパフォーマンス的には劣ることになる。

また、Datagramフレームの利用などを上げているが、具体的なVPN over QUICの仕組みには言及しておらず、方向性含め議論となると思われる。
(「QUICの信頼性のないデータグラム拡張(MESSAGEフレーム/Datagramフレーム)」)

HTTP/3として通信を開始したあとに、クライアントを認証したあとに、そのストリームはMASQUEプロトコル専用に移行します。
f:id:ASnoKaze:20190302143206p:plain

  • MASQUE通信を行うクライアントとサーバは事前に共通鍵を共有しておくか、サーバはクライアントの秘密鍵に対応する公開鍵をリストしておきます。
  • クライアントは通常のHTTP/3で、サーバに接続します。(ラベルEXPORTER-masqueを用いた、TLS keying material exporterで得られた値を後のノンスとして使用する。)
  • クライアントは/.well-known/masque/initialに対してCONNECTメソッドのリクエストを投げます。この際に認証に使う、Masque-Authenticationを付加します。公開鍵を使う場合と、共通鍵を使う場合で異なります
  Masque-Authentication: PublicKey u="am9obi5kb2U=";a=1.3.101.112;
  s="SW5zZXJ0IHNpZ25hdHVyZSBvZiBub25jZSBoZXJlIHdo
  aWNoIHRha2VzIDUxMiBiaXRzIGZvciBFZDI1NTE5IQ=="

  Masque-Authentication: HMAC u="am9obi5kb2U=";a=2.16.840.1.101.3.4.2.3;
  s="SW5zZXJ0IHNpZ25hdHVyZSBvZiBub25jZSBoZXJlIHdo
  aWNoIHRha2VzIDUxMiBiaXRzIGZvciBFZDI1NTE5IQ=="
  • サーバはMasque-Authenticationを見てクライアントを認証します
    • クライアントを正しく認証できなかった場合は、405 Method Not Allowedを返します。これは予期せぬCONNECTメソッドに対する通常の応答であるため、第三者が試みてもMASQUEプロトコルに対応してるかはわかりません。
    • クライアントを正しく認証できた場合は、101 Switching Protocolsを返してそのストリームをMASQUEプロトコル専用にします。

リバースプロキシのエラーを示す Proxy-Statusヘッダの提案仕様

CDNクラウドのロードバランサを使用するのは一般的です。これらのリバースプロキシは様々な理由により502 Bad Gateway504 Gateway Timeoutを返しますが、トラブルシュートするには情報が少ない場合があります。

また、追加の情報を示す場合においても、各社によって異なっています。

そこで、プロキシのエラー情報を示すProxy-Statusレスポンスヘッダを定義する「The Proxy-Status HTTP Header Field」という提案仕様がFastlyのmnot氏らより出されています。

初版のdraftだが読む。

Proxy-Statusヘッダはまずそのエラータイプが示され、Extra Parameters続きます(ない場合もあります)。

f:id:ASnoKaze:20190221012051p:plain
Proxy-Statusの例を示します。

   HTTP/1.1 504 Gateway Timeout
   Proxy-Status: connection_timeout; proxy=SomeCDN; origin=abc; tries=3

上記は、SomeCDNがオリジンであるabcに対して3回思考した後、connection_timeoutのエラーとなったことを示します。
proxy, origin, tries などがExtra Parametersです。

Proxy Status Types

現在はエラーとして様々なタイプが定義されています。

エラーごとに推奨されるステータスコードも併記されている。また、エラーごとに付与されるExtra Parametersが異なるが本記事では省略する。

  • dns_timeout: 宛先ホスト名の解決にタイムアウトした(504)
  • dns_error: 宛先ホスト名の解決エラーとなった(502)
  • destination_not_found: 適切なバックエンドを決定できなかった(500)
  • destination_unavailable: ネクストホップが利用できないと判断した(503)。ヘルスチェックがダウンしてる場合など。
  • destination_ip_prohibited: 宛先IPへの接続を禁止する設定になっている(502)
  • destination_ip_unroutable: 宛先IPへの経路を見つけることができなかった(502)
  • connection_refused: ネクストホップに拒否された(502)
  • connection_terminated: ネクストホップに切断された(502)
  • connection_timeout: ネクストホップへの接続がタイムアウトした(504)
  • connection_read_timeout: 期待すべきデータを待ったが上限にたっした(504)
  • connection_write_timeout: データを書き込もうとしたが出来なかった。バッファがはけなかった場合など(504)
  • connnection_limit_reached: コネクション数の上限に達した
  • http_response_status: 4xxや5xxのレスポンスを受け取った
  • http_response_incomplete: 受け取ったレスポンスが不完全(502)
  • http_protocol_error: HTTPプロトコルエラー(502)
  • http_response_header_block_size: HTTPレスポンスヘッダブロックが大きすぎる(502)
  • http_response_header_size: HTTPレスポンスヘッダのいずれかが大きすぎる(502)
  • http_response_body_size: HTTPレスポンスボディが大きすぎる
  • http_response_transfer_coding: レスポンスのtransfer-codingデコードエラー(502)
  • http_response_content_coding: レスポンスのcontent-codingデコードエラー(502)
  • http_response_timeout: HTTPレスポンスのタイムアウト(504)
  • tls_handshake_error: ネクストホップとのTLSハンドシェイクエラー(502)
  • tls_untrusted_peer_certificate: ネクストホップとのTLSハンドシェイクにおける信頼できない証明書エラー(502)
  • tls_expired_peer_certificate: ネクストホップとのTLSハンドシェイクにおける証明書有効期限切れ(502)
  • tls_unexpected_peer_certificate: ネクストホップとのTLSハンドシェイクにおける期待されない証明書のエラー(502)
  • tls_unexpected_peer_identity: ネクストホップとのTLSハンドシェイクにおける名前の不一致(502)。Subject Alternative Nameの不一致など
  • tls_missing_proxy_certificate: ネクストホップとのTLSハンドシェイクで証明書を要求したが設定されていなかった(500)
  • tls_rejected_proxy_certificate: TLSハンドシェイク中に得られた証明書を拒否した(500)
  • tls_error: ネクストホップとの通信中のTLSエラー(502)
  • http_request_error: オリジンに代わって、プロキシが400, 403などのステータスコードを返す
  • http_request_denied: HTTPリクエストを拒否し、HTTPリクエストはフォワードしなかった。
  • http_upgrade_failed: プロキシとネクストホップ間でHTTP Upgradeに失敗した(502)
  • proxy_internal_error: オリジンと関係しないプロキシ内のエラー(500)

Fake SNIという提案仕様について

SNIを用いた通信のブロッキング及び、「Encrypted SNI拡張」のブロッキングについてはIETFTLS WGでも話題となりました((TLS) SK filtering on SNI, blocking ESNI)。

Encrypted SNIはSNIを暗号化する一方で、ClientHelloにencrypted_server_name拡張をつけます。そのため、経路上の観測者はEncrypted SNIが使われていることを検知できます。

それを回避するために、ニセのSNIをつける「Fake Server Name Indication」というdraftが提出されています。

初版のdraftであり、これから議論のあるところだとは思うが、とりあえず面白そうなので読んでみる。

Fake SNI

Fake SNIを利用するサービスは事前に、偽のホスト名を公表します。DNSを利用することを想定していますが、他の方法でも問題ありません。

DNSを用いる場合は、TXTレコードで下記のように本来のドメイン名に関連付ける偽のホスト名を定義しておきます。

_fakesni.example.com. 60S IN TXT "myfakerecord.com IP"

f:id:ASnoKaze:20190220124940p:plain

  • まずクライアントは通信を行うドメインの偽のホスト名を取得します。
  • クライアントは取得した偽のSNIを設定してTLSハンドシェイクを開始します
  • サーバは、Fake SNIのSNIを受け取った場合に、本来のホスト名の証明書を返します。なおTLS1.3ではCertificateは暗号化される
  • クライアントは本来のホスト名の証明書が取得でき、正しく検証できた場合に通信を継続します。

こうすることで、経路上の観測者にはFake SNI使ってるかどうか区別はつかなくなります。

感想

偽のホスト名は公開されているため、経路上の観測者自身が名前解決を行い記録しておいたり、DNS通信を収集することで、偽のホスト名と紐づく本来のドメインはわかりうる。頻繁に偽のホスト名を変えることで対応はできそうではあるが

また、偽のホスト名として任意のドメインを使えるというのは、経路上の中間装置などにどのような影響を与えうるか気になった。

Fetch Metadataリクエストヘッダについて (Sec-Fetch-*)

ブラウザがリソースをFetchするさいに、そのFetchに関するメタ情報をリクエストヘッダに付与するというのがFetch Metadataという仕様です。

この情報を用いれば、画像の読み込みのFetchで銀行用のAPIが叩かれるはずがないといった、明らかな不正なリクエストを検知することができるようになる。

毎度おなじみGoogleのMike West氏によって仕様「Fetch Metadata Request Headers」が書かれている。

以前「Sec-Metadataヘッダについて」で紹介したSec-Metadataをより改良したものです。

ヘッダを分けることでヘッダ圧縮の仕組みとも相性が良くなっています。

Example

以下のような、そのフェッチに関する情報を示すSec-Fetch-*ヘッダが付与される。

Sec-Fetch-Dest: image
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: cross
Sec-Fetch-User: ?F


なお、Chrome CanaryでGoogleのサイトにアクセスすると、すでにこのヘッダが付いていることが確認できた。
f:id:ASnoKaze:20190215012147p:plain

意味

Sec-Fetch-Dest

Sec-Fetch-Destは、リクエストの先が何であるかを示します。

現在は、下記のどれかになります:
"audio", "audioworklet", "document", "embed", "empty", "font", "image", "manifest", "object", "paintworklet", "report", "script", "serviceworker", "sharedworker", "style", "track", "video", "worker", "xslt", "nested-document".

Sec-Fetch-Mode

Sec-Fetch-Modeは、リクエストのモードを示します。

現在は、下記のどれかになります:
"cors", "navigate", "no-cors", "same-origin", "websocket"

Sec-Fetch-Site

Sec-Fetch-Siteは、リクエストイニシエータのオリジンと、リクエスト先のオリジンの関係を示します。

現在は、下記のどれかになります:
"cross-site", "same-origin", "same-site"

Sec-Fetch-User

Sec-Fetch-Userは、user activationによって行われたリクエストかどうかを示します。
user activationについては、「HTML StandardのActivation」を参照。

ブーリアンの値を取ります。

Signed Exchange Reporting for distributors について

Webサイトを一つに固めて署名して再配布可能にする、Web Packagingという仕組みがあります。

現在、Web Packagingは以下の3つの仕様からなっています。

AMPなどをより標準化された仕組みで実現するために、現在議論が進められています。

HTTPリクエストとHTTPレスポンスの対(HTTP exchanges)を署名したsxgというファイルを再配布するわけですが、このsxgファイルの検証エラーをユーザエージェントからレポートできるようにする「Signed Exchange Reporting for distributors」という議論がされています。

Signed Exchange Reporting for distributors

概要

publisherがarticle.htmlを署名して作成したarticle.html.sxgを、distributorが再配布します
f:id:ASnoKaze:20190211021908p:plain

  • distributorはpublisherからarticle.html.sxgを取得する
  • ユーザエージェントは、distributorに該当のリソースのリクエストを投げます
  • distributorはarticle.html.sxgを返します
  • ユーザエージェントはarticle.html.sxgの証明書および署名を検証します。
  • 署名の検証に失敗したユーザエージェントは、予め指定されたエンドポイントにその旨レポートをPOSTします。通常のレポート先は、distributorになるでしょう。
レポートのポリシー適応

Network Error Loggingの仕組みを用いて、distributorがレポートの送信先エンドポイントを指定します。

NELについては以前説明したとおりです。
asnokaze.hatenablog.com

このようなレスポンスヘッダで、このポリシーを適応しておきます

Report-To: {"group": "sxg-errors",
            "max_age": 10886400,
            "endpoints": [{ "url": "https://report.distributor.example/" }] }
NEL: {"report_to": "sxg-errors", "max_age": 2592000}

NELの仕様にもプルリクが出ている
https://github.com/w3c/network-error-logging/pull/100

レポートの内容

このようなレポートがPOSTされます。

{
  "type": "signed-exchange",
  "age": 1,
  "url": "https://distributor.example/publisher.example/article.html.sxg",
  "user_agent": "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) ...",
  "body": {
    "phase": "sxg",
    "type": "sxg.signature_verification_error",
    "status_code": 200,
    "referrer": "https://www.example/",
    "method": "GET",
    "sxg": {
      "outer_url": "https://distributor.example/publisher.example/article.html.sxg",
      "inner_url": "https://publisher.example/article.html",
      "cert_url": "https://distributor.example/publisher.example/cert",
    }
  }
}

bodyのtypeとかはその他にも増えるのかな?証明書チェーンの辿れない場合とか、証明書の有効期間が合わない場合とか、いくつかのパターンがありそうな気はする。

Delegated Credentials for TLS について

BoringSSLが「Delegated Credentials for TLS 」に対応したので、簡単に仕様を眺める。


Delegated Credentials for TLS 」の仕様は、もともとはTLS1.3の仕様の著者でもあるEric Rescorlaによって書かれていたようだが、すでに WG DraftとなっておりMozillaFacebook、Cloudflareらの人が共著となっている。

Delegated Credentials for TLS

他社の提供するリバースプロキシやCDNTLSを終端する際、自身のドメインの証明書と秘密鍵を渡す必要があります。その際に、本来のドメイン所持者から委譲する形でクレデンシャルを発行し、終端者はそのクレデンシャルを持ってしてTLSハンドシェイクを行えるようにしようというのが「Delegated Credentials for TLS 」です。

こうすることで、本来の証明書所持者が委譲するクレデンシャルをより適切な形でコントロールできるようになります。CAとのオペレーションなく、自由な有効期限と自由な署名アルゴリズムでクレデンシャルを発行することが出来ます。

おおまかな流れは以下のとおりです
f:id:ASnoKaze:20190127193905p:plain

  • 委譲する側が、最初にdelegated credentialを発行し、対応する秘密鍵とともにTLSを終端するサーバに渡します
  • TLSハンドシェイク
    • クライアントはこの仕様に対応していることを示すdelegated_credential TLS拡張を送信します。
    • サーバはTLSハンドシェイク中に証明書チェーンとdelegated credentialを送信する
    • クライアントはdelegated credentialを検証し、問題なければ通信を続けます。

delegated credential

delegated credentialは、有効期限と公開鍵をもつ署名されたで=たです。署名を検証することで証明書が正しく委譲されていることを確認できます。

Credential は以下の情報を含みます。有効期限と公開鍵などです。

      struct {
        uint32 valid_time;
        SignatureScheme expected_cert_verify_algorithm;
        ProtocolVersion expected_version;
        opaque ASN1_subjectPublicKeyInfo<1..2^24-1>;
      } Credential;

DelegatedCredentialには、上記のCredentialと署名が含まれます

      struct {
        Credential cred;
        SignatureScheme algorithm;
        opaque signature<0..2^16-1>;
      } DelegatedCredential;

Lurk

以前書いたLurk(Limited Use of Remote Keys)という、秘密鍵を渡さずに第三者TLS終端をさせる仕様について書きました
asnokaze.hatenablog.com

このLurkとの違いは、ハンドシェイク中に鍵が必要な処理はバックエンドサーバが行っていましたが、Delegated Credentialsでは事前にdelegated credentialを渡せばTLS終端するサーバだけで処理が完結します。

f:id:ASnoKaze:20190127200856p:plain

HTTP/2 ORIGINフレームのセキュリティを改善する提案仕様

AkamaiのMike Bishop氏らから、「DNS Security with HTTP/2 ORIGIN」という提案仕様が出ている。簡単に読む

ORIGINフレームとは

ORIGINフレームとは、RFC8336で標準化されているHTTP/2の拡張フレームです。

HTTP/2では、複数のドメインへのリクエストでもコネクションを使い回すことができます。https://a.example.comに接続した後に、https://b.example.comへのリクエストする場合、IPが同じで、そのドメインでも証明書が有効な場合(ワイルドカード証明書)は、すでにあるコネクションを再利用できます。

しかし、本当はa.example.comのサーバはb.example.comのコンテンツを提供できないという場合があります。その場合、サーバはステータスコード421を返し、それを受け取ったクライアントは改めてb.example.comにコネクションを張り直してコンテンツを取得します。これでは実際にコンテンツを取得するのが遅くなってしまいます。

ORIGINフレームでは、そのサーバがコンテンツを提供できるドメインリストをサーバから通知することで、クライアントが既存のコネクション上でリクエストを送信できるドメインを知ることが出来るようにします。

こうすることで、より効率よくコネクションの再利用が出来るようになります。

フレーム構造などは以前書いた記事が詳しいです
asnokaze.hatenablog.com

DNS Security with HTTP/2 ORIGIN

このとき、クライアントはORIGINフレームにかかれているドメインであれば無条件に既存のコネクションを使って良いわけではありません。サーバが悪意を持って他のドメインをORIGINフレームに記載すれば、本来のドメインを所持するサーバへ行くリクエストを、自身に送らせることができます。

そのため、通常のコネクションを再利用する時と同様、そのドメインがコネクションを張ったときに取得した証明書でも有効であることを確認します。その他にもCTログやOCSPを確認して証明書の有効性を確認することを推奨しています。

しかし、ORIGINフレームの仕様(RFC8336)では、ORIGINフレームで提供されたドメインの名前解決をすることは義務付けられていません。それは、名前解決の遅延や、新たな名前解決はクライアントが接続しようとしているサイトをネットワーク運用者に晒すことになるため、クライアントに一任されています。

しかし、名前解決が必須でないことは問題であり、必須にしようというのが「DNS Security with HTTP/2 ORIGIN」の提案です。

想定攻撃

この提案では名前解決を用いない場合にある種の攻撃が容易になるとしています。

たとえば秘密鍵が漏れた場合などを想定しています。そのときに、攻撃者はそのドメインへのリクエストを、自身のサーバに誘導することがきでるようになります。

例えば、victim-server.example.com秘密鍵が流出した場合。

攻撃者は、まずユーザに自身のWebサーバにアクセスさせます、そのURLなんでも良く、SNS掲示板に貼ればアクセスさせることは容易です。その後、攻撃者のサーバはORIGINフレームでvictim-server.example.comのコンテンツが提供できるとクライアントに通知し、さらに「Secondary Certificate Authentication in HTTP/2」の仕様に乗っ取りvictim-server.example.comの証明書と、秘密鍵の所持を証明します。

そうすると、クライアントはvictim-server.example.comへのリクエストを攻撃者のサーバに送信することになります。victim-server.example.comが、twitterだったりgoogleだったりすると思うと恐ろしいですね。

名前解決を行っていれば、この攻撃を防ぐことができます。もちろん、名前解決の通信を改ざんできれば攻撃は可能ですが、ORIGINフレームを用いることで攻撃が容易になっているということです。

おわりに

上記のような攻撃を緩和するために、ORIGINフレームで名前解決を必須にしようという提案でした。

不正な証明書が発行されたり、秘密鍵が漏れた場合に、CTログやOCSPに反映されるまでの間に発生しうる攻撃に対して、どれまでの緩和策が必要なのかというのは難しい問題だなと感じました。