要約

  • TLSのserver_nameは、ClientHelloを送る側が提示し、証明書、TLSコンテキスト、透過転送先の候補を早期に絞るDNS名である。提示者の身元を証明するcredentialではない。
  • サービス証明書の検証、HTTPのHost/:authority、上流SNI、ECHのinner/outer、アプリ認可には別々の証拠が要る。同じ文字列でも権限は継承されない。

正しく動いた選択器が生んだ誤認可

クライアントは価値の高いtenant名をSNIに入れた。server-name callbackは対応するSSL_CTXを選び、サーバーは予定どおりの証明書を提示した。ここまでは仮想ホスティング機能の正常動作である。

誤りは次の一行だった。連携コードが選択された設定名をアプリのprincipalへコピーした。ClientHelloを書ける相手なら、構文上正しい任意の名前を送れる。それにもかかわらず、相手が自分で指定した行き先が、相手自身の身元になった。

サーバー証明書は、検証するクライアントにサーバーを認証させるためのものだ。クライアントをサーバーへ認証する向きではない。暗号方式の欠陥ではなく、経路情報を権限へ昇格させた設計上の欠陥である。

SNIが保証するのは形式と希望先

RFC 6066では、ClientHelloにServerNameListを置く。標準化されたhost_nameはASCII形式のDNSホスト名であり、IPv4/IPv6リテラルは許されない。同じname typeを複数入れることもできない。

この厳密さは相互運用性のためにある。DNSを引いた証拠、ゾーンを管理する証拠、秘密鍵を持つ証拠、tenant契約を持つ証拠ではない。特定IPへ直接接続し、別のDNS名をSNIで送ることもできる。

サーバーが名前を理解しつつ受け入れない場合、fatalなunrecognized_nameで止められる。一方、明示したfallbackで継続する実装もある。監査ではSNIなし、未知名、不正形式、default virtual hostを実際に通し、どの設定が選ばれたかを確認する必要がある。

transcriptの完全性と名前の権利は別物

TLS 1.3ではClientHelloもtranscriptに入り、最終的にFinishedで認証される。正常完了した通常の握手なら、途中の第三者がSNIだけを密かに書き換えたとは考えにくい。

しかし証明されるのは「このクライアントが、この接続で、この値を送った」ことまでである。「このクライアントにその値を名乗る権利がある」とは証明されない。発言が改変されていないことと、発言者に権限があることは違う。

client certificateなどがなければ、サーバーにとって相手は匿名のままでもTLS 1.3は成立する。ログ項目をclient_requested_server_nameと呼ぶべき理由はここにある。verified_tenantと名付けた瞬間、別の証拠を省略してしまう。

証明書を選ぶ側と検証する側

SNIはサーバーのcredential選択を助ける。選ばれたcredentialが目的のサービスを認証するかは、クライアント側の別判断である。

RFC 9525は、クライアントが受け入れるreference identifierを、サーバーが見せた証明書とは独立に構成するよう求める。その後にpresented identifierとの一致、証明書チェーンや期限などを検査する。SNIで候補が当たっても、この手順は消えない。

OpenSSLでも、SNIを設定するSSL_set_tlsext_host_name()と、証明書検証で期待するDNS名の設定は別である。前者だけのクライアントは正しい証明書を引き出せても、それを正しく検証したとは限らない。

複数SANを持つ共有証明書なら、二つの名前がともに有効になり得る。それは二つのtenantが同一の権限主体になったことを意味しない。

HTTP authorityは握手後に現れる

TLSが終わると、HTTP/1.1はHost、HTTP/2やHTTP/3は主に:authorityで要求対象を伝える。RFC 9110が重要なrouting情報として扱う、アプリ層のauthorityである。

通常のクライアントでは同じURIからSNIとHTTP authorityを作るため、一致することが多い。ただし別レイヤーの別メッセージであり、接続再利用、プロキシ、試験、誤設定、攻撃によって異なり得る。

不一致時の規則を先に決めるべきだ。拒否、限定された共有集合、接続がoriginに適さない場合の421などがある。最初のSNIに、その接続上の後続要求をすべて許可させてはならない。

もちろん両者が一致しても、それはクライアント本人の証明ではない。行き先の二つのヒントが整合しただけであり、principalとpermissionは別に必要である。

TLS終端の先には新しいSNIがある

プロキシが下流TLSを終端すると、上流へは新しいTLSクライアントとして接続する。ClientHello、SNI、証明書、reference identity、transcript、失敗状態はすべて別になる。

上流SNIは固定値、上流host、下流HTTP authority、管理されたmapなどから選べる。Envoyはそれらを区別し、実際に送った上流SNIに対するSAN検証と、要求authorityに基づく検証も別設定として扱う。

透過転送では証拠がさらに限定される。NGINXのssl_prereadはTLSを終端せずClientHelloのSNIを読み、上流を選べる。その装置が証明できるのはhintの抽出と経路選択だけで、最終証明書の検証やhandshake完了ではない。

下流のSNI文字列を上流ログへコピーしても暗号上の連続性は生まれない。二つのconnection ID、名前の出所、選択理由、検証結果が必要である。

ECHでは外側の正しい名前がoriginではない

Encrypted Client Helloは秘密にしたい値をClientHelloInnerへ置く。ClientHelloOuterには通常、フロントサービスへ到達してretryを成立させるpublic nameが入る。

ECHが受理されれば、認証されたinnerからTLSパラメータを処理する。拒否時にはpublic name向け接続を認証してretry情報を得る場合があるが、RFC 9849は、それがorigin認証ではなくアプリへ成功として渡してはならないと定める。

つまりouter SNIは経路として正しく、originとして意図的に権限が足りない。同じsni欄へordinary、outer、accepted innerを押し込めると、観測モデルがこの境界を壊す。

ECHの状態、観測者の役割、名前の出所を保存しつつ、inner名の閲覧範囲は絞る必要がある。監視基盤が平文の接続先台帳を作り直せば、ECHの目的を別の場所で失う。

callbackではなく拒否結果を検証する

OpenSSLではearly ClientHello callback、server-name callback、ALPN選択の順序があり、server-name callbackはSSL_CTXを切り替えられる。GnuTLSにも名前を設定・取得するAPIがある。

APIが存在することは能力であって、特定接続での実行証拠ではない。どの値を正規化し、どのcontextへ進み、fallbackしたかをtraceに残す。TLS 1.3と旧版resumptionでの違いも、実装版ごとに試す。

最重要テストは、credentialなしで特権tenantのSNIを送ることだ。証明書選択は成功してよいが、principalは匿名で管理操作は拒否されなければならない。

続いてSNIなし、未知名、HTTP authority不一致、共有証明書、下流/上流の別名、passthrough、ECHの成功・拒否・未使用を実行する。設定ファイルではなく、意地の悪い入力に対するrunning codeの反応が境界を証明する。