Encrypted ClientHelloの仕組み
2025年09月02日 火曜日

CONTENTS
Encrypted ClientHello(ECH)は、TLS の ClientHello を暗号化するための仕様であり、現在標準化の最終段階にあります。今回は、読者の方にある程度の暗号の知識を仮定して、ECHについて説明します。
SNI とプライバシー
多くのユーザにとって、自分がどのサイトのどのサービスを利用したかは、他人に知られたくないものです。広域監視に代表されるように、インターネットに盗聴者が存在するとして、どのサイトにアクセスしたかという情報が漏れてしまう理由は、3つあると言われています。
1番目はDNSです。ユーザは通常サーバ名を使ってサーバを指定しますが、実際の通信はIPアドレスが用いられます。そこで、サーバへの通信を始める前は、まずDNSサーバに問い合わせをして、サーバ名をIPアドレスに変換します。盗聴者がDNSのトラフィックを盗み見れば、サーバ名が分かってしまいます。幸いなことに、「暗号化されたDNSサーバの探索」で説明したように、DNSの通信は暗号化され始めています。
2番目はIPアドレスです。IPパケットの宛先のIPアドレスが知られると、どのサーバにアクセスしているのかが漏れてしまいます。しかし、現在は1つのIPアドレスで、多数のサイトをホスティングしていることが一般的なので、この弱点はそれほど問題視されていません。
3番目はTLSのSNI(Server Name Indication)拡張です。以下の図のように、1つのIPアドレスで、多数のサイトをホスティングしているシステムにおいて、TLSの終端サーバを公開サーバと呼ぶことにします。実際のコンテンツを配布する(TLSの機能を提供しない平文の)サーバをバックエンド・サーバと呼びましょう。

Webホスティング
公開サーバがバックエンドのサーバへ通信を振り分けるには、TLSレベルでサーバ名を通知する必要があります。この役割を担うのが、SNIです。TLS 1.3では、多くの部分が暗号化されていますが、SNIは暗号化されていません。以下に、TLS 1.3 のフルハンドシェイクを図示します。

TLS 1.3のフルハンドシェイク
細かい説明は省略しますが、内側が白色の四角は暗号化されておらず、灰色は暗号化されていることを表します。灰色の濃さが途中で変わっているのは、鍵が変更されるからです。SNIは、白色の ClientHello によって運ばれます。よって、盗聴可能です。
ClientHelloには、もう一つプライバシー性の高い情報としてALPN(Application-Layer Protocol Negotiation)拡張があります。これは、TLSのハンドシェイクが完了した後に、どのようなアプリケーション・プロトコルが使われるかという情報です。HTTP/2なら “h2”、WebRTC なら “webrtc” という文字列が使われます。
クライアントは、ClientHelloの中で1つ以上のALPNの値を提案し、サーバは選んだ値を EncryptedExtensions に入れて返します。その名前や図から分かるように、EncryptedExtensionsは暗号化されています。盗聴者には、サーバが何を選んだかは知られませんが、何が提案されたのかは分かってしまうのです。
ECHは、ClientHello全体を暗号化するための仕組みです。そのため、SNIやALPNが盗聴されることを防止できます。ECHの仕様はかなり大きく、実際に実装している最中は、あまりの難しさに、「SNIだけ暗号化すればよかったのでは?」と何度も思いました。この記事では、SNIだけを暗号化する考察から始めて、それでは不十分で、結局 ECH が必要である理由を説明したいと思います。実際のIETFでの標準化も、そういう過程をたどりました。
SNIを公開鍵暗号で暗号化
ClientHelloとServerHelloの主要な役割の一つは、鍵を交換し、第三者に知られていない秘密の鍵を共有することです。鍵を共有する前のClientHelloを暗号化するには、公開鍵暗号を使うしかないでしょう。
そこで、まずクライアントは、サーバのRSAの公開鍵をあらかじめ手に入れているとします。手に入れる方法は、たとえばDNSです。サーバ名で TXT RR (Resource Record)を引くと、そのサーバのRSA公開鍵が手に入るとします。
RSAの公開鍵が手に入ったら、SNIを暗号化し、それをClientHelloに入れて、サーバに届けます。(通常は共通鍵暗号と組み合わせますが、このハイブリッドな方法は、次の節で導入します。)
これで十分ですね。なぜ今まで、こんな簡単なことが標準化されていなかったのでしょうか?実は、この方法にはとても簡単に実行できる攻撃方法があるのです。その方法は、RFC8744では「カット&ペースト攻撃」と呼ばれていますが、個人的にはコピー&ペースト攻撃と呼ぶのが相応しいような気がします。
攻撃方法の手順を以下に示します。
- クライアントが、バックエンド・サーバのSNIをその公開鍵を使って暗号化し、そしてClientHelloに格納し、公開サーバへ送信することで、TLSコネクションを張ろうと試みます。
- 盗聴者は、この通信を盗み見て、「暗号化されたSNI」をコピーします。
- その盗聴者は、自分自身がクライアントとなって、その公開サーバへTLSコネクションを張ります。このとき、盗んだ暗号化されたSNIをClientHelloの中にペーストしておきます。
- このとき公開サーバは、「暗号化されたSNI」を自分の秘密鍵で復号できてしまいます。平文のSNIが手に入るので、公開サーバは、SNIに対応するバックエンド・サーバの証明書をCertificateに入れてクライアントに送り返します。
- 盗聴者はCertificateを復号できます。なぜなら、盗聴者と公開サーバがTLSで交換した鍵で暗号化されているからです。盗聴者は、証明書にある情報から、ターゲットのクライアントが、どのバックエンド・サーバに接続したのかを知ります。
カット&ペースト攻撃が成立するのは、ClientHelloと「暗号化されたSNI」が結びついていないためです。
ClientHelloと結びつける
ClientHelloと「暗号化されたSNI」を結びつけるには、どうすればいいでしょうか? 現在では、共通鍵暗号のAEAD(Authenticated Encryption with Associated Data)モードを使うのが一般的です。AEADの仕組みを以下に示します。

AEADの暗号化と復号
AEADの暗号化も復号も、入力が4つで、出力が2つあります。入力の nonce は、initial vector と言う方が分かりやすい人もいるかもしれません。出力の認証タグですが、復号側は、暗号文にくっついてくる認証タグと、復号の結果得られた認証タグを比較することで、改竄がないことを検証できます。
AEADでは、平文と認証データ(associated data)を結びつけることができます。そこで、平文としてSNI、認証データにClientHelloを使います。共通鍵は、RSAを使うなら乱数を生成して使うことになりますし、Diffie-Hellman系の鍵交換を使うようにプロトコルを改変して、TLSの鍵交換とは別に得てもよいでしょう。
盗聴者が、コピーした「暗号化されたSNI」を利用した場合、復号の入力となる認証データ(ClientHello)が異なりますので、改竄されたことを検知できます。これで完璧ではないでしょうか? しかし、まだ攻撃者につけ入る隙を残しています。ECHの仕様の10.12.1節で説明されているクライアント・リアクション攻撃は以下の通りです。
- クライアントがTLSコネクションを張り始めます
- 攻撃者は、ClientHelloを横取りし、公開サーバに成りすましてハンドシェイクを継続します。このとき、「適当」なCertificate(証明書)を返します。 ClientHelloを横取りしTLSの鍵交換を実行したので、EncryptedExtensionsやCertificateは、クライアントが復号できるように暗号化されます。
- クライアントが証明書をパースして、サーバ名が異なると判断したら、コネクションを切ります。このため、クライアントがそのサーバにアクセスしたのではないことが漏洩してしまいます。
これはタイミングに依存する微妙な攻撃です。CertificateVerifyの検証の結果、コネクションを切ったのではないと判断する必要があります。CertificateVerifyとは、サーバの秘密鍵を使って生成された署名です。攻撃者は、サーバの秘密鍵を持っていないので、正しい署名は作り出せません。不正な署名を検証する訳ですが、公開鍵暗号の処理なので、ある程度時間がかかります。ある程度の時間がかかってコネクションを切られた場合と、区別する必要があるのです。
この節で考察したような高度な仕組みを作っても、暗号化できるのはSNIのみですし、しかもまだ攻撃者に付け込む隙を与えてしまっています。
ClientHello 全体の暗号化
深呼吸して考え直してみましょう。SNIのみならず、ALPNなどを暗号化するには、結局 ClientHello全体を暗号化するのが一番でしょう。すると、暗号化されたClientHelloは、もう一つのClientHelloへ拡張として収められるはずです。外側のClientHelloに中に、内側のClientHelloが暗号化されて格納されているという二重構造になります。
攻撃者は、外側のClientHelloを盗めばクライアントと暗号路を作り出せるでしょう。しかし、内側のClientHelloは復号できないので、内側のClientHelloを使った暗号路は作り出せません。(具体的には、内側のClientHelloの中にある乱数を知り得ないためです。)
あれ? ということは、内側のClientHelloを使った暗号路で、バックエンド・サーバの証明書を返すような仕組みなら、攻撃者は手が出なくなりませんか?これが ECHの核心となるアイディアです。
ECHでのクライアントとサーバの関係を以下に図示します。

ECHを使ったWebホスティング
公開サーバは、外部のClientHelloを処理します。外部のClientHelloに格納されているSNIの値は、公開サーバの名前です。公開サーバは、バックエンド・サーバの秘密鍵を持っているので、内部の暗号化されたClientHelloを復号できます。バックエンド・サーバは複数個あるので、複数個の秘密鍵を持っています。適切な秘密鍵を選択する際には、「設定ID」という数値が用いられます。設定IDは衝突しているかもしれないので、候補すべてで復号を試みます。
復号された内部のClientHelloには、バックエンド・サーバの名前が指示されたSNIが格納されています。復号に成功したら、公開サーバは内部のClientHelloをバックエンド・サーバへリレーします。ECHでは、成功時にTLSを終端するのは、バックエンド・サーバであり、公開サーバは、以降、単なるパケット・リレー・サーバとして働きます。
なお、現在のホスティング・システムをあまり変更せずにECHを導入するには、リレーを担うECHサーバを現在の公開サーバの前に設置する(分担モード)か、公開サーバが外部と内部のClientHelloの両方を処理してリレーしない方法(共有モード)を取るかのどちらかでしょう。しかし、それだと用語が混乱してしまうので、この記事ではバックエンド・サーバがTLSを終端することにしています。
ECH の失敗時
公開サーバが、ECH拡張の復号に失敗したら、どうすればいいでしょうか? 個人的には、分かりやすい成功時よりも、失敗時の方がECHの肝であると感じています。
ECH拡張の復号に失敗したら、公開サーバは何事もなかったように、外部のClientHelloを用いて、クライアントとTLSコネクションを張ります。そして、クライアントだけが分かる方法で、ECHに失敗したことを伝えます。
ECHに失敗した際、クライアントは、確立した暗号路を用いて、アラートを送りコネクションを切ります。そのため、盗聴者は何が起こったのか理解できません。
また、オプションで、最新の公開鍵が送られてくることがあります。その場合、クライアントは最新の公開鍵を使って、再びECHを使ったTLSを試みます。
ECHの導入に向けて
上記、RFC8744では、SNIの暗号化手法を比較するための基準の一つとして、「目立たない」(Do not stick out)を挙げています。現在は、ECH導入の初期ですから、インターネットのトラフィックを観察すると、ECH拡張を格納しないClientHelloの方が多いでしょう。ECH拡張を含むClientHelloは、目立ってしまうように思います。
これを緩和するアイディアが、グリーシング(greasing)です。グリーシングとは、「知らない場合にエラーを起こさずきちんと無視する」かテストするための出鱈目な値のことです。ECHに対応したクライアントは、対象となるサーバがECHに対応していない場合、出鱈目な値を持つECH拡張をClientHelloの中に格納します。
こうすれば、正しい値か、出鱈目な値かに関わらず、徐々にECH拡張を持つClientHelloが増えて行き、目立たなくなるでしょう。また、対象となるサーバがECHに対応し公開鍵をDNSで公開したときは、出鱈目な値が、正しい値に置き換わることになりますが、これも目立たない変化と言えるでしょう。
SVCB/HTTPS RR との関係
ECHのバックエンド・サーバの公開鍵、正確には、設定IDなども含む「ECH設定」は、SVCB/HTTPS RR(Resource Record)の ech
パラメータを使って公開されます。dug を使って、実際の例を見てみましょう。
% dug research.cloudflare.com https
...
research.cloudflare.com. 300(5 mins) IN HTTPS 1 . alpn=["h2"] ipv4hint=[104.18.4.139,104.18.5.139] ech=[(4, DHKEM(X25519, HKDF-SHA256), "9976bac08ccc8aa8fb21ad21fd62898243a17a8ad732c7bf69ef0c2ea7635022", [(HKDF-SHA256,AES-128-GCM)], 0, "cloudflare-ech.com", [])] ipv6hint=[2606:4700::6812:48b,2606:4700::6812:58b]
...
ech
パラメータの値の内、16進数表記の長い文字列が公開鍵です。
この記事の初めの方で説明した TXT RR で公開する方法だと、別途バックエンド・サーバの A/AAAA RR を検索しなければなりません。これだと検索が2回になりますし、TXT RR と A/AAAA RR の対応が、実際には存在しない組み合わせになることがあります。
SVCB/HTTPS RRでは、IPアドレスとECHの設定を一度に検索できますので、存在ない組み合わせになることはありませんし、検索の回数も一回で済みます。
過去二回に渡り、SVCB RRについて説明してきました。「暗号化されたDNSサーバの探索」と「SVCB RRでよく使われる3つのポイントを紹介」は、この記事を書くための伏線だったのです。
おわりに
ブラウザのいくつかでは、すでにECHの機能が有効にされています。サーバ側は、Cloudflareのホスティングを中心に対応が始まっています。