仙石浩明の日記

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

9件のコメント »

  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

  7. 初めてポストします。仙石さまとstoneには10年以上お世話になっております(社内NW関連)。
    TLS対応されたということで、お伺いさせてください。

    stone -q sni -q tls1 -q verbose -q verify,none -q CApath=/etc/letsencrypt/live/mydomain.org localhost:10022/ssl 20022

    10022はバックエンドのNginx(ワイルドカード証明書処理)で、
    クライアント(teraterm)からstone稼動サーバの20022に
    名前ベースでsshしたときに、この名前がClientHelloに入るでしょうか?両側の設定を試行錯誤していますがうまくいかないので質問させていただきました。
    Nginxまでサーバ名が到達すれば、ワンポート・ワンアドレスで受けるTCPバーチャルホストが成立するので、何卒よろしくお願いいたします。

    コメント by うさこさん — 2018年3月20日 @ 22:07

  8. ssh クライアントって名前ベースで接続できるのでしたっけ?

    stone は ssh プロトコルを解釈しないので、ssh プロトコルに Virtual Host が含まれていたとしても、それを読み取ることはしません。

    TLS プロトコルの ClientHello に virtual (という Virtual Host 名) を入れたい、ということでしたら、-q servername=”virtual” オプションを指定するか、stone の引数に -q sni virtual:10022/ssl 20022 などと指定します。

    コメント by hiroaki_sengoku — 2018年3月21日 @ 21:59

  9. 早速のご返答ありがとうございます。
    上ご指摘の通り、-q servernameを指定すると、Nginxまで到達することを確認しました。ありがとうございました。

    連携はこれでバッチリですが、
    エンドユーザ(ここで言うteraterm利用者)には証明書等意識させたくないこと、
    また、証明書は一つに纏められたものの、バックエンドのサーバ数分ポートを用意するのも運用上??なので質問させていただいた次第です。
    もう少しがんばってみます。
    参考までに、自環境はこうなっています。

    windows -teraterm経由ssh-> [{stone:20022}--ssl-->{Nginx:10022}] –ssh–>[Nginxの転送設定先]

    {}:マイクロサービス
    []:ホスト
    (自環境ではLXDのコンテナです。stone手前のiptablesのNATチェインは省略)

    今後コンテナをたくさん作るにあたっての、
    楽な運用という観点から質問させていただきました。

    ありがとうございました。

    コメント by うさこさん — 2018年3月22日 @ 00:30

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

コメントする