複数TLSコネクションの署名処理をまとめて行うBatch Signing

GoogleのDavid Benjaminさんにより「Batch Signing for TLS」という仕様が提出されています。

TLSではハンドシェイク中に証明書を持っていることを証明するために、対応する秘密鍵を用いて署名を行います。この署名処理(特にRSA署名)はCPUをたくさん消費するほか、秘密鍵をハードウェアモジュールに格納している場合はオーバーヘッドが高くなります。

この署名処理を、複数コネクション分をまとめて1回で行ってしまうのが「Batch Signing for TLS」です。

Acknowledgmentsに書かれているとおり、Roughtimeプロトコルに同じような機能があります

概要

複数のハンドシェイク中のコネクションから、署名を行うメッセージを集め、Merkleツリーを構成します。

葉を署名対象のメッセージのハッシュ値として、ルートにのみ署名を行います。そのため、複数のメッセージに対して1回の署名で済ませることができます。

メッセージ1, 2, 3をBatch Signingする場合
f:id:ASnoKaze:20190730180837p:plain

  • 葉の初期化: 各メッセージ1, 2, 3 のハッシュ値を偶数番号の葉とします。奇数番号の葉は乱数です。
  • ノードの計算: 各レベルごとに、左右の子を連結したハッシュをノードの値とします
  • ルートの計算: ルートまでノードの値を計算します。

ノードに署名をします。

署名を検証する場合、ルートを計算するのに必要なノードのみが与えられます。メッセージ2を検証する場合、必要なノードはt03, t10, t21です。このノードを順々に連結しハッシュを求めることでルートの値を計算でき、署名値を検証できます。

もちろん、検証する際に他のコネクション用のメッセージを取得できると問題です。そのため、署名するメッセージは偶数番号の葉で、奇数番号は乱数であり結合しハッシュを取ったもののみが他の人にも共有されるため、メッセージ自体がなんだったかそのハッシュ値さえわかりません。

SignatureScheme

Batch Signing用のSignatureSchemeも追加で定義します

       enum {
           ecdsa_secp256r1_sha256_batch(TBD1),
           ecdsa_secp384r1_sha384_batch(TBD2),
           ecdsa_secp521r1_sha512_batch(TBD3),
           ed25519_batch(TBD4),
           ed448_batch(TBD5),
           rsa_pss_pss_sha256_batch(TBD6),
           rsa_pss_rsae_sha256_batch(TBD7),
           rsa_pkcs1_sha256_legacy_batch(TBD8),
           (65536)
       } SignatureScheme

HTTPSで接続するための追加情報を格納するHTTPSSVCレコード

2020/07/19 追記
仕様に幾つかの変更があったため、新しく記事を書き直しました
asnokaze.hatenablog.com



HTTPSで接続する際に以下の情報を持っていると都合がよいです

  • SNIを暗号化するESNIの鍵情報など情報、(通常ESNI DNSレコードに記述される)
  • HTTP/2やHTTP/3で通信可能な事を示すAlt-Svc (通常、HTTPレスポンスヘッダやAlt-Svcフレーム、Alt-Svcレコードで提供される)

それらの情報を通知するのに、DNSに新しくHTTPSSVCレコードを追加する「HTTPSSVC service location and parameter specification via the DNS」という提案仕様がGoogleAkamaiの方の共著で提出されています。

また、このHTTPSSVCレコードではドメインのApexでも使用できるためApexでCNAME使いえない問題も解決できるほか、HTTP Strict Transport Security [HSTS] をクライアントに通知できるので初回接続のセキュリティーを改善します。

拡張可能でもあるため、今後もこのレコードに機能を追加することもできます。

HTTPSSVC レコードの利用例です。

 example.com.      2H  IN HTTPSSVC 0 0 svc.example.net.
 svc.example.net.  2H  IN HTTPSSVC 1 2 svc3.example.net. "hq=\":8003\" \
                                    esnikeys=\"...\""
 svc.example.net.  2H  IN HTTPSSVC 1 3 svc2.example.net. "h2=\":8002\" \
                                    esnikeys=\"...\""
  • example.comsvc.example.netからも提供できることを示します。
  • svc3.example.netからHTTP/3を8003ポートで提供できることを示します(また必要なesnikeysを示します)
  • svc2.example.netからHTTP/2を8002ポートで提供できることを示します(また必要なesnikeysを示します)

HTTPSSVCレコードは存在自体がHSTSを示すので、クライアントはHTTPのリンクもHTTPSで接続しに行きます。

フォーマット

HTTPSSVCは、2つの数字がまずならびます。

仕様では、拡張としてesnikeysでESNI用の鍵も付与できるようになっています。

TCP Slow Startを改善する HyStart++について

20190725追記
draft-01 で計算式に変更が入っています


TCP Slow Startを改善する 「HyStart++: Modified Slow Start for TCP」というdraftがMicrosoftの方より提出されている。

HyStart++はすでにWindowsで実装されている他、HyStartもLinuxのCubicに実装されている。

Slow StartとHyStart++

TCPはコネクション開始直後は送信するデータ量は少なく、徐々に2倍にしていく。パケットロスが発生したタイミングでこのスロースタートは終わることになる。

2倍, 2倍で送信するデータ量を増やしていくと適切な送信量を大幅に超えてしまいうる。大きく超えると多くのパケットロスが発生してしまい、デメリットが大きいため適切なタイミングで線型的にデータ量を増やしていきたい。特定の閾値ではなく、RTTの悪化をトリガーとして線型増加するLimited Slow-Start(RFC3742)に移行するのがHyStart++です。

f:id:ASnoKaze:20190712222721p:plain

各ラウンド毎に、前回のRTTよりもEta(今回のRTT/8 を 4~16msec 以内に収めた値) msec RTTが悪化していた場合はLimited Slow-Startに移行する。

      MIN_SSTHRESH = 16
      MIN_ETA = 4 msec
      MAX_ETA = 16 msec
         if (cwnd is >= MIN_SSTHRESH)
            Eta = clamp(MIN_ETA, currentRoundMinRTT / 8, MAX_ETA)
            if (currentRoundMinRTT >= (lastRoundMinRTT + Eta))
               exit slow start and enter LSS

HyStart++?

参照されているHyStartのドキュメントが有料で読めず、Kernelに貼られているURLもリンク切れになっていた

HyStartで定義されているアルゴリズムが2つあるが、有効だったそのうち一つがHyStart++として改良されている?わからなかった。

提案仕様「HTTP Transport Authentication」について

HTTPレイヤにおいて、使用しているトランスポートレイヤの認証を行う「HTTP Transport Authentication」という仕様がGoogleのDavid Schinazi氏から提案されています。軽く読んだのでメモがてら



この提案では新しいリクエストヘッダ、Transport-Authenticationヘッダを定義します。サーバはこのヘッダをもとに接続相手が正しいクライアントかトランスポートレイヤ的に確認することができます。後述の通りTLSの利用を前提としています

もちろん、この機能は既存のHTTP認証を置き換えるものではなく補完する機能となります。

既存のHTTP認証だけでは、トランスポートのコネクション相手が本当に正しいかはわからず、途中でTLSがほどかれている可能性もあります(サーバ認証するのでほぼないでしょうが)。逆に、TLSのクライアント認証だけではどのAuthorityに対するHTTPリクエストかはわからない状態で認証することになります(HTTP/2ではSNIとリクエスト先のhostが一緒とは限らないため)。

下記の記事でも紹介したとおり、HTTP/3上で別の通信を行うに際して利用することを想定しています。

David Schinazi氏は、MASQUEプロトコルの提案者でもあり、まさにMASQUEプロトコルで利用していた認証方式を別出しした形に近いです。

認証とTransport-Authenticationヘッダ

このHTTP Transport Authenticationでは認証用に、事前に共有鍵をサーバとクライアントを共有している、もしくはクライアントの公開鍵をサーバが知っている前提になります。

TLS keying material exporter (rfc5705)の仕組みを利用して、該当のTLSコネクション由来の乱数を取り出しNonceとして利用します。この取り出したNonceを共有鍵、もしくは秘密鍵で暗号化することで認証用データを作ります。作った認証用データをTransport-Authenticationヘッダに入れて送信します。

それでは、Transport-Authenticationヘッダの中身を見ていきます。

   Transport-Authentication: Signature u="am9obi5kb2U=";a=1.3.101.112;
   p="SW5zZXJ0IHNpZ25hdHVyZSBvZiBub25jZSBoZXJlIHdo
   aWNoIHRha2VzIDUxMiBiaXRzIGZvciBFZDI1NTE5IQ=="

(表示上改行を入れています)

ヘッダにはいくつかのディレクティブがつきます

  • u: 認証するユーザidをBase64で符号化したもの
  • a: しようする暗号アルゴリズムのOID
  • p: 認証用データ。Nonceを暗号化したもの

HTTPレイヤからトランスポートを認証するメリット

MASQUEプロトコルの仕様でも書かれている通り、TLSのクライアント認証とは異なり通信経路上の第三者に認証を行っていることがわからないというメリットがあります。

QUICのAckとロスリカバリについて

QUICのAckとロスリカバリについて自分なりにまとめる。
(トランスポートは詳しくないのでご容赦ください)

あわせて、関連記事もどうぞ

とくに、ストリームやフレームに関しては下記記事を参照のこと
asnokaze.hatenablog.com

はじめに

QUICでは、UDP上で信頼性のあるデータ通信を提供するトランスポートプロトコルです。
(暗号化およびHTTP/3については、関連記事参照)

f:id:ASnoKaze:20190703231338p:plain

今回はdraft-20の主に下記仕様に基づいています

QUICでは内部的に仮想的な通信単位であるストリームを持ち、フレームと呼ばれるメッセージをやりとりします。TCPではパケットロスが発生すると、OSレイヤでパケットロスが回復されるまで後続のパケットを処理できません。しかし、QUICでは同一ストリーム内でのみデータの順番が保証されているため、パケットロスが発生してもデータのロスがなかったストリームは問題なく処理を進めることができます。

このように、UDP上にストリームという通信単位を持つというのは強力ですが、Ackやロスリカバリも多くの工夫がされています。

例えば、QUICではロスしたパケット自体そのものを再送することはなく、必要なデータのみを新しいパケットに格納して送信します。ロスしたパケットの中身のうち不必要なものは再送しませんし、時間が経過し状況が異なっているため新しいACKフレームを送ったり、はたまた優先度の高いアプリケーションデータを先に送るということもできます。(フレームタイプごとに再送処理が異なる)

また、QUICではパケット番号は常に1つずつ増えていきます。先述の通り、パケットロスした場合もそのままパケットを再送するのではなく、新しいパケットとして送信されます。

TCPでは、同一のシーケンス番号で再送を行うため、パケットを受け取った側は再送されたパケットなのかそうでないのかがわかりません。これは、RTTを測定する上での曖昧性となります。RTTをより正確に測定できればより適切なタイムアウト値を設定できるようになります。

その他にも、QUICのACKフレームではロスしたパケットをたくさん示すことができるようになっていたりします。詳しく見ていきましょう

パケットとアプリケーションデータ

TCPではシーケンス番号を使用していました。

QUICではシーケンス番号の持っていた役割をパケット番号とオフセットに分離しています。すべてのQUICパケットにパケット番号がついており明確に1つずつ増えていきます。パケット番号は明確に送信者がパケットを送信した順番を示します。

それとは別にアプリケーションデータを格納するSTREAMフレームにはデータのオフセット, 長さがあり、データ上の順番を特定することができます。

そのため、ロスしたアプリケーションデータを別のパケット番号で送信することができます。
f:id:ASnoKaze:20190704001213p:plain

Ack自体はパケット番号ごとに行うので、パケロスと判断されたパケットに含まれていたデータを送り直してやれば大丈夫です。また「パケットNが届いてないというAck」に対してAckを返してやれば、確かにロスしたことは伝わったから再送がされるだろうということになります。

ACKフレーム

QUICのAckはACKフレームを送信することで行われます。ACKフレームは受信したパケット番号レンジと、受信できていないパケット番号レンジを複数表現することができます。

ACKフレームは下記のような構造をしています。
f:id:ASnoKaze:20190704010950p:plain

  • Largest Acknowledged: ACKフレームを作成するまでに受信した最大のパケット番号
  • ACK Delay: Largest Acknowledgedで示されるパケットを受信してから、このパケットを送信するまでのディレイ(ms)
  • ACK Range Count:このACKフレーム内のGapフィールドとACKレンジの個数
  • First ACK Range: Largest Acknowledgedに続いて受信したパケットの数を示すACK Range(後述)
  • ACK Ranges: 0回以上のGapとACK Range(後述)

ACK Rangesの構造は下記の通りになります。下記のように複数のGapとACK Rangeのフィールドからなります。
f:id:ASnoKaze:20190704012648p:plain

  • Gap: 前のACKレンジに続いて、受信できていないパケットの数
  • ACK Range: 前のGapに続いて、受信したパケットの数

このように一つのACKフレームで、受信できたレンジと、受信できてないレンジを表現することができます。

f:id:ASnoKaze:20190704014232p:plain
例えば、パケット番号1 ~ 110, 120 ~ 130までのパケットを受信したことを相手に伝えるACKフレームは

  • Largest Acknowledged: 130
  • ACK Range Count: 1
  • First ACK Range: 10 (パケット番号120~129)
  • Gap: 8 (パケット番号111~119 から1引いたものを表記する)
  • ACK Range: 109 (パケット番号1~110 から1引いたものを表記する)

ACKフレームをいつ送信するかについて説明していきます。

  • ACKすべきパケット(ACKのみPADDNGのみ以外のパケット)を受信した際に、max_ack_delay(デフォルトで25ms)以内に送信すべきです。
  • 受信したパケット番号の最大値+1以外のパケットを受信した場合はすぐにACKフレームを送信すべきです。
  • ただし、ACKフレームを送信する前に複数のパケットを処理してもよいです(個々別にACKしないにする)

ロス検出

QUICでもAck情報とタイムアウトを使って、TCPのFast Retransmit [RFC 568]、Early Retransmit [RFC 582]、FACK [FACK]、SACK損失回復[RFC 6675]、およびRACK [RACK]に則りパケットロス検出を行います。

基本的には

  • 3パケット以上の順番の入れ替わり (kPacketThreshold == 3 は仕様で推奨されている)
  • 1パケット以上の順番の入れ替わりがあり、9/8 * max(SRTT, latest_RTT) 以上経過している

また、QUICでは、Ackが送られてこない場合のRTO(Retransmission Time Out)と、データ末尾の再送を促すTLP(Tail Loss Probe)のタイマーを一つにまとめたPTO(Probe Timeout)を持つ。PTOの計算式は下記のとおりである。

pto = smoothed_rtt + max(4*rttvar, kGranularity) + max_ack_delay

もしPTOタイマーが切れた場合、もし送信するデータが有ればそれを送信するか、ロスしたと思われるSTREAMのデータをプローブとして送信します。これにより、ACKフレームを送信させることができます。

連続したPTOタイマーは指数関数的増えていき、送信頻度は少なくなってきます。

今回書かなかったこと

結構奥が深い

  • crypto context毎にパケット番号空間が違うので、異なるcrypto contextに対してACKすることはできなくなっています
  • ハンドシェイク時に使用するCRYPTOはより積極的に再送されます
  • RTTの測定アルゴリズム (仕様を参照)
  • QUICはECN (Explicit Congestion Notification)をサポートしてます
  • 輻輳制御について。仕様上は、輻輳制御はTCP NewReno です。Cubic やその他のアルゴリズムを使用しても良いです。

処理中のPOSTリクエストを別のサーバで引き継ぐPartial POST Replayについて

なんらかの理由でWebサーバを停止する場合に、処理中のPOSTリクエストをそのまま別のサーバで引き継げるようにする「HTTP Partial POST Replay」という仕様がFacebookAlan Frindell氏から提出されています (HTTP Workshopの資料はこちら)。

スポットインスタンスを利用していたり、サーバの設定を変えて再起動したい場合、新しいリクエストは受け付けないようにし、すでに来ているリクエストのみ処理をするのは一般的です。それでも大きなファイルをアップロードしているPOSTリクエストは処理が終わるまで時間がかかってしまう場合がありあります。

やむをえずPOSTリクエストの処理を中断してしまうと、ユーザは再度大きなファイルをアップロードしなおす必要があり、とてもストレスがかかります。

HTTP Partial POST Replay」では、ユーザの接続を切ることなく別のサーバでPOSTリクエストを引き継ぐことができます。

Partial POST Replay

ひとことで「HTTP Partial POST Replay」の説明をすると、POSTリクエストを処理しているWebサーバがリバースプロキシまで処理中のリクエストを差し戻し、別のサーバで改めて続きを処理します。そのため、構成としてはPartial POST Replayに対応したリバースプロキシがいる前提となります。

クライアントとProxy間の接続は切断することなく、処理を引き継ぐことができます。

流れ

f:id:ASnoKaze:20190630185717p:plain
処理の引継ぎを行うWebサーバはPOSTリクエストに対して「3xx Partial POST Replay」(ステータスコードは未定)を返します。このHTTPレスポンスには、Webサーバが引き継ごうとしているHTTPリクエスト情報が格納されています。リクエストヘッダと疑似ヘッダは、echo-というプレフィクスをつけてレスポンスヘッダに格納されます。POSTしていたデータはレスポンスボディに格納されています。

このレスポンスを受け取ることで、Proxyは元のリクエストと現時点でPOSTされているデータを復元することができます。

f:id:ASnoKaze:20190630191627p:plain
Proxyは復元できたPOSTリクエストを別のWebサーバに割り振りなおします。このときクライアントとのコネクションは切断することなく、送信されてくるデータはProxyがバッファリングし続けています。

また、再度同じWebサーバに割り振られるのを防ぐために、partial-post-replayリクエストヘッダをつけることで、Partial POST Replayであることを明示します。

Cross-Origin-Embedder-Policyヘッダについて

ChromeがCross-Origin-Embedder-Policy (COEP)の実装に着手しそうなので、ざっと仕様を眺める(議論段階であり、正式な仕様ではありません)
mikewest.github.io

ちょっと話が難しいので間違ってたらすみません。

なお以前は、仕様名は「Cross-Origin-Resource-Policy-Policy(CORP-P)」などとも呼ばれていた。仕様自体の議論や、名称の変遷については下記Issueを参照のこと
Opting into a CORS-only mode (Cross-Origin) · Issue #4175 · whatwg/html · GitHub

Cross-Origin-Embedder-Policy

Spectre攻撃以後、サイドチャネル攻撃を用いてメモリの内容を推測できる場合、Same-Originポリシーは十分ではないことがわかっています。つまり、ブラウザにクロスオリジンのリソースを読み込ませることに成功すれば、内容を推測できる可能性が出てきたわけです。

Cross-Origin-Embedder-Policyヘッダでは、埋め込むリソースがCross-Origin-Resource-Policyを明示するように強制します。また、そのためにCross-Origin-Resource-Policy: same-site (仕様上だとcross-originという表記もある)を指定できるように拡張します。

CORP自体は以前書いたとおりです
asnokaze.hatenablog.com


このドキュメントでは変更に合わせ、HTMLやFetchの仕様に対してモンキーパッチを当てています。

流れ

ブラウザは、example.comからhtmlファイルを読み込みます。このとき、レスポンスにCross-Origin-Embedder-Policy:require-corpヘッダがついています。これによって、サブリソースは Cross-Origin-Resource-Policyが必須になります。

サブリソースのレスポンスにCross-Origin-Resource-Policy:cross-siteとクロスオリジンでの読み込みを明示すると、ブラウザはそのリソースを読み込めるようになります。

f:id:ASnoKaze:20190623013333p:plain