ECH(Encrypted Client Hello)
TLSで最後まで平文だったSNIをHPKEで封じ、閲覧先ドメインの盗聴・検閲を断つ仕組みを、鍵配布・拒否時リトライ・GREASEまで内部動作から追える。
- ECHは本物のSNI等を含むClientHelloInnerをHPKEで暗号化し、公開名だけのClientHelloOuterへ封入して送る。TLS1.3で唯一残っていた平文メタデータを塞ぐプライバシー拡張。
- 鍵はDNSのHTTPS/SVCBレコードでECHConfigとして配布し、config_idで試行復号を避ける。AADにOuter全体を縛り、outer_extensions圧縮でInnerを縮めて外形一致を狙う。
- 鍵不一致時はサーバーが公開名で握手を続けretry_configsを返す。受理可否はServerHello.randomの末尾8バイトの秘密シグナルで判定。非対応でもGREASE ECHで見分けにくくする。
平文 SNI という「最後の一枚」
TLS 1.3 は Certificate すら暗号化しますが、最初の ClientHello だけは共有秘密の確立前に送るため暗号化できません。そこに乗る拡張 server_name(SNI)は、接続先ホスト名を経路上へ平文で晒します。本文が暗号化されていても「誰がどのドメインへ繋いだか」というメタデータは、ドメイン単位の検閲・ブロック・プロファイリングに十分です。DNS を DoH/DoT で隠しても、直後の SNI で同じ情報が漏れれば台無しになります。
横にスクロール
ECH(Encrypted Client Hello)は、この一枚を塞ぐプライバシー拡張です。全体像は姉妹記事「Encrypted Client Hello(ECH)の二重ハンドシェイク」に譲り、本稿は HPKE の封じ方・DNS 配布・拒否時リトライ・GREASE といった内部動作の精度に踏み込みます。
ClientHelloInner を HPKE で封じる
ECH は二つの ClientHello を組み立てます。ClientHelloInner は本物の SNI やパラメータを含む本命で、ClientHelloOuter はワイヤ上を流れる封筒です。Outer の server_name には公開名(public_name、例 public.example)を置き、拡張 encrypted_client_hello に HPKE(RFC 9180)で暗号化した Inner を封入します。
暗号化は HPKE の単発 Seal(base モード)です。クライアントは接続ごとに一時鍵を生成して SetupBaseS を実行し、封筒鍵 enc(KEM の被カプセル化公開鍵)と暗号文 payload を得ます。ここで見落とせないのが info 文字列で、値は次のようにその ECHConfig 自身を織り込みます。
info = "tls ech" || 0x00 || ECHConfig
これにより、ある設定向けに作った暗号文を別設定へ流用する取り違えを暗号レベルで排除します。ワイヤ上に流れる ECH 拡張の構造はクライアント側で次のタグ付き共用体になります。
enum { outer(0), inner(1) } ECHClientHelloType;
struct {
ECHClientHelloType type;
select (type) {
case outer:
HpkeSymmetricCipherSuite cipher_suite; // KDF+AEAD の組
uint8 config_id; // 1バイト鍵識別子
opaque enc<0..2^16-1>; // HPKE被カプセル化鍵
opaque payload<1..2^16-1>; // 暗号化されたInner
case inner:
Empty; // Innerでは中身なしの合図
};
} ECHClientHello;
config_id は 1 バイトの鍵識別子で、サーバーが「どの ECH 秘密鍵で復号すべきか」を試行復号なしに選ぶために使います。Inner 側の拡張は type=inner の空値で、「これが復号後の本命だ」という内部シグナルにすぎません。
payload の AEAD 計算では、追加認証データ(AAD)に ClientHelloOuter 全体を使います。ただし循環参照を避けるため、encrypted_client_hello 拡張内の payload フィールドだけを同じ長さのゼロ列に置換した ClientHelloOuterAAD を用います。これで Outer の key_share や乱数を含む全体が暗号文に暗号学的に結び付き、途中で Outer を差し替える攻撃を検出できます。
outer_extensions 圧縮で外形をそろえる
Inner と Outer は多くの拡張(supported_versions、key_share など)を共有します。素朴に両方へ書くと Inner が肥大し、Outer との長さ差から「ECH を使っている/中身が大きい」と外形推測される余地が生まれます。そこで Inner では共通拡張を実体で持たず、ech_outer_extensions(OuterExtensions)に参照する ExtensionType の列だけを並べます。サーバーは復号後、参照先を Outer から補って Inner を再構成(デコード)します。目的は帯域節約だけでなく、Inner の符号化長を詰めてパディングと合わせ、暗号文サイズの手がかりを消すことにあります。
public_name は「露出してよいダミー」ですが無害な飾りではありません。ECH が拒否・失敗した場合、クライアントはこの公開名の証明書で TLS を確立し直します。したがって public_name は攻撃者が差し替えできない、正当な証明書を持つドメインである必要があります。ここを軽視すると、フォールバック経路が中間者の格好の的になります。
鍵配布と、拒否されたときの筋道
クライアントは接続前に ECH 公開鍵を知る必要があり、これを DNS の HTTPS リソースレコード(type 65、SVCB 系)の ech パラメータに ECHConfigList として載せて配布します。中身は ECHConfig のバージョン、config_id、HPKE 公開鍵、対応する KEM/KDF/AEAD、public_name です。取得経路自体の真正性は DNSSEC が、問い合わせの秘匿は DoH/DoT が補います。DNS が偽装されれば攻撃者の鍵で Inner を封じてしまい、ECH は成立しません。
example.com. IN HTTPS 1 . ( alpn="h2,h3" ech="<base64 ECHConfigList>" )
鍵は輪番(ローテーション)します。クライアントの持つ config_id が古いと、サーバーは復号できません。このとき ECH は接続を落とさず、Outer の public_name で普通に TLS 1.3 を完了させ、EncryptedExtensions の中に最新鍵入りの retry_configs(ECHConfig の並び)を返します。クライアントはそれを使ってその場でリトライでき、鍵交代の隙間でも接続が途切れません。
肝心なのは「サーバーが Inner を受理したか、Outer で妥協したか」をクライアントが知る方法です。ECH は専用フラグを平文で立てず、ServerHello.random の末尾 8 バイトに秘密のシグナルを埋め込みます。
accept_confirmation = HKDF-Expand-Label(
HKDF-Extract(0, ClientHelloInner.random),
"ech accept confirmation",
transcript_ech_conf, 8)
サーバーが Inner を復号できたときだけ、Inner 由来の乱数とトランスクリプトからこの 8 バイトを導出して ServerHello.random 末尾に置きます。クライアントは同じ計算で照合し、一致すれば受理、しなければ拒否と判断します。第三者からは乱数と区別できないため、受理可否そのものが観測者に漏れません。HelloRetryRequest を挟む経路では、ラベル "hrr ech accept confirmation" で同種の確認を別途行います。
サーバーが HelloRetryRequest を返すと、クライアントは 2 回目の ClientHello を送ります。このとき ECH 拡張の enc は空文字列にします。最初に確立した HPKE 暗号化コンテキストを再利用するため、被カプセル化鍵を再送する必要がないからです。config_id と cipher_suite は据え置きです。
復号する主体:スプリットモード
ECH には二つの配備形態があります。共有モードでは TLS を終端するサーバー自身が ECH を復号します。スプリットモードでは、最前段の client-facing server が ECH だけを復号し、取り出した ClientHelloInner を、実際にコネクションを終端するバックエンドへ透過的に転送します。多数のドメインを同一 IP に収容する CDN・共有基盤は後者と相性が良く、client-facing server が「隠れ蓑」となって、その背後に無数の実サイトを匿します。逆に単独運用の小規模サーバーは Outer 役の公開名終端を用意しにくく、これが配備の実務的な壁になります。
| 観点 | 従来SNI | ECH(Inner暗号化) |
|---|---|---|
| 宛先ドメインの可視性 | 平文で露出 | Outerは公開名のみ、Innerは暗号化 |
| 鍵取得 | 不要 | DNSのHTTPS/SVCBレコードから事前取得 |
| 鍵不一致時 | 該当なし | 公開名で継続しretry_configsで再試行 |
| 受理可否の判定 | 自明 | ServerHello.random末尾8バイトの秘密シグナル |
GREASE ECH:使わない接続も紛らす
ECH 非対応の接続だけが ECH 拡張を持たないと、その「無さ」自体が識別の手がかりになります。これを消すのが GREASE ECH です。実鍵を持たないクライアントも、type=outer の ECHClientHello をダミーで組み立てて送ります。具体的には config_id をランダムな 1 バイトに、cipher_suite を対応スイートの一つに、enc を選んだ KEM の正しい長さの乱数に、payload を「ECH を実際に使ったなら生じたはずの Inner 長(EncodedClientHelloInner)+ AEAD 拡張分」に等しい長さの乱数にします。狙いは二つで、未知拡張を許容しない硬直した中間装置や実装を早期に炙り出す相互運用テストと、実 ECH 接続とダミーを外形(拡張の有無・長さ)だけからは区別させないことです。GREASE は元々 IETF が TLS 拡張全般の硬直化対策として導入した枠組みで、ECH はその応用形です。
- ECH は ClientHelloInner を HPKE で暗号化し、public_name だけの Outer に封入する。info は
"tls ech" || 0x00 || ECHConfig。 - config_id(1バイト)で試行復号を回避。AAD には payload をゼロ化した ClientHelloOuter 全体を縛る。
- outer_extensions で Inner の共通拡張を参照化し、符号化長を詰めて外形の手がかりを消す。
- 鍵不一致では公開名で握手継続+retry_configs で即リトライ。受理可否は ServerHello.random 末尾8バイトの秘密シグナルで判定。
- GREASE ECH は非対応接続もダミー拡張で紛らし、硬直実装の検出と外形匿名化を同時に狙う。
一段で言うと
ECH は「本物の ClientHello を HPKE で封じ、公開名だけの ClientHello に入れて送る」プライバシー拡張です。鍵は DNS の HTTPS レコードで配り、config_id で復号鍵を素早く選び、AAD と outer_extensions 圧縮で改ざん検出と外形一致を両立させます。鍵が回っても public_name で握手を続けて retry_configs で継ぎ、受理の成否は ServerHello.random 末尾 8 バイトに秘密裏に埋め込む——この緻密さゆえに、観測者からは「ECH を使ったか、成功したか」すら見えにくくなります。検証には ECH 対応ビルドの openssl s_client -ech_config_list や対応ブラウザの開発者ツールで、拡張一覧と retry の挙動を確かめるとよいでしょう。
ネットワークの記事ガイド
ECH(Encrypted Client Hello)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ECH
比較で見る軸
難易度: advanced / カテゴリ: ネットワーク / タグ数: 6
導入後に効く点
鍵はDNSのHTTPS/SVCBレコードでECHConfigとして配布し、config_idで試行復号を避ける。AADにOuter全体を縛り、outer_extensions圧縮でInnerを縮めて外形一致を狙う。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- ネットワーク
- タグ数
- 6
判断チェックリスト
- 自社の用途が「ECH / TLS」に近いか確認する。
- 強みである「ECHは本物のSNI等を含むClientHelloInnerをHPKEで暗号化し、公開名だけのClientHelloOuterへ封入して送る。TLS1.3で唯一残っていた平文メタデータを塞ぐプライバシー拡張。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。