仙石浩明の日記

2007年6月25日

stone に Server Name Indication (TLS 拡張) 機能を実装

RFC 3546 で、 TLS (Transport Layer Security つまり SSL) の拡張が規定された。 その中の一つが、「Server Name Indication」と呼ばれる拡張であり、 クライアントがサーバに対して、 サーバのホスト名を伝えることができるようになった (規定されたのは 2003年6月なのであるが、まだあまり普及していない)。

なぜクライアントがサーバへ、 当のサーバの名前を伝えてやる必要があるかというと、 サーバが複数のホスト名を持つ場合があるからだ。 例えば WWW サーバは、 http リクエスト中の「Host:」フィールドを見てレスポンスを切り替える。 この機能はバーチャルドメインと呼ばれ、 一つの IP アドレスで複数のホスト名のサービスを提供する方法として、 広く使われている。

ところが (従来の) https の場合、この方法が使えない。 WWW サーバは SSL 通信を開始するにあたって、 *最初に*サーバ証明書を クライアントへ送る必要があるからだ。 http リクエスト中の「Host:」フィールドに、 別のホスト名が書いてあったとしても後の祭。 「リクエストしたホスト名と、 サーバから送られてきた証明書に記載されたホスト名が一致しない」という旨の 警告が WWWブラウザに表示されてしまう。

もう少し詳しく説明すると、 クライアント (WWWブラウザ) とサーバは、 SSL 通信を始めるにあたって、 次のようなハンドシェークを行なう。

クライアント サーバ
ClientHello 乱数, セッションID, 暗号/圧縮方式
ServerHello 乱数, セッションID, 暗号/圧縮方式決定
Certificate サーバ証明書
ServerKeyExchange 共通鍵の交換
(CertificateRequest クライアント認証を要求する時のみ)
ServerHelloDone ServerHello の終了を通知
(ClientCertificate クライアント認証を要求された時のみ)
ClientKeyExchange 共通鍵の交換
ChangeCipherSpec 次のデータから暗号化することを通知
Finished 以上のハンドシェークのハッシュ値(暗文)
ChangeCipherSpec 次のデータから暗号化することを通知
Finished 以上のハンドシェークのハッシュ値(暗文)

このハンドシェークの後、 クライアントが暗号化された http リクエストを送信し、 それを受けてサーバが暗号化されたレスポンスを返す。

https サーバがバーチャルドメイン機能を持つには、 https サーバがサーバ証明書を送信する (上のハンドシェーク図の 3行目) より前に、 クライアントがリクエストしたいホスト名を通知する必要がある。 上図から明らかなように、 ホスト名の通知は一番最初の「ClientHello」で行なわれなければならず、 そのための拡張が、 「Server Name Indication」というわけである。 もちろんこの時点では、まだ鍵の交換は行なわれていないので、 ホスト名は平文で送られる。

前置きが長くなってしまったが、 この Server Name Indication (SNI) を stone でサポートしてみた (stone.c Revision 2.3.1.11 以降)。 ただし stone が利用している OpenSSL で SNI がサポートされるのは 0.9.9 以降である (追記: 0.9.8f 以降でもサポートされた) ので、 OpenSSL 0.9.9 以降のライブラリを使って stone を make する必要がある 。

二台の http サーバ senri.gcd.org と asao.gcd.org があるとき、 次のように stone を実行する:

stone -z sni \
      -z servername=senri.gcd.org \
          -z cert=senri.gcd.org-cert.pem \
          -z key=senri.gcd.org-key.pem \
          senri.gcd.org:http 443/ssl -- \
      -z servername=asao.gcd.org \
          -z cert=asao.gcd.org-cert.pem \
          -z key=asao.gcd.org-key.pem \
          asao.gcd.org:http 443/ssl

転送元ポート指定「443/ssl」が二度現われていることに注意。

最初の「senri.gcd.org:http 443/ssl」は、 「-z servername=senri.gcd.org」と指定しているように、 クライアントが通知するサーバのホスト名が senri.gcd.org の場合の指定である。 サーバ (つまり stone) は、 「-z cert=ファイル名」と「-z key=ファイル名」で指定されるサーバ証明書を返し、 クライアントからの通信を、 SSL 復号を行なった上で senri.gcd.org:http へ中継する。

二番目の「asao.gcd.org:http 443/ssl」についても同様に、 クライアントが asao.gcd.org を通知すれば、 stone は asao.gcd.org のサーバ証明書を返すとともに、 クライアントからの通信を、 SSL 復号を行なった上で asao.gcd.org:http へ中継する。

この例では http サーバは二台のみであるが、 同様に何台でも指定できる。 また、もちろん物理的に異なるサーバを用意する必要があるわけではなく、 一台の http サーバで複数のポートを開き、 各ポートで別々のホスト名のサービスを提供してもよい。

OpenSSL 0.9.9 の s_client コマンド (SSL クライアント) でアクセスしてみると、 senri.gcd.org を通知すれば (-servername senri.gcd.org オプション)、

% openssl s_client -connect localhost:443 -servername senri.gcd.org -CApath /usr/local/ssl/certs
CONNECTED(00000003)
depth=2 /C=JP/ST=Kanagawa/L=Kawasaki/O=GCD/CN=GCD Root CA/emailAddress=root@gcd.org
verify return:1
depth=1 /C=JP/ST=Kanagawa/L=Kawasaki/O=GCD/OU=Hiroaki Sengoku/CN=GCD_Sengoku_CA/emailAddress=sengoku@gcd.org
verify return:1
depth=0 /C=JP/ST=Kanagawa/L=Kawasaki/O=GCD/OU=Hiroaki Sengoku/CN=senri.gcd.org/emailAddress=sengoku@gcd.org
verify return:1
Server did acknowledge servername extension.

サーバ (stone) は「CN=senri.gcd.org」の証明書を返し、 同じ 443番ポートへのアクセスでも 通知するサーバ名を asao.gcd.org へ変えるだけで、

% openssl s_client -connect localhost:443 -servername asao.gcd.org -CApath /usr/local/ssl/certs
CONNECTED(00000003)
depth=2 /C=JP/ST=Kanagawa/L=Kawasaki/O=GCD/CN=GCD Root CA/emailAddress=root@gcd.org
verify return:1
depth=1 /C=JP/ST=Kanagawa/L=Kawasaki/O=GCD/OU=Hiroaki Sengoku/CN=GCD_Sengoku_CA/emailAddress=sengoku@gcd.org
verify return:1
depth=0 /C=JP/ST=Kanagawa/L=Kawasaki/O=GCD/OU=Hiroaki Sengoku/CN=asao.gcd.org/emailAddress=sengoku@gcd.org
verify return:1
Server did acknowledge servername extension.

サーバが返す証明書が「CN=asao.gcd.org」に変わるし、 stone が中継する先も asao.gcd.org:http へ変わる。 したがって一つの IP アドレスに 複数のホスト名を持たせるバーチャルドメイン機能を、 任意の SSL 通信で実現できる。

なお、上記 stone 実行方法 (オプション指定) は、やや煩雑なので、

stone -z sni \
      -z certpat=%n-cert.pem \
      -z keypat=%n-key.pem \
      -z servername=senri.gcd.org \
          senri.gcd.org:http 443/ssl -- \
      -z servername=asao.gcd.org \
          asao.gcd.org:http 443/ssl

などと、証明書ファイルの指定をまとめることもできる。 「-z certpat=%n-cert.pem」オプションによって証明書のファイルのパターンを 指定する。 「%n」はサーバのホスト名で置き換えられる。 すなわち、 「-z servername=senri.gcd.org」を指定した場合は、 「-z cert=senri.gcd.org-cert.pem」を指定したのと同じ結果になる。 「-z keypat=%n-key.pem」についても同様。

現時点で SNI をサポートしている WWWブラウザは、 私の知っている範囲だと Firefox 2.0 等と IE7 だけであるが、 今後は開発される WWWブラウザの大半が SNI をサポートすることになるだろう。 そうなれば https サーバも、 バーチャルドメインで運用することが一般的となるはずである。

Name Based SSLについてもうちょっと腰を入れて調べる。
IE7ではRFC3546 のServer Name Indicationの対応がされている。
Apacheの方では、RFC3546は、mod_gnutlsを入れれば対応は可能なようです。
mod_gnutlsのサイトはこちら。
が、2005年から時が止まったまま・・・。むう。

OpenSSL 0.9.9 の安定版がリリースされれば (追記: SNI をサポートした 0.9.8f が 10/11 にリリースされた)、 apache などの WWW サーバにおいても SNI サポートが普通になると思われるが、 それまでは stone で SSL 暗号化を行なうようにすれば、 手軽に SNI を利用できる。 サーバに OpenSSL 0.9.9 をインストールしてしまうと、 OpenSSL を利用する全てのソフトウェアが影響を受けてしまうが、 例えば以下のように stone.c をコンパイル (Linux でのコンパイル例) して、 stone だけ 0.9.9 をリンクするようにすれば、 影響を stone だけに限定できる。

% cc -Wall -DCPP='"/usr/bin/cpp -traditional"' \
     -DPTHREAD -DUNIX_DAEMON -DPRCTL -DSO_ORIGINAL_DST=80 -DUSE_POP -DUSE_SSL \
     -I /usr/local/ssl-0.9.9/include -L /usr/local/ssl-0.9.9/lib \
     -o stone stone.c -lpthread -ldl -lssl -lcrypto

以上は stone が SSL サーバとして、 サーバホスト名通知を受付ける場合であるが、 もちろん stone を SSL クライアントとして実行し、 サーバホスト名を通知することもできる。

% stone -q sni -q verbose -q verify -q CApath=/usr/local/ssl/certs
        -q servername=asao.gcd.org localhost:443/ssl 10080
Jun 23 08:43:46.116894 3084876480 start (2.3c) [18500]
Jun 23 08:43:46.185448 3084876480 stone 3: 127.0.0.1:443/ssl <- 0.0.0.0:10080
Jun 23 08:43:48.610513 3084876480 3 TCP 6: [depth2=/C=JP/ST=Kanagawa/L=Kawasaki/O=GCD/CN=GCD Root CA/emailAddress=root@gcd.org]
Jun 23 08:43:48.610707 3084876480 3 TCP 6: [depth1=/C=JP/ST=Kanagawa/L=Kawasaki/O=GCD/OU=Hiroaki Sengoku/CN=GCD_Sengoku_CA/emailAddress=sengoku@gcd.org]
Jun 23 08:43:48.610928 3084876480 3 TCP 6: [depth0=/C=JP/ST=Kanagawa/L=Kawasaki/O=GCD/OU=Hiroaki Sengoku/CN=asao.gcd.org/emailAddress=sengoku@gcd.org]
Jun 23 08:43:48.624724 3084876480 [SSL cipher=AES256-SHA]

「-q servername=asao.gcd.org」オプションで、 通知するサーバホスト名を指定している。 この実行例の場合「asao.gcd.org」を通知しているので、 サーバからは「CN=asao.gcd.org」の証明書が返されている。 この実行例では、中継先が「localhost:443」であるので 「-q servername=」オプションを指定する必要があるが、 もし中継先 (サーバ) ホスト名が通知すべきサーバホスト名に一致するのであれば、 「-q servername=」オプションは省略できる。

Filed under: stone 開発日記 — hiroaki_sengoku @ 07:20

6件のコメント »

  1. [Linux] stone経由でSSL-IPベースVirtualホスト

    用途: 1つのグローバルIPアドレスと1台のサーバーでApacheの「名前ベースVirtualホスト」で複数ドメインを運営している場合などで、SSLサイトをいくつか稼動させたいと思っても、簡単にはできませんでした。 なぜなら、この場合のサイト証明書はIPアドレス別かポート別…

    コメント by ぶろぐ-なーお's BLOG-(有)モーションクリエイト — 2007年11月5日 @ 21:34

  2. 仙石さま、はじめまして。
    このたび、仙石さまの記事を参考にstoneで複数SSL証明書のテストを行いました。(URL先に記事をかかせていただきました。)
    OPENSSL-0.9.8gにTLS拡張がバックポートされたのでそれを使用しましたが、それが原因かどうかわかりませんが、コンパイル時に以下の警告が出ました。 そのまま使っていますが問題ないかどうか、原因などわかりましたらご教示いただければ幸いです。
    **警告内容
    stone.c: 関数 `mkStoneSSL’ 内:
    stone.c:7332: 警告: 引数 1 個の `SSL_CTX_new’ を渡しますにより、ポインタの示す型からの修飾子が切り捨てられます
    **

    コメント by なーお — 2007年11月5日 @ 21:43

  3. はい、問題ないと思います。
    0.9.8 までは、ssl.h にて
    SSL_CTX *SSL_CTX_new(SSL_METHOD *meth);
    と定義されていたのですが、
    0.9.9 からは、
    SSL_CTX *SSL_CTX_new(const SSL_METHOD *meth);
    に変更されています。
    stone.c では 0.9.9 以降に合わせて const 宣言を追加しているのが原因です。

    コメント by 仙石浩明 — 2007年11月6日 @ 01:44

  4. 早速のお返事ありがとうございました。 当分この設定で使ってみようと思います。

    コメント by なーお — 2007年11月6日 @ 05:26

  5. 高止まりするSSLサーバ証明書に定額サービスを、ということで来春より新進米国CAベンダと組んで日本参入を計画中の者です。単刀直入に「SNIは何が嬉しいか?」をご教示頂きたいのですが、要は、物理的にServerを複数持たずとも、httpsサイト含め、Sever1台での可用性が高まり経済的、ということでしょうか?必要となるSSL証明書の枚数に変わりはないのでしょうか?

    コメント by 百万遍 — 2007年12月19日 @ 12:58

  6. はじめまして。看護大学の橋本です。便利にSTONEを使わせていただいています。利用方法は(1) MS社のKMSとの通信 (2) SSL V3 のPoodle問題対策
    (3) PostFIX の SMTP/Submission Over SSL/TLS
    感謝しております。

    コメント by hash — 2015年1月23日 @ 14:56

この投稿へのコメントの RSS フィード。

コメントする