2007年06月

このエントリーを含むブックマーク 2007年06月25日

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=」オプションは省略できる。



hiroaki_sengoku at 07:20|この記事のURLComments(4)TrackBack(1)stone 開発日記 
このエントリーを含むブックマーク 2007年06月15日

stone で UDP と TCP の相互 変換を実装してみた (stone.c 2.3.1.10以降)。 つまり、UDP パケットが届いたら、 その頭にデータ長 (2バイト) を付けて 次図の形式のデータにして TCP セッションへ送信する。 あるいは逆に、 TCP セッションから次図の形式のデータを受信したら、 「データ長」の部分を取り除いて 「可変長データ」の部分を UDP パケットとして送信する。

┌──┬──┬──┬─≪─┬──┐
│データ長 │ 可変長データ  │
└──┴──┴──┴─≫─┴──┘

もちろん「データ長」はネットワーク バイトオーダ (ビッグエンディアン)。 この形式は、 DNS の問合わせ/応答を TCP を使って行なう方法 (RFC1035 4.2.2. TCP usage) と 同じであるので、

# stone -n 192.168.1.1:domain/udp localhost:domain
Jun 15 06:34:35.215996 16384 start (2.3c) [15707]
Jun 15 06:34:35.248158 16384 stone 3: 192.168.1.1:53/udp <- 127.0.0.1:53

などと stone を実行 (TCP 53 を UDP 53 へ変換) しておいて、

% host -T www.gcd.org localhost
Using domain server:
Name: localhost
Address: 127.0.0.1#53
Aliases:

www.gcd.org has address 60.32.85.220
www.gcd.org has address 60.32.85.221
www.gcd.org has address 60.32.85.216

などと TCP で localhost への問合わせ (-T オプション) を行なうことができる。 すなわち、stone が TCP での問合わせを UDP に変換して ネームサーバ (192.168.1.1:53/udp) へ中継している。

あるいは逆に、

# stone -n 192.168.1.1:domain localhost:domain/udp
Jun 15 06:38:36.660967 16384 start (2.3c) [18576]
Jun 15 06:38:36.662647 16384 stone 3: 192.168.1.1:53 <- 127.0.0.1:53/udp

などと stone を実行 (UDP 53 を TCP 53 へ変換) しておいて、

% host www.klab.org localhost
Using domain server:
Name: localhost
Address: 127.0.0.1#53
Aliases:

www.klab.org has address 211.13.209.203

などと UDP で localhost へ問合わせることができる。 すなわち、stone が UDP の問合わせを TCP に変換して ネームサーバ (192.168.1.1:53) へ中継している。

この UDP TCP 相互変換機能を、 ssh や VPN-Warp などを使った (TCP) ポートフォワードと組合わせることにより、 UDP のポートフォワードが可能になる。



hiroaki_sengoku at 09:05|この記事のURLComments(0)TrackBack(0)stone 開発日記