Blog

2015.12.06

TLS 1.3 Encrypted SNI拡張の議論

こんにちは、エンジニアの前田です。この記事は、 HTTP2 Advent Calendar 2015 - Qiita の6日目の記事です。

今日はhttp/2自体ではないですが、関連の深いTLSについて、提案され検討中のEncrypted SNI拡張を説明してみようと思います。この提案はまだまだ検討の初期段階で、draftも作文されていません。これからまだまだいろいろ検討・変更される部分があるはずです。へー、そういう話もあるのかー程度に読んでいただければと思います。

SNI (Server Name Indication)とは

ひとつのサーバーIPアドレスで、複数のホストをサービスすることがあります。バーチャルホスト(またはマルチテナント)と呼ばれる方法です。ブラウザなどのHTTPクライアントは、行き先サーバーのIPアドレスをDNSで引いて接続します。バーチャルホストをブラウザ側から見ると、異なるホストなのにDNSが同じIPアドレスになるという風に見えます。同一のIPアドレスにいる複数のサーバーを、どうやって区別できるのでしょうか。

図1: cleartext HTTPの場合
図1: cleartext HTTPの場合

クリアテキストhttpでは、クライアントがhttp/2の:authorityないし、http/1.1のHostヘッダで行き先ホストを指定することで、同じIPアドレスにあるホストのうちどこに接続したいかをサーバーに伝えることができます。サーバーやロードバランサはこれを見て、実際の処理をどのバックエンドに回すかを決めることができます(図1)。

図2: TLSの場合:authorityでは遅すぎる
図2: TLSの場合:authorityでは遅すぎる

これと同じことをTLSでやろうとすると問題があります(図2)。:authorityまたはHostヘッダが得られるよりも以前に、接続先ホストに対応したTLSサーバー証明書を渡さなければならないからです。実はこの問題は証明書で解決することもできます。ワイルドカード証明書やSAN (Subject Alt Name)証明書を使う方法です。しかし、この方法はいつでもできるというわけではありません。

図3: SNI(Server Name Indication)拡張
図3: SNI(Server Name Indication)拡張

そこでSNI (Server Name Indication)拡張を使います。ClientHelloの拡張として、どの接続先に行きたいかを書いておきます。これによって、サーバーやロードバランサは正しい証明書とバックエンドを選択することができます。

SNIのプライバシー上の問題点

しかし、SNI拡張は平文で送信されるという問題があります。暗号化しようと思っても、証明書を選ぶために使うのですから、まだどんな鍵で暗号化したらいいかわからないからです。横からパケットを盗聴している攻撃者からは、「あいつ、あのサーバーにアクセスしようとしてるぞ」とわかってしまいます。ここにプライバシー上の問題があります。Encrypted SNI拡張はこの部分を暗号化し、攻撃者からは、どこあての通信が行われているのかわからないようにしたいのです。

しかしちょっと待ってください。IPアドレスを知るためにはDNSを引かなければなりません。そのためにはサーバー名を使いますから、DNSルックアップから行き先サーバーがわかってしまいます。この問題は、IETFでDPRIVEというワーキンググループで別途検討、仕様化が進められています。ここでは、DPRIVEが完了して、DNSからはプライバシーがもれなくなったという時点を想定して、SNIを秘匿する方法を考えましょう。

Gatewayサーバーを使ったSNIプライバシー

SNIだけを秘匿しても、結局は特定のIPアドレスへ接続しに行くわけなので、どこのIPアドレスに行ったかによって接続先はわかってしまうことになります。そこで、ここでは以下のようなアーキテクチャで、接続先を秘匿することを考えます。

図4: Encrypted SNIによる接続のアーキテクチャ
図4: Encrypted SNIによる接続のアーキテクチャ

クライアントと接続先を秘匿したいhiddenサーバー(目的のサーバー)との間にGatewayサーバーを置きます。クライアントはまずGatewayサーバーにTLSで接続し、Gatewayサーバーが実際のhiddenサーバーへと通信を中継します。このとき、クライアントはGatewayサーバーだけにわかるように、暗号化されたEncrypted SNIによって、自分は実際にはどこに接続したいのかを伝えます。

ここで達成したい目標は、クライアントからGatewayサーバーへ行っている通信が、クライアント-Gateway間の通信を観測している者には「実はGateway自体と話をしているのか、hiddenへ行っているのか、複数あるとしたらどのhiddenへ行っているのか、わからない・区別できないようにする」ことです。

これを実現するためにはいくつかの前提が必要になります。

  • クライアントは、そのhiddenサーバーへ行くにはこのGatewayサーバーに話しかければよいと知ることができる。これはDNSなどで実現できそうです。
  • Gatewayはどのhiddenサーバーを配下に持っているか知っている
  • hiddenサーバーは、Gatewayが自分に中継してくれると知っている
  • Gatewayにhiddenサーバーへ中継してくれるか問い合わせることはできない。そのかわり、「指定したhiddenサーバーを知っていたら中継して」と頼むことはできる
  • TLS1.3の0-RTTを使用する。SNIを暗号化するためのキーを、クライアントとGatewayの間で知っていなければなりません。また、クライアントとhiddenの間の通信がGatewayに知られないためには、hiddenサーバーとの間でもTLS設定ができている必要があります。このために、TLS1.3の0-RTTキーを使います。逆に言うと、1-RTTが必要になる最初の1回の通信はSNIを守ることはできない(そのサーバーと通信しようとしていることは外部からわかってしまう)ことになります。

基本的な流れは以下のようになります。

  • クライアントはGatewayサーバーへ接続する際、SNIにはGatewayを指定し、Encrypted SNI拡張によって「本当はここに行きたいんだ」というhiddenを指定する
  • GatewayがEncrypt SNI拡張を理解し、かつhiddenが自分の管理しているhiddenサーバーであれば、接続をhiddenサーバーへ中継する
    • 理解しない、またはhiddenが管理下にない場合は、Gatewayはだまって自分との接続として処理する(Gatewayの証明書を返す)
    • 中継が行われた場合は、hiddenサーバーからのServerHelloをGatewayから返す(hiddenの証明書が入っている)
  • クライアントは、Gatewayサーバーから返ってきたServerHelloのサーバー証明書を見て、最終的につながったのはGatewayなのかhiddenサーバーなのかを判別する
  • クライアントとhiddenサーバー間で接続が確立した後、Gatewayはパケットを中継するだけの動作になる(復号・再暗号化は必要なく、また内容は理解できないしする必要もない)

提案1: IETF94横浜での提案

図5: Encrypted SNIの最初の提案
図5: Encrypted SNIの最初の提案

IETF94横浜でEKRとDKGにより、最初の案が示されました (図5)。

  • EncryptedExtensionsの中にRSNI(RealSNI: 本当に接続したい先)を入れる
  • Gatewayがもしこれを理解し、しかもRSNIで指定されたhiddenサーバーが配下にあれば、hiddenサーバーへ中継する
  • clientとhiddenサーバーの間でend-to-end securityが確立する。Gatewayサーバーはパケットの中継はするが、中身を見ても復号できないし、する必要もない

ここで、Gatewayからhiddenサーバーへ渡すClientHelloは、最初にクライアントから来たものと同じになります。つまり、hiddenサーバーは、Gatewayから来たSNI=gatewayのClientHelloが、実はクライアントからのものであるとして処理しないといけません。

そのほかにも、いくつか検討しなければならないことがあります。hiddenはSNIを得られないので、マルチテナントできません。クライアント、gateway、hiddenの全部が、このスキームでclientがhiddenに接続したいということを知っていなければなりません。

そしてもうひとつの指摘は、通常のTLSトンネル上でさらにTLSトンネルを張ったほうがカンタンではないかというものでした。トンネル上でさらにTLSを張ると図6のようになります。

図6: tunnel in tunnel
図6: tunnel in tunnel

Encrypted SNIとtunnel-in-tunnelの一番の違いは、2回暗号化するか、1回しかしないか、です。Encrypted SNIでは、Gatewayが中継する通信は、パケットの素通しだけで、復号したり暗号化したりという処理は必要ありません。一方でtunnel-in-tunnelでは、クライアントからのパケットは外側の暗号化をいったんほどいて中継し、hiddenからのパケットはクライアント向けに暗号化して渡す必要があります。内側の暗号化をやめると、Gatewayで通信が見えてしまうことになり、それも困ります。

提案2: tls ML上での提案

これらの議論を受けて、今日になってtls ML上で別の方法が提案されました。内側にtunnelがあるとわかっているならば、外側の暗号化はもうしなくてもいいじゃない、という方法です。

図7: tunnel
図7: tunnel

この方法では、Gateway行きのClientHello1に、EarlyDataIndicationで送られる部分として、hidden行きのClientHello2をつけます。ClientHello2には、本当の行き先をSNI=hidden.example.comと書いておき、Gateway用の0-RTT鍵で暗号化しておきます。Gatewayは「UseTunneledTLSフラグ」があるのを見て、ClientHello2を自分の鍵で復号し、そのSNIからこれをどのhiddenに振り分けるかを決めることができます。UseTunneledTLSフラグの実装方法案としてはEncryptedExtensionsを使う方法や、TLS content typeを使う方法などがありそうです。

hiddenは、このClientHello2が自分へのClientHelloであるとして処理をし、ServerHelloを返し、その後のハンドシェークを実行します。Gatewayでは以降のパケットを素通ししてやることによって、クライアントとhiddenの間でのTLS接続が確立します。

つまり、トンネルの最初のパケットだけを、Gatewayだけが読める形で暗号化してやることによって、Gatewayで適切な処理ができる、という方法です。この方法では、さらに多段にすることも可能に思えます。この方法の議論は始まったばかりで、この先どのように展開していくかは注意が必要です。

まとめ

TLSのSNI拡張が平文であることから、仮にDPRIVEができたとしても、クライアント(ブラウザ)がどのサーバーに接続したいかが第三者に知られてしまうという、プライバシー上の問題があります。この問題を解決するために、SNI自体を暗号化する仕組みが提案されました。暗号化するためのキーはTLS 1.3の0-RTTを使って決めます。また、IPアドレスから通信先が知られないよう、Gatewayを置いた方式をベースとしています。

Encrypted SNIを扱うための、2つの方法が提案されました。どちらも、まだいろいろ検討しなければならない技術的な問題があります。また、このような複雑な仕組みを導入するだけの投資に見合うのかどうかということも考える必要があるでしょう。「拡張」であるとしたら、どれだけのサーバーとクライアントがこの技術を実装してくれるのか、それによってこの技術の有効性も左右されることになるでしょう。

いずれにしても、議論としては面白く、かんたんなことで実現できるのであれば、またひとつインターネットが安全になるわけです。まあ個人的には、EKRやDKGという大変有能かつ忙しい方々、このような新しい拡張は後回しにして、まずはTLS1.3を早くfixしてもらいたいとも思うのですが……。