Help us understand the problem. What is going on with this article?

Encrypted SNI / TLSハンドシェイクの暗号化

More than 1 year has passed since last update.

SNI とは

一つのIPアドレスと一つのTCPポートで、複数のWebサイトをホスティングする技術の一つに、virtualhostがあります。
これは、HTTPリクエスト内で指定するドメイン名から、同じIPに対するアクセスをドメインごとに振り分ける機能です。

たとえば、以下のコマンドを実行すると、こんな出力がされます。

(ちなみに、vがリクエストヘッダの表示、 Iがボディの非表示のオプションです。)

$ curl -v -I example.com

*   Trying xxxxx...
* TCP_NODELAY set
* Connected to example.com (xxxxx) port 80 (#0)
> HEAD / HTTP/1.1
> Host: example.com
> User-Agent: curl/7.62.0
> Accept: */*
>

…(省略)

真ん中の行に、Host: example.comという行があるのが見えます。HTTP通信はTCPの上で行われる通信ため、通信内容にドメイン名は必要ないのですが、HTTP/1.1対応のブラウザだとこのようなリクエストが投げられます。

Nginxなどのサーバーは、このHostを見分けて、同じIPアドレスへのリクエストでも、異なる処理ができるように
なります。格安レンタルサーバーなどではこの手法を取っています。

今HTTP/2.0はどうなんだろうなーと調べてみましたが、やはりHostフィールドはあるようです。
RFC 8.1.3章

そしてここからが本題です。HTTPSを使う場合、Hostはどうなるでしょう。
はい、そこのあなた、正解です。HTTPヘッダごと暗号化されます。

なので、もし判別しようと思った場合、TLSハンドシェイクの時点で、どのサーバーにアクセスしたいのかをリバースプロキシにあたるサーバーが知る必要が有ります。
また、そのときにクライアントはサーバー(virtualhost)のTLS証明書を取得します。

このために、拡張TLS通信のハンドシェイクの一番最初の段階であるClientHelloでは、SNI(Server Name Indication)と呼ばれる技術を使います。
これはClientHelloの中で、server_nameの項目にドメイン名を入れて、接続するホスト先を指定してあげることができます。

      enum {
          server_name(0), max_fragment_length(1),      ←これ
          client_certificate_url(2), trusted_ca_keys(3),
          truncated_hmac(4), status_request(5), (65535)
      } ExtensionType;

( https://www.ipa.go.jp/security/rfc/RFC6066JA.html より )

また、このSNIは、RFCの中で

「クライアントは、あるサーバをサポートされている名前種別によって見つけ出す(locate)ときはいつでも、その client hello 中に種別が "server_name" の拡張を含むこと」が推奨される(RECOMMENDED)。」

と定められています。どういうことかというと、ChromeやFirefoxといった、モダンなブラウザは基本的にこのserver_nameを使用しています。

つまり、HTTPS通信を使う場合でも、最初のコネクション時に、接続するドメインを指定することになります。

ということは、HTTPSを使っても、どのドメインに接続しているかはネットワーク上の機械が確認できてしまうということになります。

このため、HTTP bodyやパスだけを暗号化したところで、ネットワーク側である程度の検閲やブロッキング、ログ収集ができてしまいます。

この対策として考えられたのが、Encrypted SNIです。

Encrypted SNI とは

Encrypted SNI(以下、ESNI)は、その名前の通りSNI、つまりClientHelloの中のserver_nameを暗号化してしまう技術です。

パケットを割り振ったり、Hostによって処理を変えるサーバー(リバースプロキシ、Nginxなど)が、SNIを解釈できればその後TLS暗号が使えるわけです。
初回接続時だけ、HostやTLSによらない暗号化を行います。

暗号化とDNS

具体的な暗号化についてです。

server_nameを暗号化し、ClientHelloを送信します。
それを受け取ったサーバーは、復号化したのち、server_nameを見て、ホスト振り分けを行うことになりますね。

(このとき使うのは、ディフィー・ヘルマン半静的鍵共有と呼ぶらしいですが、ちょっと普通の公開鍵暗号と何が違うのかわかりませんでした、ごめんなさい。)

では、TLS通信のハンドシェイクの暗号化につかう鍵は、どうやって手に入れるのでしょう。ここまで来ると卵が先か鶏が先か、の気分になってきます。

この方法ですが、DNSを使用します。DNSのレコードの中に、鍵交換に使うための公開鍵をサーバーが埋め込んでおきます。
クライントは、まずTLSハンドシェイクの中で、DNSクエリーで取得した鍵で暗号化を使えるわけですね。

DNSクエリーの中身を覗かれる心配、改ざんされる心配ですが、前者はDNS over HTTPSやDNS over TLS、後者はDNSSECによって解決できます。

いや、またTLS使うんならそこのESNIはどうなんだよ…と言いたくなりますが、まあ1.1.1.1などはvirtualhostを使っていないようなので、SNIをあえて使わないようにすれば大丈夫な様子です。(これ僕も思いました。)

ESNI の実用例

今の所、ESNIはTLS 1.3の拡張提案でしかないため、実際にブラウザで利用することはできません。
ただ、Firefox Nightlyに追加している途中という噂もあります。

ESNIを使うことで、先程の問題点などを解決することができ、より安全にインターネットが使えるようになるのはそう遠くないことだと思います。
もちろん、IPアドレスの逆引きなど、最後の穴はいくつか残っています。これからのESNIの動向に注目したいです。

(参考: https://blog.cloudflare.com/encrypted-sni/
(また、使っているブラウザがDNSSECやTLS1.3に対応しているかどうかはここで確認できます。 https://www.cloudflare.com/ssl/encrypted-sni/

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away