SSL/TLS でアクセスしてみよう (1)

前へ << SSL/TLS の導入 (4) SSL/TLS でアクセスしてみよう (2) >> 次へ

SSL とは

SSL (Secure Socket Layer protocol) とは「セキュリティ機能付き HTTP」のことです。 オンラインショッピングサイトで住所・氏名などの個人情報を入力する際、 「このページは暗号化されています」 などとダイアログが表示されることがありますが、 そのとき「http://....」という URL ではなく 「https://....」という URL にアクセスしているはずです。 このとき使用されているプロトコルが SSL です。

SSL 有効 SSL 無効
SSL を使用しているかどうかは、右図のようなブラウザ右下の鍵のマークを見てもわかります。 鍵がきっちりはまっているのは SSL を使用しているということです。 一方、鍵が外れている状態は SSL を使用していない、ということです。

SSL を使用することによって、以下の効果があります。

  • 盗聴防止
  • 改竄防止
  • なりすまし防止

SSL/TLS の歴史

SSL は Netscape 社が開発したプロトコルです。 SSL/2.0 と SSL/3.0 は Netscape 社が開発したものですが、 SSL/3.0 を元にした TLS (Transport Layer Security) が RFC として定められています。

SSL と TLS の歴史は以下の通りです。

  • 1994年、Netscape 社は SSL/1.0 を開発しました。これは公開されませんでした。
  • 1995年、Netscape 社は SSL/2.0 を開発し、Netscape ブラウザに実装しました。
  • 1995年、Microsoft は SSL を元に、セキュリティホールなどを修正した PCT (Private Communications Technology protocol) を開発し、 Internet Explorer 2.0 に実装しました。
  • 1996年、Netscape 社は SSL/3.0 を開発し、Netscape ブラウザに実装しました。
  • 1996年、Microsoft は Internet Explorer で SSL/3.0 に対応し、 事実上 SSL v3 が主流となりました。
  • 1999年、IETF (Internet Engineering Task Force) の TLS ワーキンググループ が SSL/3.0 を元にした TLS/1.0 を RFC 2246 として標準化しました。
  • 2003年現在、TLS/1.1 が標準化にむけて IETF で検討中です。
SSL/3.0 と TLS/1.0 はほぼ同じ規格です。

なお、SSL とは別に、S-HTTP (Secure HyperText Transfer Protocol) というプロトコルがありました。 これは Enterprise Integration Technologies 社が提唱したもので、 RFC 2660 として S-HTTP/1.4 が定義されています。 S-HTTP は SSL/TLS とは異なり、HTTP に特化したつくりになっています。

初期のころは SSL と S-HTTP のどちらが主流になるかわからなかったそうです。 ただし、現在では明らかに勝負はつきました。 現時点で S-HTTP を実装しているブラウザや web サーバはありません。

OpenSSL

SSL プロトコルを自分で実装しようとするのは、当ページ管理人の手に余ります。 というわけで、OpenSSL (http://www.OpenSSL.org/) というフリーのライブラリを使わせていただきます。ありがたや。
OpenSSL のページの日本語訳が http://www.infoscience.co.jp/technical/openssl/ にありますが、かなり古いですのでお勧めできません。 この会社は Apache のドキュメントの 和訳もやっていますが ( http://japache.infoscience.co.jp/)、こちらも情報が古いです。 いろいろと事情はあるのでしょうが、公開している以上は情報の鮮度を 保ってほしいものです (この点に関しては、 当ページ管理人もあまり偉そうなことは言えませんが)。
FreeBSD では、4.0-RELEASE から OpenSSL が標準でインストールされるようになりました。 もし OpenSSL がインストールされていない OS を使用している場合は、 OpenSSL のサイトからダウンロードしてインストールしてください。

ほとんどの場合は

% gzip -dc OpenSSL-0.9.7b.tar.gz | tar xf -
% cd OpenSSL-0.9.7b
% ./config
% make
% make install
で OK なはずです。

https クライアント - C 言語版

C 言語で https なサイトにアクセスするサンプルプログラムです。 https://www2.ggn.net/cgi-bin/ssl にアクセスし、 結果を取得します。
このページは http://www.moxienet.com/lynx/ssl-test から リダイレクトされるサイトで、テキストブラウザの Lynx で SSL が使用できるかどうかをチェックするためのページらしいです。
コンパイルするには libssl と libcrypt が必要ですので、
% cc -o https-client https-client.c -lssl -lcrypto
として下さい。Solaris ならば
% cc -o https-client https-client.c -lresolv -lsocket -lnsl -lssl -lcrypto
など。

また、/usr/lib/ 以外のディレクトリに libssl が置いてある場合は -L /usr/local/lib-L /usr/local/ssl/lib などと、ライブラリの場所を指定することもお忘れなく。

引数なしで実行すると、 https://www2.ggn.net/cgi-bin/ssl にアクセスし、 結果を表示します。

% ./https-client
サーバからのレスポンス
HTTP/1.1 200 OK
Date: Tue, 10 Jun 2003 19:03:09 GMT
Server: Apache/1.3.26 (Unix) GGN-MM/1.3.1 mod_ssl/2.8.10 OpenSSL/0.9.6d
Connection: close
Content-Type: text/html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
<HTML><HEAD><TITLE>SSL Test</TITLE></HEAD>
<BODY BGCOLOR="#FFFFFF" TEXT="#000000" LINK="#0000FF" VLINK="#4040FF" ALINK="#2020FF">
<P>Hello, user from 220.145.196.26!</P>
<P>You have successfully connected using SSL (TLSv1).
      You received this document using the 168-bit EDH-RSA-DES-CBC3-SHA cipher.</P>
<P>I see you're not using <A HREF="http://lynx.browser.org/">Lynx</A>.
      You should give it a try.</P>
</BODY></HTML>
また、
% ./https-client host /path/file.html
と、引数でホスト名とパスを指定すると、https://host/path/file.html にアクセスします。

まずはお約束から

https-client.c

   20: #include <openssl/crypto.h>
   21: #include <openssl/ssl.h>
   22: #include <openssl/err.h>
   23: #include <openssl/rand.h>
必要なヘッダファイルを include します。これらのヘッダファイルはすべて OpenSSL に同梱されています。
まずは web サーバに接続しますが、 TCP コネクションの確立は以下のように通常の HTTP クライアントと同様に行います。 この部分は、SSL/TLS とは全く関係ありません。
   51:   servhost = gethostbyname(host);
   52:   if ( servhost == NULL ){
   53:     fprintf(stderr, "[%s] から IP アドレスへの変換に失敗しました。\n", host);
   54:     exit(1);
   55:   }
   56: 
   57:   bzero((char *)&server, sizeof(server));
   58:   server.sin_family = AF_INET;
   59: 
   60:   bcopy(servhost->h_addr, (char *)&server.sin_addr, servhost->h_length);
   61: 
   62:   /* ポート番号取得 */
   63:   service = getservbyname("https", "tcp");
   64:   if ( service != NULL ){
   65:     server.sin_port = service->s_port;
   66:   } else {
   67:     /* 取得できなかったら、ポート番号を 443 に決め打ち */
   68:     server.sin_port = htons(443);
   69:   }
   70: 
   71:   s = socket(AF_INET, SOCK_STREAM, 0); 
   72:   if ( s < 0 ){
   73:     fprintf(stderr, "ソケットの生成に失敗しました。\n");
   74:     exit(1);
   75:   }
   76: 
   77:   if ( connect(s, (struct sockaddr*) &server, sizeof(server)) == -1 ){
   78:     fprintf(stderr, "connect に失敗しました。\n");
   79:     exit(1);
   80:   }

ここからがいよいよ SSL/TLS に関連する部分です。
   84:   SSL_load_error_strings();
   85:   SSL_library_init();
SSL_load_error_strings() で OpenSSL のエラー文字列を読み込みます。 OpenSSL にはどのソースの何行目でどういうエラーが起こったのかを簡単に知る関数があるのですが、 このエラー内容は数値で管理されています。ただ、エラーが発生しても、
error:0406C06E:lib(4):func(108):reason(110)
というわかりづらいエラー詳細しか得ることができません。 しかしあらかじめ SSL_load_error_strings() を実行しておけば、
error:0406C06E:rsa routines:RSA_padding_add_PKCS1_type_1:data too large for key size
という (比較的) わかりやすいエラーメッセージが表示されるようになります。

その次の SSL_library_init() は SSL/TLS の初期化を行います。 これを呼ぶことで暗号方式やメッセージダイジェスト関数などが自動的に登録されます。


   39:   SSL_CTX *ctx;
   86:   ctx = SSL_CTX_new(SSLv23_client_method());
   87:   if ( ctx == NULL ){
   88:     ERR_print_errors_fp(stderr);
   89:     exit(1);
   90:   }
使用するプロトコルを決めます。ここでは SSLv2、SSLv3、TLSv1 のいずれかを使用するため、 SSLv23_client_method を選んでいます。 もし SSLv2_client_methodSSLv3_client_method TLSv1_client_method を使うと、それぞれ SSLv2・SSLv3・TLSv1 を使うことになります。

ついでにここでエラー情報を取得する方法を解説しておきます。 OpenSSL では全てのエラー情報を ERR_print_errors_fp()ERR_error_string() などの関数で取得することができます。 このサンプルではエラーが発生すると標準エラー出力にエラー情報を出力し、 終了します。

例えば SSL_library_init() を呼ばずに SSL_CTX_new() を呼ぶとエラーとなりますが、その際以下のようなメッセージが標準エラー出力に吐かれます。

93652:error:140A90A1:SSL routines:SSL_CTX_new:library has no ciphers:ssl_lib.c:1365:

   92:   ssl = SSL_new(ctx);
   93:   if ( ssl == NULL ){
   94:     ERR_print_errors_fp(stderr);
   95:     exit(1);
   96:   }
SSL_new で、SSL_CTX 構造体にセットしたプロトコルや暗号化方式を元に、 コネクションを管理する SSL 構造体を新たに生成します。

SSL はサーバとのコネクションを管理するもの、 SSL_CTX は SSL/TLS における暗号化や認証方法などを管理するもの、と考えてください。

SSL 構造体と SSL_CTX 構造体の意味あいがわかりにくいかもしれませんので、 例をあげておきます。もし、2つの SSL 対応 web サーバに同時に接続するとします。 両サーバにクライアント側が提示するプロトコル・暗号化方式が同じものでよいならば、

  SSL *ssl_1, *ssl_2;
  SSL_load_error_strings(); 
  SSL_library_init();
  SSL_CTX *ctx = SSL_CTX_new(SSLv23_client_method());
  ssl_1 = SSL_new(ctx);
  ssl_2 = SSL_new(ctx);
などと、SSL_CTX は 1つで済むわけです。 しかしコネクションは 2つ張るので SSL 構造体は 2つ必要になります。
   98:   ret = SSL_set_fd(ssl, s);
   99:   if ( ret == 0 ){
  100:     ERR_print_errors_fp(stderr);
  101:     exit(1);
  102:   }
SSL_set_fd() でソケットと SSL の構造体を結びつけます。
  105:   RAND_poll();
  106:   while ( RAND_status() == 0 ){
  107:     unsigned short rand_ret = rand() % 65536;
  108:     RAND_seed(&rand_ret, sizeof(rand_ret));
  109:   }
まず、このコードは /dev/random や /dev/urandom が存在する最近の UNIX 系 OS では必要ありません (別に残しておいても害はありません)。 なぜなら、この後に続く SSL_connect() を実行したときに、自動的に適切な初期化が行われるからです。
/dev/random や /dev/urandom を実装している最近の UNIX 系 OS の例:
  • FreeBSD 2.2-RELEASE 以降
  • NetBSD 1.3 以降
  • Linux カーネル 1.3.30 以降
  • Solaris 8 Release 12/02 以降 (Solaris9 も含む)

以下、/dev/random や /dev/urandom が存在しない環境向けの説明です。 /dev/random・/dev/urandom が存在しない環境で上記のコードを省略すると (上記のコードなしで SSL_connect() を実行すると)、

94779:error:24064064:random number generator:SSLEAY_RAND_BYTES:PRNG not seeded:md_rand.c:506:
   You need to read the OpenSSL FAQ, http://www.openssl.org/support/faq.html
というエラーになります。

ここで PRNG というものを説明しておかなくてはなりません。 暗号において乱数は非常に重要です。周期性が短いなどの質の低い乱数を使っていると、 暗号アルゴリズムには問題がなくても平文を解読されてしまうこともあります。 乱数を生成するものを PRNG (Pseudo Random Number Generator: 疑似乱数発生器) と呼びます。 OpenSSL は独自の PRNG を実装しています。

PRNG は乱数を生成するアルゴリズムでしかありませんので、 もし初期状態が同じなら全く同じ乱数を返します (OS ブート直後に 2,56,9,38 という乱数を返したとすると、 次回の OS ブート時にも 2,56,9,38 という同じ乱数が返ってくるということ)。 常に異なる値を返すために、はじめに「種」(seed、乱数の元ネタとなるもの) を与えなくてはいけません。 当然ながら毎回同じ種を与えてしまっては、結局 PRNG は同じ乱数を返しますので、 常に異なる種をセットする必要があります。 つまり、「乱数を生成するための種」を生成するのに乱数を使う、ということになります。


  105:   RAND_poll();
RAND_poll() は PRNG に自動的に種を与える関数ですが、 /dev/random や /dev/urandom が存在しない場合、種の生成に失敗してしまいます。
失敗することがわかっているのにあえて RAND_poll() を呼んでいるのは、 /dev/random・/dev/urandom を持つ環境で適切な種をセットするためです。

  106:   while ( RAND_status() == 0 ){
  107:     unsigned short rand_ret = rand() % 65536;
  108:     RAND_seed(&rand_ret, sizeof(rand_ret));
  109:   }
そこで RAND_status() を使って十分な種が準備できたかを判定し、 もし種が足りてない場合は rand() から取得した乱数の下位 2バイトを取り、 RAND_seed() を使ってそれを PRNG に追加しています。
なぜ下位 2バイトかと言うと、rand() の戻り値は long のため、 最上位ビットは常にゼロだからです。
PRNG は「最低でもこれだけのサイズの種が必要」という量があり (OpenSSL 0.9.7c では 32 バイト)、それを下回っているとやはりエラーとなってしまうので、
種を追加、RAND_status() でチェック、種を追加、RAND_status() でチェック…
と処理を繰り返し、必要なサイズの種がそろったらループを抜けるようにしています。

なお、rand() は OS にもよりますが、質の低い乱数生成関数です (参考: FreeBSD QandA 2228)。 しかし ANSI C で規定された関数なので、ここではあえて rand() を使っています。 あと、実装にもよりますが、 rand() の下位バイトを抽出するのはよくないケースがあります。 (周期が短かったり、最下位ビットが 0・1 の繰り返しだったり。参考: 良い乱数・悪い乱数)。 しかしここではサンプルということでご容赦いただきたいと思います。

/dev/random・/dev/urandom が存在するような「今どき」の OS を使うことをお勧めしておきます。 ちなみに /dev/random・/dev/urandom は OS が観測したランダムノイズを元に生成される乱数です。 ここで言うランダムノイズは、 例えばマウスの移動量やキーボードの入力タイミングなどをイメージするとよいのではないでしょうか (実際にはプロセスの状態とか、デバイスとの入出力タイミングなの値が使われているのではないかと思います)。


  112:   ret = SSL_connect(ssl);
  113:   if ( ret != 1 ){
  114:       ERR_print_errors_fp(stderr);
  115:       exit(1);
  116:   }
SSL_connect を呼ぶことで、自動的にサーバとハンドシェイクが行われます。 具体的には、以下のことがこの時点で決定します。
  • 使用するプロトコル (SSLv2 or SSLv3 or TLSv1)
  • 使用する暗号化方式・鍵交換方式・ハッシュ方式
  • サーバ証明書の取得
  • 使用する共通鍵

HTTP

これで SSL/TLS コネクションの確立は終了しました。 その後は、普通に HTTP リクエストを送ります。 まず HTTP リクエストの文字列を作ります。
  119:   sprintf(request, 
  120:           "GET %s HTTP/1.0\r\n"
  121:           "Host: %s\r\n\r\n",
  122:           path, host);
  123: 
  124:   ret = SSL_write(ssl, request, strlen(request));
  125:   if ( ret < 1 ){
  126:     ERR_print_errors_fp(stderr);
  127:     exit(1);
  128:   }
HTTPS であっても、送信するリクエストは通常の HTTP と全く同じです。

送信するための関数は、HTTP では

write(s, request, strlen(request));
でしたが、HTTPS では
SSL_write(ssl, request, strlen(request));
となります。
web サーバからの受信部分です。
  131:   while (1){
  132:     char buf[BUF_LEN];
  133:     int read_size;
  134:     read_size = SSL_read(ssl, buf, sizeof(buf)-1);
  135:     
  136:     if ( read_size > 0 ){
  137:       buf[read_size] = '\0';
  138:       write(1, buf, read_size);
  139:     } else if ( read_size == 0 ){
  140:       /* FIN 受信 */
  141:       break;
  142:     } else {
  143:       ERR_print_errors_fp(stderr);
  144:       exit(1);
  145:     }
  146:   }
こちらも HTTP では
read_size = read(s, buf, sizeof(buf)-1);
だったものが、
read_size = SSL_read(ssl, buf, sizeof(buf)-1);
となっているだけで、その他は HTTP クライアントと同じです。

後始末

  148:   ret = SSL_shutdown(ssl); 
  149:   if ( ret != 1 ){
  150:     ERR_print_errors_fp(stderr);
  151:     exit(1);
  152:   }
  153:   close(s);
コネクションを切断します。まずは SSL/TLS のコネクションを切り、 その後ソケットのコネクションを切っています。
  155:   SSL_free(ssl); 
  156:   SSL_CTX_free(ctx);
  157:   ERR_free_strings();
確保したメモリを開放します。
  • SSL_new() で確保した領域を開放するために SSL_free() を呼ぶ
  • SSL_CTX_new() で確保した領域を開放するために SSL_CTX_free() を呼ぶ
  • SSL_load_error_strings() で確保した領域を開放するために ERR_free_strings() を呼ぶ

SSL/TLS の思想

HTTP クライアントでのデータ送受信は
write(s, request, strlen(request));
read_size = read(s, buf, sizeof(buf)-1);
でした。一方、HTTPS クライアントでは
SSL_write(ssl, request, strlen(request));
read_size = SSL_read(ssl, buf, sizeof(buf)-1);
となります。

SSL/TLS 独自の初期化・設定は必要ですが、 本質的には readSSL_read に、 writeSSL_write に変更し、 第一引数のディスクリプタ (int) を SSL 構造体に変更するだけで SSL/TLS 化は完了です。 SSL/TLS 化しても、HTTP プロトコル部分は一切変更する必要がありません。 SSL/TLS は HTTP から完全に独立しているのです。

つまり、SSL/TLS は TCP とアプリケーション層の間に暗号化・認証のレイヤを挿入する技術なのです。

通常のネットワークSSL/TLS を利用したネットワーク
アプリケーション層HTTP や POP3 など
トランスポート層TCP
セッション層IP
アプリケーション層HTTP や POP3 など
暗号化・認証層
SSL/TLS
トランスポート層TCP
セッション層IP

昔は HTTP に SSL を組み合わせた HTTPS しか普及していなかったのですが、 最近は POP・FTP・NNTP・IMAP などに SSL/TLS 機能を組み込んだ実装が普及してきました。

謝辞

このプログラムの初期バージョンは http://stingray.sfc.keio.ac.jp/security/ssl/ssl.html を全面的に参考にさせていただきました (このページはなくなってしまったようです)。 作者の三谷洋司さんに感謝します。 ただし、ソースに「SSLeay」という記述があることから、 OpenSSL の前身である SSLeay 用のプログラムと思われます。 OpenSSL と組み合わせると そのままでは動作しなかったので、 少々修正させていただきました。
前へ << SSL/TLS の導入 (4) SSL/TLS でアクセスしてみよう (2) >> 次へ

ご意見・ご指摘は Twitter: @68user までお願いします。