dorapon2000’s diary

忘備録的な。セキュリティとかネットワークすきです。

HTTP/2のTCPレベルのHoLブロッキングとQUIC

HTTP/2はHTTP/1.1で問題だったHTTPレベルのHoLブロッキングを解消しました。しかし、TCPレベルのHoLブロッキングという問題が依然として残っています。。。。。。🤔🤔

ぐぐるとたくさん解説サイトが出てきますが、いろいろ調べて、自分の言葉に落とし込んでみます。

概要

  • HTTP/1.1にはHTTPレベルのHoLブロッキング問題がある
    • HTTP/2で解決する
  • HTTP/2にはTCPレベルのHoLブロッキング問題がある
    • HTTP/3で解決する
  • HTTP/3ではTCPを使わずに、HTTP/2におけるストリームを別々で管理する

HoLブロッキング とは

f:id:dorapon2000:20210411070906p:plain

HoL (Head of Line) ブロッキングとは、待ち行列ができている中で先頭の客しかサービスを受けられない場合、残りの人が待たされなくてはいけないことを指します。

コンビニで例えるなら、レジにお客さんが並んでいるとして、精算中の人以外の人はスマホをいじりながら待つ必要があります。そんな中、精算中の人がお金が足りないなどと言ってATMに駆け出したら、いらっとくるわけです。このような状況がHTTPでも同じように発生します。

HTTP/1.1

HTTPコネクションを張るということはTCPコネクションを張るということです。ここでの、コネクションという単語はどちらの意味でも取ることができます。

特徴

f:id:dorapon2000:20210411135230j:plain

  • KeepAliveすることにより1つのコネクションで複数のファイルを送信可能。
    • ただし単一コネクション内では並行には送れない(HoLブロッキング
  • コネクションは一般に1つのドメインに対して並列に6つまで張ることができる
    • HTTPの仕様ではなく、一般的なブラウザによる制約
  • 6つ以上張りたい場合はドメインを複数用意する(ドメインシャーディング)
    • 2つのドメインを用意すれば、最大12個のファイルを並列に送信できる

HTTPレベルのHoLブロッキング

1つのHTTPコネクションだけに注目します。

http://www.plantuml.com/plantuml/png/SoWkIImgAStDuKegoYylJYrIqBLJ059bG9QLWhjhY4AYeLv1NZeNI0miMw40Ksc58OSXXTjK8rmwOL8EgNafG7S10000

GET a1.pngGET a2.pngGET c3.png の順に、HTTPリクエストがクライアントの送信キューで順番待ちしているとします。a1.pngのレスポンスがどこかで損失した場合、サーバは再送しますが、その間GET a2.png以降のリクエストを送ることはできません。具体的には、a1.pngは複数のTCPパケットに分割して送られており、そのうちの1つ以上が損失するとこのような状況になります。

HoL=GET a 1.pngが、GET a2.png以降のHTTPリクエストをブロッキングしています。

HTTP/2

HTTP/1.1のコネクションが、HTTP/2ではストリームにあたります。そして、HTTP/2では単一のTCPコネクションで複数ストリームを扱うことで並行にファイルを送信します。

HTTP/2の説明は詳解HTTP/2をとても参考にしています。

特徴

f:id:dorapon2000:20210411170325p:plain

  • 単一のTCPコネクションを使う
    • 1つのTCPパケットに複数ストリームのバイナリが含まれる可能性もある
  • 100以上のストリームでファイルを並行に送信可能
    • 同時に流せる最大ストリーム数はブラウザやサーバによって異なるがapacheは100
    • ファイルごとにストリームが生成される(再利用不可)
    • HTTP/2が出た今、ドメインシャーディングは非推奨*1

TCPレベルのHoLブロッキング

f:id:dorapon2000:20210411170329p:plain

HTTP/2でストリームを謳っても、TCPの上で成り立つ以上、MTUのサイズに整えられたTCPパケットが送信されます。そのパケットが損失すれば、TCPの機能で再送と順序制御がされます。ここがネックなのです。

上図では、ストリーム1だけを含むパケットが損失したため再送されます。その再送パケット以外をブラウザが受け取ったとして、ストリーム2とストリーム3のデータはすべて揃ったのだから、緑とオレンジの画像は表示されると期待してしまいます。

しかし、実際はTCPはシーケンス番号通りの順にしか処理できないため、4つ目のパケットをキューに入れて、3つ目のパケットが来るまで待ちます。キューに入っている間はHTTP側はストリーム3のデータを取り出せないため、オレンジの画像も表示できないです。

HoL=順序が入れ替わったパケットが、後続のパケットの処理をブロッキングしています。

HTTP/2の欠点

ここから見えてくるのは、HTTP/1.1と比べてHTTP/2がパケットロスに弱いという性質です。HTTP/1.1では独立したTCPコネクションを張っていたため、パケット損失はそのコネクションにしか影響がありません。HTTP/2は後続のすべてのストリームに影響があります。

具体的には、2%のパケロスでHTTP/1.1のほうが性能がよくなるようです*2*3。ただ、2%のパケロス環境も相当なようで、現実ではそこまでネックにはならないみたいです。

HTTP/3のQUIC

TCPレベルのHoLブロッキング問題を改善しているのがHTTP/3です。HTTP/3はほぼHTTP/2+QUICの組み合わせを指すため、QUICが肝になります。

QUICの特徴

UDP上で構築されていますが、トランスポート層プロトコルに相当します。

  • TCPと同じような動作をする
    • コネクション志向
    • 再送制御
    • 輻輳制御
  • 複数のストリームを扱うことができ、各ストリームごとに制御することができる
    • アプリケーション層のHTTP/2のストリームの概念をトランスポート層まで落としてきたといえる
    • 特定のストリームのパケット損失は他のストリームに影響がでない

TCPだから問題だったHoLブロッキング問題を、TCPを使わないことで解決しています。大胆ですね汗。

まとめ

L4〜L7のことを調べていると、面白くて無限に文献を漁ってしまいます。QUICはすごいし名前も速そう!

参考文献

example.comのTLS1.3ハンドシェイクを見てみる

example.comのTLS1.3ハンドシェイクを覗いてみます。

この記事を書くために、TLS1.3について別記事にまとめました。

dorapon2000.hatenablog.com

通信を覗く方法

下記のサイトのやり方でexample.comと通信するchromeの通信をWiresharkで覗きました。

【解析/Wireshark】https(SSL/TLS)を復号化する方法(SSLKEYLOGFILE),ブラウザで見る方法 | SEの道標

TLSハンドシェイク

以下のスクショは、実際にキャプチャしたTLSパケットです。

f:id:dorapon2000:20210406004132p:plain

見やすくシーケンス図にすると次のようになります。[]は暗号化済みのメッセージで外部からは中身を見ることができません。

http://www.plantuml.com/plantuml/png/hOzFIyD04CNl-HHp3ofux44AeTA3Y9ZWfUIm9A_TeVjddPrey-rTh8UA13pqCZ3pVeyViyvgH2VlgeF4AL7tr98rOSpM754rLg87tJmGJiOdXziXUbfHunX1QGldOdKP5VAdipgdz0gLkSpt2LcfBh491r33oKAeJHYMBNzGgl-dRY4Va3DKEhjD66HEYf4s9qLGZg5VBEyQYFAU1wFegVwrqnk4z_Ft-P43PuknNthW4a4ObuNF-AGMENCCrF5mXFPNWhTTzt9ttrVhuf-y-m80

翻訳版。

http://www.plantuml.com/plantuml/png/SYWkIImgAStDuKeloYyjK0Zn2LR8ICnBASv8p4xbSiueoizDLT2rKqYjICmjo4dLIyxFLR1I03HB1Za9Wq4KtdCgxu7iN7C6tZ-A8L8HZGfYn0QbW8Oc6YDAQm_p-jTsnHKS4IEI_AxTjJFJzxutK-A4c5BQb375fqGq2H4KjOiqyMqhp2LUHFtjgcruElXLrZwDeMaG0w9VU4z4az3A0T4cnZHpYj0yKmBAewrxnPcs0iBJ6TKhhLCer0fFUQH6MHgW-mTqXqO8uw9togUlzJmOi8RoTv8xc-pbGn2JJHbpdCrqu9nuKvYAKxESJxny5ArBwMlMESnwCsBq7Z_O8urjVgavivMpcqlzt4wRohg8eBxL7RTtTk_RtbKwloswd_VPM3mH_gp8l512QZPfQBOqJfRN6n8jufeu9NOh_y28ihywWFkXKjhIhUY__W40

Clinet Hello

Client HelloはサーバにTLS通信の開始を伝えるわけですが、暗号スイート以外の必要な情報は各拡張機能にいれておきます。

思ったより拡張の数が多かったので1フィールド+6拡張に絞って見ていきます(あんまり絞れていない)。

  • 利用できるTLSのバージョン(supported_versions)
  • 利用できる暗号スイート(Cipher Suitesフィールド)
  • 利用できる鍵交換アルゴリズムの提示(supported_groups)
  • 通信予定の鍵交換アルゴリズムのパラメータ(key_share)
  • 利用できる署名アルゴリズムの提示(signature_algorithms)
  • 今後PSKの交換をするかどうか(psk_key_exchange_modes)
  • 過去の通信で交換したPSK(pre_shared_key)

f:id:dorapon2000:20210407040902p:plain

supported_versions拡張

クライアント(ここではChrome)が対応しているTLSのバージョンを格納します。

f:id:dorapon2000:20210407015424p:plain

TLS1.0~1.3まで対応しているようです。もちろんクライアントとしては新しいバージョンで通信してほしいわけです。

Cipher Suitesフィールド

クライアントが対応している暗号スイートを格納します。TLS1.3はTLS1.2までとフォーマットが異なり、暗号化アルゴリズム共通鍵暗号)の部分だけが示されています。詳しくは僕の前の記事に書いています。

※Aはアルゴリズムの略です。

TLS1.2: TLS_鍵交換A_署名A_WITH_(暗号化A_鍵長_暗号利用モード)_ハッシュ関数
TLS1.3: TLS_(暗号化A_鍵長_暗号利用モード)_ハッシュ関数

f:id:dorapon2000:20210407041005p:plain

なるほど~。TLS1.2以前とTLS1.3の暗号スイートが混ざっています。このClient HelloがTLS1.3として解釈されたときは上の3つが、そうでないときは下のものが利用されるというわけですね。TLS後方互換性に配慮していることが見て取れます*1

ちなみにVersionフィールドがTLS 1.3ではなくTLS1.2なのも後方互換性のためです。TLS1.3では本当はVersionフィールドを消したかったみたいです*2

supported_groups拡張

クライアントが対応している鍵交換アルゴリズムを格納します。メッセージの暗号化に必要な鍵を交換するために使います。

f:id:dorapon2000:20210407021111p:plain

ECDHEのうち3つの楕円曲線に対応しているようで、逆に、DHEは提示していません。

x25519はTLS1.3から導入されたアルゴリズムで、新しいためまだ未対応のケースが多いようですが、さずがChrome様。対応済みです。

key_share拡張

TLS1.2では、鍵交換アルゴリズムを決定後に、アルゴリズムの計算に必要なパラメータを別の通信で送ります。TLS1.3では、そのアルゴリズムが使われるかわからないのに、Client Helloの段階で送ってしまいます。そうすることで、あわよくば通信に必要な往復を削減できるのが特徴です。

f:id:dorapon2000:20210407022200p:plain

ここでは、x25519が使われると想定して、そのパラメータを格納しています。どうなるでしょうか。

signature_algorithms拡張

クライアントが対応している署名アルゴリズムを格納します。サーバの認証に必要です。

f:id:dorapon2000:20210407022721p:plain

ECDSAとRSAに対応しているようです。鍵交換アルゴリズムでx25519に対応しているので、その署名アルゴリズムverのed25519にも対応していそうなものですが、提示されていませんね。謎です。

psk_key_exchange_modes拡張

TLS1.3ではセッションの再開にPSK(Pre-Shared Key)を使えます。PSKの共有方法は、前回のセッション中に交換するか、TLSスコープ外(手渡しとか?)で交換するかです。psk_key_exchange_modes拡張では、PSKの交換に対応しているか、PSKで接続を確立する方法を格納します。

f:id:dorapon2000:20210407023419p:plain

対応していました。PSKのみのセッション再開ではPFS(同じ鍵で過去の通信まで復号できないこと)がないため、PSKとECDHEを使ったハイブリッドな鍵交換でPFSを保証できるpsk_dhe_ke方式が指定されています。

pre_shared_key拡張

PSKが格納されています。キャプチャする前に試しにアクセスしているため、そのときに交換したものだと思われます。

f:id:dorapon2000:20210407052229p:plain

Client Helloまとめ

Chrome「サーバーさんサーバーさん

  • TLS1.3に対応してるよ!
  • こんな暗号スイートに対応してるよ!(雑)
  • x25519で鍵交換しようよ!
    • 無理でもsecp256r1とsecp384r1に対応してるよ!
  • RSAとECDSAで署名の検証ができるよ!
  • PSK対応してるよ!
  • PSK持ってるよ!

Hello Retry Request

クライアントが提示した鍵交換アルゴリズムのパラメータに対応していなかった場合、このメッセージでやり直しをさせます。

f:id:dorapon2000:20210407032011p:plain

Chromeはx25519のパラメータを送っていましたが、サーバはsecp256r1のものがほしかったようです。

Change Cipher Spec

TLS1.2では暗号スイートの変更の際に利用されたメッセージですが、TLS1.3ではHello Retry Requestがあるため不要なはずです。なぜChange Cipher Cpecを送信しているのかはよくわかりません。

  • The server sends a dummy change_cipher_spec record immediately after its first handshake message. This may either be after a ServerHello or a HelloRetryRequest.

https://tools.ietf.org/html/rfc8446#appendix-D.4

ミドルボックス互換性のためのようです。

2度目のClient Hello

Chromeは署名アルゴリズムのパラメータとしてsecp256r1のものを送って再挑戦します。

Client Helloが更新された際のPSK

In addition, in its updated ClientHello, the client SHOULD NOT offer any pre-shared keys associated with a hash other than that of the selected cipher suite.

https://tools.ietf.org/html/rfc8446#section-4.1.4

Hello Retry Requestが送られたあとはPSKによるセッションの再開ができない可能性があるようです。

今回はそのパターンのようで、セッション再開されていればやり取りされないはずのCertificateとCertificate Verifyがあります。

Server Hello

クライアントが提示したアルゴリズムリストから1つづつアルゴリズムを選んでクライアントに教えます。 よく考えるとHello Retry Requestと同じ事をしており、実際に中身もほぼ同じなので省きます。

Encrypted Extensions

鍵交換/署名/暗号化アルゴリズム同意後であれば、それ以外の情報はもう暗号化して隠せます。なのでTLS1.3では、Server Helloとは別でEncrypted Extensionsがあります。

f:id:dorapon2000:20210407034021p:plain

ALPNはHTTP通信のバージョンの合意をとるプロトコルで、クライアントがh2(HTTP/2)で通信したいと提案してきたので、対応しているサーバもh2を返しています。

f:id:dorapon2000:20210407034522p:plain

こちらがClient Hello内にあったALPNプロトコルです。

Certificate

サーバの認証に必要な証明書チェーンが格納されています。

f:id:dorapon2000:20210407044610p:plain

ブラウザからも見られます。

f:id:dorapon2000:20210407044810p:plain

Certificate Verify

サーバの認証に必要なデジタル署名が格納されています。証明書チェーンとセットで使われます。

f:id:dorapon2000:20210407044858p:plain

署名アルゴリズムrsa_pss_rsae_sha256が指定されていました。

Finished

クライアントもサーバもハンドシェイクの最後はFinishedを送り、今までのやりとりが改ざんされたものでないか検証をします。

TLS Session Ticket

PSKの計算に使うチケットをサーバ側から任意のタイミングでクライアントに通知します。Client Hello時のpsk_key_exchange_modes拡張で、クライアントがPSKに対応していることをサーバは確認済みです。

今回は連続で2つのチケットを送信していますが、なぜかはわからないです。

まとめ

1つ1つ調べるのが大変でしたが、TLS1.3について理解が進みました。別のサイトについてモチベーションが保てていれば見ていきたいと思っています。

参考

*1:https://tools.ietf.org/html/rfc8446#appendix-D

*2:PDF版プロフェッショナルSSL/TLS 付録A

TLS1.2と1.3での鍵交換/認証/暗号化に関する用語をまとめる

TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256

TLS1.2での暗号スイートの一例ですが、なんでこんなにいくつもアルゴリズムを使う必要があるの?と思うわけです。しかも、ECDHEとECDSAとEdDSAなど用語が似ていたり、RSAは鍵交換と認証を両方やってしまったり、勉強している中でとても混乱してしまいそうです。

TLS1.2とTLS1.3の違いも含めて、自分なりにまとめて整理していきます。間違いがあればご指摘いただけると嬉しいです。

TLSで行われていること

以下の4つの視点でまとめていきます。仮にクライアントAがサーバBと安全ではない経路を使って安全に通信したいとします。

  • 鍵交換
    • Aは暗号化用の鍵を誰にも知られずBに伝えたい
  • 認証(デジタル署名)
    • AはBが本当にB本人であることを知りたい
  • 暗号化
    • Aは長いメッセージを誰にも知られずBに伝えたい
  • 完全性の検証
    • AはBから受け取った返信が改ざんされていないことを確認したい

TLS1.2の適当な暗号スイートを上記4つに当てはめると以下のようになります。

f:id:dorapon2000:20210404205740p:plain

なお、TLS1.3で暗号スイートと言うと「TLS_AES_128_CCM_SHA256」のように少し短くなっており、暗号化以降の部分が抜き出された形になります。TLS1.2では暗号スイートとして一緒くたになっていましたが、TLS1.3からそれぞれ別々に合意が取られます。

暗号化・完全性の検証

一番とっかかりやすいところは暗号化だと思います。共通鍵暗号で秘匿したいメッセージを暗号化するためのアルゴリズムです。

種類 TLS1.2 TLS1.3*1
AES ブロック暗号 o o
3DES ブロック暗号 o x
RC4 ストリーム暗号 o x
ChaCha20 ストリーム暗号 x o

最も主流なのはAESで、TLS1.3でも引き続き採用されています。長いメッセージをブロックという単位で区切って暗号化するブロック暗号には、暗号利用モードというバリエーションがあります。ブロックを順々に暗号化する際、前のブロックをどう活用するかを決めます。

完全制の検証 TLS1.2 TLS1.3*2
AES_XXX_CBC x o x
AES_XXX_GCM (AEAD) o o o
AES_XXX_CCM (AEAD) o o o

CBC秘匿用の暗号利用モードであり、完全制の保証には別途MAC (Message Authentication Code) の検証が必要でした。このMACの存在はBEAST攻撃など数多くの問題を引き起こすため、MACを使わないAEAD (Authenticated Encryption with Associated Data) のみがTLS1.3で許可されています。

鍵交換

次は、先のアルゴリズムでメッセージを暗号化/復号するための鍵を、安全に相手と共有する必要があります。もし、安全ではない経路中にそのまま鍵を流したりなんてしたら、攻撃者が鍵を盗み見る可能性があります。これを防止するのが鍵交換アルゴリズムです。

鍵交換アルゴリズムは小さいメッセージ(鍵)を暗号化して相手に送るので、要は暗号化アルゴリズムです。ただ、共通鍵暗号よりも安全だが計算コストが高い公開鍵暗号を利用します。

TLS1.2 TLS1.3
RSA o x
DHE o o
ECDHE o o

RSAはこれまで長らく鍵交換アルゴリズムとして使われてきました。しかし、セキュリティを担保するために必要な鍵長が1024bit→2048bit (→3072bit) と増えており、計算コストも増加してしまいます。また、RSAは鍵を盗難されると過去のメッセージもすべて復号されてしまう欠点があります(PFSがない)。なので、TLS1.3ではDHEとECDHEだけをサポートします。

楕円曲線暗号

ECDHEはDiffie–Hellman鍵交換アルゴリズム楕円曲線暗号で実現する鍵交換アルゴリズムです。RSAと比べて鍵長が短く、PFSがあります。

利用できる楕円はいくつか定義されており、その中から選びます

ECDHE 楕円曲線 TLS1.2 TLS1.3*3
secp256r1 P256 o o
x25519 Curve25519 x o
x448 Curve448 x o

楕円曲円暗号の仕組みは以下の記事がシンプルでわかりやすかったです。

qiita.com

認証(デジタル署名)

鍵が安全に交換できて、暗号通信できるけど、本当に通信相手は正しいの?PKIをベースに署名アルゴリズムによって通信相手の認証を行います。

PKI (Public Key Infrastructure) は日本語で公開鍵基盤といい、一般に信頼された認証機関(CA)がサーバが本物であることを保証するという仕組みそのもの、暗号通信のインフラのことを指します。

大雑把な認証の流れは以下です。公開鍵を取得する手順にもなります。

  1. サーバがCAから本物であることを証明する証明書を貰う(CAによる署名)
  2. クライアントに証明書を送る
  3. クライアントは証明書が本物か検証すると同時に、証明書からサーバの公開鍵を取得する

その署名アルゴリズムが以下です。

TLS1.2 TLS1.3
DSA oだが実質x x
RSA o o
ECDSA o o
EdDSA x o

なぜ鍵交換に出てきたRSAがなぜ署名にもでてくるの!?という気持ちになりますが、 RSAが復号鍵でも暗号化できるという特殊な性質だからです。TLS1.3の署名でRSAが廃止されていない理由はわからないです。ハッシュをかませるからでしょうか。しかし、鍵長が長くなるにつれパフォーマンスが落ちる問題は依然としてあります*4

ECDSAとEdDSAはどちらも楕円曲線を用いた署名アルゴリズムです。

ECDSA/EdDSA 楕円曲線 TLS1.2 TLS1.3*5
ecdsa_secp256r1_sha256 P256 o※ o
ed25519 Curve25519 x o
ed448 Curve448 x o

※TLS1.2では正確にはecdsa_secp256r1_sha256というアルゴリズム名ではないです。

まとめ

f:id:dorapon2000:20210406001145p:plain

冒頭の疑問に自分で答えます。

なんでこんなにいくつもアルゴリズムを使う必要があるの?

役割分担とマシンのスペックに応じた暗号を用意するため。

ECDHEとECDSAとEdDSAなど用語が似ていたり

ECDHEは楕円曲線を使った鍵交換アルゴリズムで、ECDSAとEdDSAは楕円曲線を使った署名アルゴリズム。EdDSAのほうが後発。

RSAは鍵交換と認証を両方やってしまったり

RSAが復号鍵でも暗号化できるという特殊な性質なため。

参考

RFC

楕円曲線

TLS1.3との違い

TLS全般

スプライトアニメーションをしながら座標移動をする

スプライト画像をアニメーションさせながら、座標移動も適応する方法に単純ながら四苦八苦したので記録に残します。

前準備

  <div class='img-container'>
  </div>
.img-container {
  width: 700px;
  height: 100px;
  margin: 100px auto 0;
  border: 1px solid #000000;
  font-size: 0;
}

f:id:dorapon2000:20210403110228p:plain

f:id:dorapon2000:20210403110237p:plain

上のボーダーで囲まれた領域を横断するように歩くスプライドアニメーションを配置したいです。png画像は実際は透過処理しています。

実現方法

多段のdivにして、親に座標移動のanimationを、子にスプライト画像のanimationを適応させることで実現できます。

  <div class='img-container'>
    <div class='animation-move'>
      <div class='animation-walk'></div>
    </div>
  </div>
/* 座標移動 */
.animation-move {
  animation: move 3s linear infinite;
}

@keyframes move {
  to {
    transform: translateX(600px);
  }
}

/* スプライトアニメーション */
.animation-walk {
  background: url(img/walk.png) no-repeat;
  width: 100px;
  height: 100px;
  animation: walk 0.3s steps(2) infinite;
}

@keyframes walk {
  to {
    background-position: -200px 0;
  }
}

f:id:dorapon2000:20210403113056g:plain

実際のコードはgithubに上げています

dorapon2000-blog/sprite_animation at main · dorapon2000/dorapon2000-blog · GitHub

座標移動させない場合

f:id:dorapon2000:20210403114847g:plain

別の画像で

f:id:dorapon2000:20210403120306g:plain