OpenSSLライブラリを使ってプログラミング(3) 証明書・検証

OpenSSLライブラリを使ってプログラミング(1)
OpenSSLライブラリを使ってプログラミング(2) 証明書・検証についての予備知識
の続き

証明書ファイルの形式

証明書のエンコーディングにはPEM形式とDER形式(ASN.1形式)があるみたいだけど、OpenSSLではPEM形式の方が扱いやすいみたい(※PEM = Privacy Enhanced Mail、DER = ASN.1 Distinguished Encoding Rules)。PEM形式の証明書は次のようなもの。

-----BEGIN CERTIFICATE-----
MIICFzCCAYACBAht6ywwDQYJKoZIhvcNAQEFBQAwYjELMAkGA1UEBhMCSVQxETAP
BgNVBAgMCENhbGFicmlhMRowGAYDVQQKDBFQeXRoYWdvcmVhbiBPcmRlcjEkMCIG
A1UECwwbUHl0aGFnb3JlYW4gUmF0aW9uYWwgUm9vdENBMB4XDTAwMDEwMTAwMDAw
MFoXDTAxMDEwMTAwMDAwMFowQzELMAkGA1UEBhMCSVQxEzARBgNVBAgMCkJhc2ls
aWNhdGExHzAdBgNVBAMMFkhpcHBhc3VzIG9mIE1ldGFwb250dW0wgZ8wDQYJKoZI
hvcNAQEBBQADgY0AMIGJAoGBAMJD5JC1ArS48fTf8xWNMFxqH58rsfkizYc9YPWC
chuRc5SGnNhy6zNSSfrFXTPATyr1FbcoIsTQ96p5QvfAvU8gz+OgBAF/hnpzZc3d
6+QZ8RJsVgsT6j9bSy0asZCP7RSqzeXtFGyrdCh3P6J5Apm40rY2V0hG6n+xICm4
rOArAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAHzy4XISzsfqPNx3Pgkr97qsWWFHK
2aSUWpccUjlc7hEWxcXNznPndNOODWzUO4UuoHNi73VtKHX7dkLoj+QCaTFbj5x2
VFwBIx3GSFPzL8FYl8kv0sCTXHdb+0hFFrhhNCjyQZ1qUuFRabTVjqFvr+XXsrMV
C86x/zYa3hSpGDU=
-----END CERTIFICATE-----

OpenSSLでは、BEGIN行とEND行とに囲まれていない部分にはコメントを書いても良いみたい。
DER形式の証明書は、あらかじめPEM形式に変換しておけば良い。opensslコマンドで変換する場合は、

openssl x509 -inform der -in 入力ファイル -outform pem -out 出力ファイル 

のようにする。
また自分の公開鍵証明書を使う場合、それに対応する私密鍵の読み込みも必要になるのだけど、鍵自体をパスワードで暗号化しておいた方がセキュリティ的には望ましい。暗号化されている場合は次のような感じになる。

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-CBC,F9DB17D22FB4CA95

BSpr3Ia9+poEruFvfiUvyYdEpXK2PA8tfvvBfBah2jno10OIG6xhtjR5UcJ0RmUe
lXKeWIgq0yOb0ix1n0BA2ZlWNW24g6QkPbivK8/VoHHuRFuvobcAUzo6dk5yErEI
bc0j7PGjkGs4UHjztYe+IIvLy3KjrAnu4CskSaCFSALuklaZzyCPD2fRKWrY9K3b
0WYr3wcjIRAuh1CIXDfGUaXcJFz1yIHhTSSsonRQ/y5VxKLxDfg76+X067pooiPx
l0OioFlOt5k9N1kUc9G1YMrSL1p1SNMZ2VOAIU2T2OhoaMvRseSW6NKPGhT0XePW
EyCeuUJ1Z3ZrNzkHe6/X2pGJtSrkVXjOxZAjHBE605FryZBIxdfM5h3UbV5vPIUB
U6v2ZpJEuJzW3cCNJSdeqtmPjT1jegm6SoFgo9FT555UbLBQhmcSVCP71MCUzNLL
QYxP6LLKB+pQmF7M35TZlay9YzDMXGDUzV38jQpOdlOvWqHXEgg4UOABImCBi0Ul
z9YYzUgKEMHQ9MSK6OXGcWSlpYiv7poIS6x9bDHND5c39a3ej27BzXQ56hWEvwBp
LiwevvqlfYedneokl/B0gyBxTTZ+RUW96GPqf9y6vqDB+2wd5/Wighmzg19FhMsX
Yy1x8A5n6uB3a3Ttf3AR7oPww4yLHUnQ8nvPKwVtgXN3/zKTqHwz6NaB6uQH30fn
FYo3u2+k8auyno1HNesdIL7poif92Zzs1SfUSTvSOVA1xW/NiweAehTFF7R5cT2z
oMDi83N7yuvgcyxJtzwCV9E+pwviHUgxP0RVnH2LZOVFrpfFt0CtDw==
-----END RSA PRIVATE KEY-----

相手に送る証明書チェーンと自分の私密鍵の読み込み

SSL/TLSで通信をおこなう場合、サーバ側はクライアント側に自分の公開鍵証明書を送る。クライアント側も、サーバ側が要求する場合には自分の公開鍵証明書を送らないといけないけど、そういうことはほとんどないと思う。たぶん。
そして証明書を受け取った側は、証明書を検証する。
でもOpenSSLライブラリを使ってプログラミング(2)に書いたように、証明書を検証する場合には、相手の証明書だけでなく、その証明書を発行したCAの証明書、そのCAの証明書を発行したCAの証明書、等々が必要になる(このような証明書の列を証明書チェーンという)。そのため、証明書を送る側は、検証を容易におこなえるように自分の証明書だけでなく、その証明書を発行したCAの証明書(以下略)、を送ることができる。この時、証明書チェーンの終わりにあたる自己署名証明書(ルート証明書)は送っても検証には役に立たないので、送っても送らなくても良い。また、証明書チェーンの途中までしか送らないということも可能(そうするメリットはあまり無いと思うけど)。
OpenSSLでは、自分の証明書の証明書チェーンを読み込むために次の関数を使う。読み込むファイルは、PEM形式の証明書を並べたもので、自分の証明書が一番最初で続く証明書も証明書チェーンの順番になっていないといけない。

int SSL_CTX_use_certificate_chain_file(SSL_CTX *ctx, const char *file);

それと、自分の公開鍵証明書を使う場合、それに対応する自分の私密鍵も必要になる。私密鍵を読み込む関数は次のもの。

int SSL_CTX_use_PrivateKey_file(SSL_CTX *ctx, const char *file, int type);

typeは定数SSL_FILETYPE_PEMまたはSSL_FILETYPE_ASN1を指定する。読み込んだ鍵が暗号化されていると、デフォルトでは、プロンプトが出てパスワードを入力するようにうながされる。この動作を変える場合は、

void SSL_CTX_set_default_passwd_cb(SSL_CTX *ctx, pem_password_cb *cb);

を使う。くわしいことは省略する(http://www.openssl.org/docs/ssl/SSL_CTX_set_default_passwd_cb.html)。それと、パスワードを間違えると、鍵が使えないので、相手に証明書を送らないという動作になる。
証明書や鍵を読み込む関数は他にもたくさんあるみたい(http://www.openssl.org/docs/ssl/SSL_CTX_use_certificate.html)だけど、とりあえず上の関数だけで足りると思う。

信頼するCA(認証機関)の証明書の組み込み

相手から証明書(証明書チェーン)を受け取った側は、相手の証明書が信頼できるものかを検証する。その検証のためには、自分が信頼するCA(認証機関)の証明書が必要になる。
なのだけど、どのCAが信頼できるのかを決めたり、どうやってCAの証明書を手に入れるのかよくわからない(webブラウザに入っている証明書をエクスポートする、でも良いのか?)。
でもここでは、信頼できるCAの証明書は手に入っているとする。
信頼する証明書は次の関数で読み込む。

int SSL_CTX_load_verify_locations(SSL_CTX *ctx, const char *CAfile, const char *CApath);

http://www.openssl.org/docs/ssl/SSL_CTX_load_verify_locations.html

SSL_CTX_load_verify_locations()を呼ぶタイミング

SSL_CTX_load_verify_locations()で組み込んだ証明書は送られてきた証明書の検証に使うのだけど、証明書の検証はハンドシェイクの時に自動でおこなわれる。そのためこの関数の呼び出しは、SSL_connect()を呼ぶ前でないといけない(サーバ側なら、SSL_accept()を呼ぶ前)。

SSL_CTX_load_verify_locations()の第2引数と第3引数
  • 第2引数CAfileには証明書ファイルの名前を指定し、第3引数CApathには証明書ファイルの置いてあるディレクトリ名を指定する。
  • 片方だけ指定しても良い。指定しない場合はNULLにする。
  • 第2引数で指定する証明書ファイルには、複数の証明書が入っていて良い(各証明書はPEM形式)。
  • 第3引数で指定するディレクトリに置かれた証明書ファイルは、各ファイルにつき一つの証明書だけを含んでいること。また各ファイル名は、証明書の持ち主の名前(subject name)のハッシュ値になっていないといけない。ディレクトリに対してc_rehashコマンドを実行すると、サフィックスが.crtと.pemのファイルに対して、ハッシュ値を名前に持つシンボリックリンクを作成してくれる。
  • 関数の戻り値は証明書の読み込みに成功なら1、失敗なら0。第2引数と第3引数を両方ともNULLにすると0になる。第2引数に存在しないファイル名を指定すると0になる。でも存在しないディレクトリ名を指定しても0にはならない(ディレクトリで指定された場合、必要になるまで証明書の読み込みをおこなわない)ので注意。

証明書の検証

SSL_connect()でハンドシェイクをおこなうと、送られてきた証明書の検証を自動でやってくれる(検証の仕方を独自のものにしたい場合は、SSL_CTX_set_cert_verify_callback()関数で設定する。とりあえずは使わないのが無難)。
検証した結果は

long SSL_get_verify_result(const SSL *ssl);

で得られる。成功なら0、失敗なら0以外の値(失敗理由を表す整数)。ただし注意がいる。
もしも相手が証明書を送ってきていない場合、SSL_get_verify_result()は0を返す(つまり検証成功となる)。なのでSSL_get_peer_certificate()を呼んで戻り値がNULLでないこと(相手が自分の証明書をおくってきたこと)もチェックしておかないといけない。
それとSSL_get_verify_result()が成功を返したというのは「相手が送ってきた証明書は信頼できる認証機関から発行されたものです」といっているにすぎない。中間攻撃者もちゃんとした認証機関が発行した証明書を用意したり不正入手したりして、検証に成功する証明書を送ってくることができる。
だからSSL_get_verify_result()の結果の確認とは別に、相手の証明書の持ち主(subject name)部分を調べて、実際に通信したい相手なのかをチェックする必要がある。

SSL_get_verify_result()が0以外の場合

SSL_get_verify_result()が0(成功)以外の場合、失敗理由を表す整数コードが返ってくる。結果コードのそれぞれが何を意味しているのかは、マニュアルのverify(1)のところに出ている。
http://www.openssl.org/docs/apps/verify.html#DIAGNOSTICS
また、

const char* X509_verify_cert_error_string(long n);

にコードを入力すると、そのコードが表している失敗理由を文字列で得ることができる。
ただしこの失敗コードはそれだけではそんなに役には立たないと思う。証明書チェーンの中の複数の証明書のどの証明書で検証が失敗したのかわからないし、検証に失敗する理由が複数あってもそのうちの一つを返すだけなので。
検証のデフォルトの実行では、検証失敗となった時点で検証をやめ、それ以上検証をおこなわない。だから返される失敗コードは、検証の過程で最初に起こった失敗の理由を表す。
検証が失敗してもそのまま検証を続けるようにすることもできる(SSL_set_verify()関数を使って)けど、その場合は、検証に失敗するごとに失敗コードが上書きされていく。その結果、SSL_get_verify_result()が返すのは、複数の検証失敗のうち最後に起こったものを表すコードになる。

検証動作を少し変更する

デフォルトの検証の仕方を少しだけ変更したい場合や、検証の失敗理由を詳しく知りたい場合、

void SSL_set_verify(SSL *s, int mode, int (*verify_callback)(int, X509_STORE_CTX *));

を使う。第2引数のmodeをSSL_VERIFY_NONEにすると標準の動作。他のモードの説明は省略する。
第3引数verify_callbackは、検証中の特定の時点で呼び出される関数

int verify_callback(int preverify_ok, X509_STORE_CTX *x509_ctx);

で、これが呼び出されるタイミングは

  1. (証明書チェーンのうちの)一つの証明書の検証が終了した時点(証明書の検証成功)
  2. 検証の失敗がおこった時点

のどちらか。第2引数preverify_okが1なら成功時の呼び出しで、0なら失敗時の呼び出し。
verify_callbackの戻り値については、1を返すと検証が続行され、0を返すとそこで検証が終了する。デフォルトの関数はpreverify_okを返している。
検証のくわしい状況は第3引数x509_ctxを調べるとわかる。

X509* X509_STORE_CTX_get_current_cert(X509_STORE_CTX *ctx);

で今調べていた証明書が得られる。また

int X509_STORE_CTX_get_error(X509_STORE_CTX *ctx);

によって失敗コードが得られるのだけど、失敗コードはただ上書きされていくだけ、ということに注意がいる。つまり、ある証明書の検証が成功した直後にこの関数を呼んでも、それ以前の証明書の検証に失敗していると0以外の値が返ってくることになる。これを避けるには、検証に失敗したときに、

void X509_STORE_CTX_set_error(X509_STORE_CTX *ctx,int s);

を使って、失敗コードを0に戻しておけば良い。ただし0にセットした後、全ての検証に成功すると、最終結果(SSL_get_verify_resultの返す値)も0(検証成功)になってしまう。逆に考えれば、この関数を使えば検証結果の変更ができるということにもなる。

プログラム例

OpenSSLライブラリを使ってプログラミング(1)に続き、プログラムはGauche + c-wrapper。
サーバ側は、証明書チェーンを送ったあと、クライアントが送ってくる内容をただ表示していくだけ。クライアント側は、サーバから受け取った証明書の検証して、検証失敗ならそこで終了し、検証できたら、あとはキーボードからの入力をただサーバ側に送りつづけるだけ。
以下のファイルを用意する。

  • サーバ側: 相手に送る証明書チェーン chain.crt
  • サーバ側: 自分の私密鍵 server_key.pem
  • クライアント側: 信頼するCAの証明書のリスト trustedCAs.crt

これらのファイルのサンプルは以下。
chain.crt

server's certificate
-----BEGIN CERTIFICATE-----
MIIBnDCCAQUCBAht6ywwDQYJKoZIhvcNAQEFBQAwGTEXMBUGA1UEAwwOaW50ZXJt
ZWRpYXRlQ0EwHhcNMDUwMTAxMDAwMDAwWhcNMjUwMTAxMDAwMDAwWjARMQ8wDQYD
VQQDDAZzZXJ2ZXIwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOep42FXEgy3
5GdJ4XMjMtb86unHqWU81qczvwbfFqDL2XjXqyYcMJiMcY87BzYqgnObpWi4nE/5
o/JJm9bI03j5Z9u2pK5DqevG85a3eBxrdYxmCVp8onzY3KDP28rohFjTKAf7eqJl
gYYo/9q1lR6oYc+yjAatE/BjZJQF/wwfAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEA
KOg3VnMaKpmVhF3wPrpi7kWbn+qSlhvummAbU1rmbgDMBFYUlYN7/3lkyoTbvDnO
yuKDV+tcJhBH6C7AgMVLWDoTlw+Wa1PVMVUZuztElql6GiwTuatH49rIJfooQMhk
tiNLKWgRSqGPbw8XL72hkUEX1b7QCfQFHS/eL7K2S7U=
-----END CERTIFICATE-----

intermediateCA's certificate
-----BEGIN CERTIFICATE-----
MIIBrjCCARcCAQAwDQYJKoZIhvcNAQEFBQAwETEPMA0GA1UEAwwGcm9vdENBMB4X
DTA1MDEwMTAwMDAwMFoXDTI1MDEwMTAwMDAwMFowGTEXMBUGA1UEAwwOaW50ZXJt
ZWRpYXRlQ0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALYPOegYOYclbjWl
WrUefg3R9r6CPIFY0r4YXttRivfZkMmewBFksIWMFyeChdnbheVnzQAhroIH1YDF
kxWV8JjzBcAK6PwQIXMiOSPFSME/rHrMNkakWg1ZaYYBRnEXhUd2uYnLqZpbj3H/
sdKLOhfMw7DnTfm8Dexu+AoTXesNAgMBAAGjEzARMA8GA1UdEwEB/wQFMAMBAf8w
DQYJKoZIhvcNAQEFBQADgYEAVIB+Dj8hjFnrXo8l7IeKBzzfP9cPr3XN8kMsHLEs
+Y+xO6CfflEwlBfzpVjSWc2O8kNsxeSA2U+oqsqVyXENroj4EVufIq81sYIgPVkH
F3dIOaEWIzT6dpHCvDf9ewsc+FTKK5U+p7wBq74ddAuJ8+W2k54o/0uQ1JxVG/UU
oGI=
-----END CERTIFICATE-----


server_key.pem (パスワード: pass)

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-CBC,84BE9425EC8669A3

h7RqcGB2LH7GZmszOi/ZjBADfphsEvZtnGmcozvLZw+S01HoW13RlKJrbZz2Lus1
Xi12xAFleVSWNGwaRMMYAcantQiDfARppNZVABKnuqAgQuSHz4tK57Whlf7rxoFV
brOfAan8irGmm2fZkSmDNYzl0Glgqzl+tS+6chhsgL5CSxOdM5aYBNXn1bps7AaG
ZyUNaQPB0Gjv3VfVvoZgrJe/tbyf2E6jb9ngVCT6Hv+8aDUzGBt0+nwnDUENfE+I
nu79ureUNvqKml3iHkrLiRAsxD0RjbbKRNAR5AWUXEbeUvR+nF22/vIhIaue6ZV7
N7JRKUc3ktiwhdn158N9/wPRLuP/gp0XZfZxDUgroDAKMisbUay4TMV7skE59jyz
sXDZgc/za1+9M7Si9z9+KeIolw5IpLoJXsKPl+fv++ddFVkxPHEjExzz5hCgrpP6
JLpTSpj0Ui4X6iwQ7akVW2FaCU9da0q8/zfxCIPtdHlY1hcCUucuB+6AaqgDbSqy
UzQuriU/HcnfIb0GPjzYxumkmmCkyP30uWEf26V92Ju5qbfKviDvCUkQT2iF1R32
SxGXyRgphuzG0/j/9ZmKfz0AMvjTFHMaDkTFKu+N+SdEN7YJ1Oao+XYcuR9oftIA
xd2Z7nsUDjLCZrTxgw+zMTC6Dix8+Ns4NDbij++NVulWVQy6FlEgS/XyU3wbsW7S
OhbeC9CTu4IJQhbPG8nJKBrmT+3nprqRXk7tocQLp6tT0rUPg2K2sXvdaqgWLDtW
YOhVD9c3BwODMrXZr6H/+OcSOI4Q2ZOS2GxOwF97Iv7AG4Pp4njfHg==
-----END RSA PRIVATE KEY-----


trustedCAs.crt

rootCA's certificate
-----BEGIN CERTIFICATE-----
MIIBpjCCAQ8CAQAwDQYJKoZIhvcNAQEFBQAwETEPMA0GA1UEAwwGcm9vdENBMB4X
DTA1MDEwMTAwMDAwMFoXDTI1MDEwMTAwMDAwMFowETEPMA0GA1UEAwwGcm9vdENB
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDfaDejoFdpRK6mhxe+IL2wQ2m
Zx+B5UQxZAW3agwL4vA1F+jo2nhRasqzsTcv5WcEQG0tz9453jua3zGpepS1688T
sp8ptuDbT/2893gzEW4v+QaFs3S9ZWPehTOVz7gDGOjgTxLiLO/YQOvBTJW4RB8u
PqM3gCWMDX1/np1E6QIDAQABoxMwETAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
DQEBBQUAA4GBAKU1tiS4/pFVctrJUsH7FSG8AvGkN44gO0uB88yMu0GozNRd5rqs
Oyx8huY+b3lDdw+WeizdNfElpcjcFgfvBM8p8TAyOSBnq/ZFtpxjtd4MkTFEWrSU
xEcYwqqBWguns0VSp9BfZbQk/1E1h9aYzdbEWgzlsdg1TAprq+8Nsuda
-----END CERTIFICATE-----

なお証明書や鍵の内容を見たい場合、opensslコマンドを次のように使えば良い。

openssl x509 -in 証明書ファイル -text
openssl dsa -in 鍵ファイル -text

次にプログラム。
まずはサーバのプログラム。
server.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)
      ((port "p|port=i" 9999))

    (define socket0 (make-server-socket 'inet port :reuse-addr? #t))

    (SSL_library_init)
    (let1 context (SSL_CTX_new (SSLv23_method))
      (SSL_CTX_set_options context SSL_OP_NO_SSLv2)

      (SSL_CTX_use_certificate_chain_file context "chain.crt")
      (SSL_CTX_use_PrivateKey_file context "server_key.pem" SSL_FILETYPE_PEM)

      (let accept-loop ((socket (socket-accept socket0))
                        (ssl (SSL_new context)))
        (SSL_set_fd ssl (socket-fd socket))
        (SSL_accept ssl)

        (let* ((buffer-size 1000)
               (buffer (make (c-array <c-uchar> buffer-size))))
          (let read-loop ((read-size (SSL_read ssl buffer buffer-size)))
            (when (> read-size 0)
              (print (cast <string> (ptr buffer)))
              (read-loop (SSL_read ssl buffer buffer-size)))))

        (SSL_shutdown ssl)
        (SSL_free ssl)
        (accept-loop (socket-accept socket0) (SSL_new context)))
      (SSL_CTX_free context))))

次がクライアント側。
client.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" 9999))

    (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)
      (SSL_CTX_load_verify_locations context "trustedCAs.crt" (make-null-ptr))

      (let1 ssl (SSL_new context)
        (SSL_set_fd ssl (socket-fd socket))
        (SSL_connect ssl)

        (when (or (not (peer-certificate-received? ssl))
                    (not (peer-certificate-verified? ssl)))
            (format #t "Certificate chain is not verified\n~a\nverify: "
              (cast <string>
                (X509_verify_cert_error_string (SSL_get_verify_result ssl))))
            (exit 0))

        (print "Please input")
        (let loop ((line (read-line)))
          (when (not (eof-object? line))
            (SSL_write ssl line (string-size line))
            (loop (read-line))))


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

(define (peer-certificate-received? ssl)
  (let1 cert (SSL_get_peer_certificate ssl)
    (cond ((null-ptr? cert) #f)
          (else (X509_free cert) #t))))

(define (peer-certificate-verified? ssl)
  (zero? (SSL_get_verify_result ssl)))