OpenSSLライブラリを使ってプログラミング(1)

にわか知識

  • SSL……Secure Socket Layer。SSLv2はセキュリティ上の欠陥が複数見つかっているので使うべきではない。現在使われているのはSSLv3(SSL3.0)。
  • TLS……Transport Layer Security。IETFが標準化した規格。SSLという名前は意図的に避けたみたい。SSLとの互換性はない。現在のバージョンは1.0(だけどプロトコルバージョンの値は3.1)。

SSL/TLSでの「セッション」と「コネクション」

SSL/TLSには「セッション」と「コネクション」というふたつの用語がある。

  • コネクションは、TCPの接続に対応する。ソケット接続ひとつに対してひとつのコネクション。
  • クライアントとサーバの共有秘密情報(master_secretと呼ばれる)ひとつに対して、ひとつのセッションが対応する。

master_secretを共有するための計算コストがそれなりにかかるので、複数のコネクションで同一のmaster_secretを使うことができる。逆に同一のコネクションの中で再ハンドシェイクをおこない新しいmaster_secretの共有をおこなうと、それ以後は新しいセッションとなる。

たとえばFTPでは制御用ポートとデータ転送用ポートの二つのポートを開く。でFTPSSL/TLSを使うときは、制御用ポートについては初めからSSL/TLSで通信して、データ転送用ポートの暗号化は追加で選択するみたい。この場合、単一のSSLセッションで二つのSSLコネクションになっているのだろう。たぶん。
とりあえずセッションのことはあまり気にしない。

OpenSSLのライブラリとヘッダファイル

  • ライブラリファイルは、libssl.*とlibcrypto.*
  • ヘッダファイルは大量にあるけど、openssl/ssl.hをインクルードすれば他のファイルもインクルードされる。

OpenSSLで使われるデータ構造

通信をおこなうだけなら、とりあえずSSL_CTXとSSLだけ知っていれば良い。あとSSL_METHODも使うけど、SSL_CTX生成時のみ。

SSL_CTX

OpenSSLを使う上でのグローバルな設定を保持するオブジェクト。次の関数で生成。

SSL_CTX *SSL_CTX_new(const SSL_METHOD *meth);
SSL_METHOD

SSL/TLSプロトコルを実装する関数群をあつめたデータ構造で、SSL_CTXオブジェクトを作るときに必要。プロトコルバージョンごと(SSLv2、SSLv3、TLSv1)に用意されている。

const SSL_METHOD *SSLv3_client_method(void);
const SSL_METHOD *SSLv3_server_method(void);
const SSL_METHOD *SSLv3_method(void);

const SSL_METHOD *TLSv1_client_method(void);
const SSL_METHOD *TLSv1_server_method(void);
const SSL_METHOD *TLSv1_method(void);

const SSL_METHOD *SSLv23_client_method(void);
const SSL_METHOD *SSLv23_server_method(void);
const SSL_METHOD *SSLv23_method(void);

これらの関数の説明はマニュアルのSSL_CTX_newの項に出ている。
http://www.openssl.org/docs/ssl/SSL_CTX_new.html
このうちSSLv23_*は、SSLv2、SSLv3、TLSv1の全てを扱えるというもの。でもSSLv2は使うべきではないので、SSLv2を禁止にするオプションと併用した方が良い。

  ctx = SSL_CTX_new(SSLv23_method()) ;
  SSL_set_options(ctx, SSL_OP_NO_SSLv2);
SSL_SESSION

セッションについてオブジェクト。以下の説明では登場しない。

SSL_SESSION *SSL_get_session(const SSL *ssl);
int SSL_set_session(SSL *ssl, SSL_SESSION *session);
void SSL_SESSION_free(SSL_SESSION *session);
SSL

コネクションについてのオブジェクト。

SSL *SSL_new(SSL_CTX *ctx);
void SSL_free(SSL *ssl);

OpenSSLを使う

だいたいの流れはこんな感じ。

  // ……いろいろあって、ソケットの接続が成功する。
  // 必要ならSSL/TLSなしでのやりとりをする。
  // SMTPやFTPS Explicitモードでは事前のやりとりが必要。
  // HTTPSやFTPS Implicitモードの場合は必要なし。

  SSL_load_error_strings();  // 必須ではない。
  SSL_library_init();        // ライブラリを使う前に必ず呼ぶ。
  
  ctx = SSL_CTX_new(SSLv23_method()) ;  // あるいはSSLv23_method()以外の関数。
  SSL_set_options(ctx, SSL_OP_NO_SSLv2);

  // SSL_CTX_load_verify_locations(ctx, ……)を使って、
  // 相手の証明書を検証するときに使うCA証明書の場所を指定する。
  // サーバ側は、相手に送る証明書チェーンを組み込んだり、
  // 暗号化のための秘密鍵を組み込んだりする作業があるが詳細は略。

  SSL_set_fd(ssl, socket_fd); // コネクションsslにソケットを結びつける。

  SSL_connect(ssl); // ハンドシェイクをおこなう。(クライアント側)
                    // サーバ側の場合 SSL_accept(ssl);

  // SSL_get_verify_result(ssl)を呼び、
  // 送られてきた証明書の検証結果をチェックする。
  // 検証そのものは、SSL_connect()、SSL_accept()
  // の呼び出し時におこなわれている。
  // 検証が成功しているなら0、失敗しているなら非0。

  // SSL_read()とSSL_write()を使って相手とやりとりをおこなう。

  SSL_shutdown(ssl);  // SSL/TLS接続をシャットダウンする。
  SSL_free(ssl);

  SSL_CTX_free(ctx);
  ERR_free_strings();

相手とのやりとりに使うSSL_read()とSSL_write()のインタフェイスは、Cのライブラリ関数fread()、fwrite()よりも、システムコールread()、write()に近い。

int SSL_read(SSL *ssl, void *buf, int num);
int SSL_write(SSL *ssl, const void *buf, int num);

証明書

ハンドシェイクをおこなう過程で、サーバから公開鍵が送られてくる。その公開鍵を使って秘密情報の共有をおこない、それを元に秘密鍵を作成し暗号化された通信をおこなう。
でも、送られてきた公開鍵そのものが、誰か別の悪意ある人の鍵かもしれない。別のサーバのふりをした偽サーバと通信していたとか、通信の中間に介入されて別の公開鍵とすりかえられていた(man in the middle攻撃)とか。したがって通信が安全なことを確かめるには、相手から送られてきた公開鍵が本当に通信相手のものかどうかの検証が必要になる。
SSL/TLSでは公開鍵は公開鍵証明書の形で送られてくるので、公開鍵証明書を検証することになる。
ただ、証明書とその検証に関することはかなり面倒なので、細かいことはあらためてまとめる。とりあえずは、相手から送られてきた証明書の中身を見ることについてだけ。

相手の証明書を得る

相手の証明書を得るには(当然ハンドシェイクの後で)、

X509* SSL_get_peer_certificate(const SSL *s);

を呼ぶ。送られてきた証明書チェーン(説明は略す)全体を得るには

STACK_OF(X509)* SSL_get_peer_cert_chain(const SSL *s);

を使う。ここでX509っていうのは公開鍵証明書の形式の名前。
ところでSTACK_OF(……)型を操作する関数は、ヘッダファイルを覗くとたくさん見つかるのに、ドキュメントが見つからない。使いそうなのは

X509* sk_X509_pop(STACK_OF(X509));
// スタックのトップから要素を取り出す。要素が無ければNULLを返す。
X509* sk_X509_shift(STACK_OF(X509));
// スタックの底から要素を取り出す。要素が無ければNULLを返す。
X509* sk_X509_value(STACK_OF(X509), int i);
// スタックの底から数えてi番目の要素を返す。底の要素はゼロ番目とする。
int sk_X509_num(STACK_OF(X509));
// スタックの要素数を返す。

あたりか。

証明書を見る

証明書全体を人が見やすい形で出力するには、

int X509_print_fp(FILE *bp, X509 *x);

が使える。
鍵の持ち主(subject)と証明書の発行者(issuer)=署名した人が見たいなら、

X509_NAME* X509_get_subject_name(X509 *a);
X509_NAME* X509_get_issuer_name(X509 *a);

int X509_NAME_print_ex_fp(FILE *fp, X509_NAME *nm,
                          int indent, unsigned long flags);

を使うこともできる。関数X509_NAME_print_ex_fp()の引数indentは複数行出力の場合のインデント幅。flagsは出力の形式をいろいろ指定するのに使う(http://www.openssl.org/docs/crypto/X509_NAME_print_ex.html)。
あと、画面に表示したりファイルに書き出すのではなく、文字列データとして得たい場合は、たぶんBIO(Basic I/O)という抽象データを使うのだと思う。
http://www.openssl.org/docs/crypto/bio.html
http://www.openssl.org/docs/crypto/BIO_read.html
http://www.openssl.org/docs/crypto/BIO_s_mem.html
たとえばこんな感じで。

  BIO* mem = BIO_new(BIO_s_mem());  // メモリBIOの作成。
  X509_NAME_print_ex(mem, name, 8, XN_FLAG_RFC2253);  // FILEでなくBIOに書き出す。
  BIO_gets(mem, buffer, buffer_size);  // BIOから読み出し、bufferに入れる。

もっと簡単な方法があってもよさそうだけど、わからなかった。

プログラム例: 証明書チェーンの表示

サーバと実際にやりとりするプログラムを書くのは面倒なので、ハンドシェイクをおこなったあと、相手の送ってきた証明書チェーンを表示するだけにした。相手自身の証明書を表示したあと、送ってきた証明書チェーンの各証明書の持ち主(subject)と発行者(issuer)を表示する。
Cは文法をだいぶ忘れているしソケットプログラムの知識が無いので、Gauche + c-wrapperで。
(c-wrapper-0.6.0より前のものだと、OpenSSLライブラリのヘッダファイルをうまく処理できないかもしれない)
print_cert_chain.scm

#!/usr/bin/env gosh

(use gauche.net)
(use gauche.parseopt)
(use c-wrapper)
(c-load "openssl/ssl.h" :libs "-lssl -lcrypto")

(define (main args)
  (let-args (cdr args)
      ((host "h|host=s" "localhost")
       (port "p|port=i" 443))

    (define socket (make-client-socket 'inet host port))
    (SSL_library_init)
    (let1 context (SSL_CTX_new (SSLv23_method))
      (SSL_CTX_set_options context SSL_OP_NO_SSLv2)
      (let1 ssl (SSL_new context)
        (SSL_set_fd ssl (socket-fd socket))
        (SSL_connect ssl)

        (print-peer-certificate ssl)
        (print-certificate-chain ssl)

        (SSL_shutdown ssl)
        (SSL_free ssl))
      (SSL_CTX_free context))))

(define (print-peer-certificate ssl)
  (let1 cert (SSL_get_peer_certificate ssl)
    (X509_print_fp stdout cert)
    (X509_free cert)))

(define (print-certificate-chain ssl)
  (define cert-chain (SSL_get_peer_cert_chain ssl))
  (let loop ((cert (sk_X509_shift cert-chain)))
    (when (not (null-ptr? cert))
      (print-subject-and-issuer cert)
      (loop (sk_X509_shift cert-chain))
      (X509_free cert))))

(define (print-subject-and-issuer cert)
  (format #t "\nSubject:~a\n" (name-string (X509_get_subject_name cert)))
  (format #t "Issuer: ~a\n" (name-string (X509_get_issuer_name cert))))

(define (name-string name)
  (define mem (BIO_new (BIO_s_mem)))
  (define p (make (ptr <c-uchar>)))

  (X509_NAME_print_ex mem name 0 XN_FLAG_RFC2253)
  (BIO_write mem "\0" 1)
  (BIO_get_mem_data mem (ptr p))
  (begin0
    (cast <string> p)
    (BIO_free mem)))

実行例はこんな感じ。

$ gosh print_cert_chain.scm --host=www.hatena.ne.jp
Certificate:
    Data:
        Version: 3 (0x2)
[証明書の内容が続くが省略]
Subject:CN=www.hatena.ne.jp,O=www.hatena.ne.jp,OU=Domain Control Validated,C=JP
Issuer: CN=GlobalSign Domain Validation CA,O=GlobalSign nv-sa,OU=Domain Validation CA,C=BE

Subject:CN=GlobalSign Domain Validation CA,O=GlobalSign nv-sa,OU=Domain Validation CA,C=BE
Issuer: CN=GlobalSign Root CA,OU=Root CA,O=GlobalSign nv-sa,C=BE

相手のCN(Common Name)はwww.hatena.ne.jpで、www.hatena.ne.jpの公開鍵に対して、GlobalSign Domain Validation CAが署名をしている。この署名が正しいものかどうかを確かめるには、GlobalSign Domain Validation CAの公開鍵が必要。
その公開鍵は証明書チェーンの2枚めの証明書から得られる。そして、2枚めの証明書の正しさを確かめるにはGlobalSign Root CAの公開鍵が必要になる。
あとはこちら側(クライアント側)でどうにかすることになる。例えばFirefoxにはGlobalSign Root CAの自己署名証明書(GlobalSign Root CA自身が署名したGlobalSign Root CAの証明書)が入っている。くわしいことは略。

次は比較して見るとちょっと変な感じがする実行例。

gosh print_cert_chain.scm --host=repository.secomtrust.net
[略]
Subject:CN=repository.secomtrust.net,serialNumber=011001040781,O=SECOM Trust Systems Co.\,Ltd.,1.3.6.1.4.1.311.60.2.1.3=#13024A50,L=Mitaka-shi,ST=Tokyo,C=JP
Issuer: CN=SECOM Passport for Web EV CA,O=SECOM Trust Systems CO.\,LTD.,C=JP

Subject:CN=SECOM Passport for Web EV CA,O=SECOM Trust Systems CO.\,LTD.,C=JP
Issuer: OU=Security Communication EV RootCA1,O=SECOM Trust Systems CO.\,LTD.,C=JP

Subject:OU=Security Communication EV RootCA1,O=SECOM Trust Systems CO.\,LTD.,C=JP
Issuer: OU=Security Communication RootCA1,O=SECOM Trust.net,C=JP

gosh print_cert_chain.scm --host=repo1.secomtrust.net
[略]
Subject:CN=repo1.secomtrust.net,serialNumber=011001040781,OU=Common PKI Platform,2.5.4.15=#131256312E302C20436C6175736520352E286229,O=SECOM Trust Systems CO.\,LTD.,1.3.6.1.4.1.311.60.2.1.3=#13024A50,L=Shibuya,ST=Tokyo,C=JP
Issuer: CN=SECOM Passport for Web EV CA,O=SECOM Trust Systems CO.\,LTD.,C=JP

Subject:CN=SECOM Passport for Web EV CA,O=SECOM Trust Systems CO.\,LTD.,C=JP
Issuer: OU=Security Communication EV RootCA1,O=SECOM Trust Systems CO.\,LTD.,C=JP

Subject:OU=Security Communication EV RootCA1,O=SECOM Trust Systems CO.\,LTD.,C=JP
Issuer: OU=Security Communication RootCA1,O=SECOM Trust.net,C=JP

Subject:OU=Security Communication RootCA1,O=SECOM Trust.net,C=JP
Issuer: emailAddress=info@valicert.com,CN=http://www.valicert.com/,OU=ValiCert Class 1 Policy Validation Authority,O=ValiCert\, Inc.,L=ValiCert Validation Network

同じような証明書チェーンを送ってきているのだけど、上は発行者がSecurity Communication RootCA1のところまで送ってきていて、下はそこからさらにもうひとつ証明書を送ってきている。