IO::Socket::SSL で証明書の検証をする

久々に更新。
Twitter でぼちぼち書いてたけど、まとめておこうかなと。

IO::Socket::SSL には、接続先の証明書を検証する機能があるが、検索してもあまりその使い方が出てこない。いつかのバージョンで、「SSL_verify_mode」を設定せずに使おうとすると警告が出るようになったため、「とりあえず SSL_VERIFY_NONE に設定したら動くよ」っていう情報があちこち見つかるのだが、「SSL_VERIFY_PEER」を使うのが推奨されているにもかかわらず、そうした場合に証明書をどうやって検証するかっていう情報がなぜかあまり出てこない。
証明書の検証をするためには、SSL_ca_file あるいは SSL_ca_path で CA の証明書のファイルが入っている場所を指定すればよい(ドキュメント)ということなのだが、どこにどういう形式で保存されているかは OS によって異なる。また、Web ブラウザとかもそれぞれ持っていたりする。Perl で使うのだから、OS による違いを気にしなくてよいような方法を採りたいところ。
探してみると、Mozilla::CA というモジュールを見つけた。モジュールには Mozilla が使用している CA の証明書のファイルを変換したものが含まれておりそれを、IO::Socket::SSLSSL_ca_file に指定できる(Mozilla::CA::SSL_ca_file() で)ようになっている。これならば OS 間の違いを気にすることなく動くではないか。すばらしい。モジュール自体のバージョンはちょっと古いが、証明書のファイルを更新するためのスクリプトも含まれており、モジュールのバージョンアップが止まっても更新していくことができそうだ。
と、一旦はうまい方法を見つけたと思ったのだが、Mozilla::CA のバグ情報に気になる情報を発見。Mozilla::CA に含まれている更新用のスクリプトが古く、参照しているファイルがずっと更新されていないというではないか。
どうも、この更新用スクリプトcurl に含まれているもののようで、curl の最新バージョンから持ってきたものを使えば新しいファイルを入手することができるらしい。ということで試してみると、ちゃんと更新され、うまくいった、かに見えた。というか、最初に試した imap.gmail.com に対しては問題なく動いた。
これで一安心かと思ったが、imap.mail.me.com で試すと、エラー。原因はわからなかったが、IO::Socket::SSL のバージョンを落とすとエラーが起らなくなったためモジュールが悪いのかと考えた。いくつかのバージョンを試していくと、1.974 までは大丈夫で、 1.975 からエラーが起こることがわかった。
1.974 と 1.975 の違いを見ると、Mac OS X だけのために修正された部分を見つけた。そこに示されたリンクを辿ると、Apple が OpenSSL に独自のパッチをあてており、指定された証明書ファイルで検証ができないばあいに、OS が持っているものを使って検証するようになっている、ということらしい(たぶん)。で、1.974 まではその機能が有効になっていたが、1.975 ではそれが働かないようになった、ということのようだ。要は、バージョンが変わったことが原因ではなく、新しいバージョンの mk-ca-bundle.pl で作成したファイルでは、証明書の検証ができていなかったのだ。
その後、OpenSSL とともにインストールされたと思われる「/usr/local/etc/openssl/cert.pem」を指定した場合には問題ないことがわかり、さらに、Mozilla::CA に最初から入っていたものでも問題ないことがわかった。そこから先がなかなかわからなかったのだが、本日、mk-ca-bundle.pl を読んでいたところ、「-p」オプションで証明書の利用目的とレベルの指定ができることがわかった。試しに、すべてが含まれる、「-p ALL:ALL」を指定すると、問題なく動いた。いろいろとオプションを試していくと、「-p EMAIL_PROTECTION:TRUSTED_DELEGATOR」の指定でエラーなく検証できることを発見。デフォルトの「SERVER_AUTH:TRUSTED_DELEGATOR」ではだめなのが納得いかないが、まあ、とりあえず。
では、どの部分が原因だったのか。「openssl s_client -showcerts -connect imap.mail.me.com:993」を実行してみると、「*.mail.me.com」の issuer は「VeriSign Class 3 Secure Server CA - G3」となっており、そこから、「VeriSign Class 3 Public Primary Certification Authority - G5」、「Class 3 Public Primary Certification Authority」という順で検証されているらしい。ここで、mk-ca-bundle.pl のデフォルトで作ったファイルを見ると、「G3」と「G5」はあるが、素のものはない。その元になっている「certdata.txt」を見ると、

CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_MUST_VERIFY_TRUST
CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR
CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST

となっている。EMAIL_PROTECTION については TRUSTED_DELEGATOR だが、SERVER_AUTH では MUST_VERIFY_TRUST だそうだ。んで、MUST_VERIFY_TRUST ってのはどういう意味よ、と検索してみたのだが、結局よくわからなかった。信頼してもよいかどうか検証しろよってことなのかしら?
なんだか釈然としないが、現状では上に書いたように EMAIL_PROTECTION の方を使えばうまくいくので他の方法が見つかるまではこれでいくしかないのかな。というか、Perl で書かれていてマルチプラットフォームで動くものならみんな同じ条件のはずだけど、全然情報が見つからないのはなぜなんだろう。証明書の検証なんて別に、、、ってことなの? 「IO::Socket::SSL VERIFY_PEER」でぐぐると、トップは CPAN の IO::Socket::SSL のページで、次が私が POPFile のフォーラムに書いた書き込みなんだよね。。。
他のモジュールではどうしているんだろうと、LWP を見てみたら、LWP::Protocol::https にその処理が入っているらしい。内容はというと、SSL_ca_file または SSL_ca_path オプションが指定された場合はそれを、指定がないばあいは Mozilla::CA を使うというもの。Mozilla::CA に含まれているファイルが古いだなんてことはまったく気にされていないよう。んー、それでいいのか?? まあ、それがいやな人は SSL_ca_file とかのオプションを使えばってことなのかね。


ということで、Perl で IO::Socket::SSL を使っていて、マルチプラットフォームで動く証明書の検証方法のベストプラクティスを誰か教えてください。。。