あるデジタル証明書が信頼するルート証明書から辿って有効であるかどうか判定するために認証パス検証(Certification Path Validation)という処理を行います。

これは、HTTPSで保護されたサイトに接続する時やS/MIME署名メールを開く際に実は裏で行われているとっても重要な処理です。

Java ではCertPathBuilderCertPathValidatorというクラスがあり、仕組み的にはX.509公開鍵証明書に限らず汎用的に証明書のパスが検証できたり、いろいろな会社の実装したアルゴリズム、実装方法が選択できるようになっています。

Sun Java にはRFC 3280準拠のパス検証実装が含まれており、これに基づくX.509証明書のパス検証ができるようになっています。


CertPathBuilderのSunプロバイダのPKIXアルゴリズム
検証対象証明書、中間証明書、CRL、証明書やCRLを取得するLDAPディレクトリ、OCSPレスポンダの設定、トラストアンカとなるルート証明書群を指定することにより、自動的にパス構築と(狭義の)パス検証を同時に行うことにより検証対象証明書のパスが有効であるかを判定します。内部でCertPathValidatorの実装が使われているわけではありません。
CertPathValidatorのSunプロバイダのPKIXアルゴリズム
検証対象証明書からトラストアンカとなるルート証明書の一つ手前までの証明書のチェーン(CertPath)を与えることにより、(狭義の)パス検証を行います。


Sun PKIXのCertPathBuilderのパス構築をザックリ図にしたのが以下、、、

pathbuild02



SunプロバイダのCertPathBuilderのPKIXアルゴリズム実装なんですが、これが、またちょっとイケズな実装になっていてエラーメッセージというか例外処理がとても不親切なんです。

パスを構築するための証明書が不足していたり、CRLが取得できなかったり、失効していたり、期限切れだったり、鍵用途が間違っていたり、中間CA証明書にあるべき基本制約のcAフラグがTRUEでなかったりすると例外が発生しますが、どんな理由であっても全く同じ

sun.security.provider.certpath.SunCertPathBuilderException: 
unable to find valid certification path to requested target


という例外メッセージが表示されます。これじゃ何がエラー理由だったのかさっぱりわからない(^^;メッセージ "unable to find valid ..." でぐぐってみると皆さんエラー理由がわからず困っておられる様子、、、、

CertPathBuilderExceptionクラスではgetCause()メソッドにより、その例外が発生した元の原因となる例外を取得できるんですが、Sunの実装では深さ優先探索のせいか?!不親切なのか?!これが設定されていないため、パス構築・検証に失敗した原因がわからないんです。

パスの深さ優先探索とは


SunのCertPathBuilderのPKIXアルゴリズムの実装ではパスを見つける際、幅優先探索と深さ優先探索のうち深さ優先探索が使われています。(人工知能系の方、Prolog系の方はよくご存知、、、(^^;)

以下のような少し複雑なPKIモデルで考えて見ましょう。

cpb01



ここでは信頼するルート証明書からエンドエンティティ証明書まで3通りのパスがあるんですが、そのうち2つは無効なパスになっています。

・ROOT→AAAのCA証明書は証明書有効期限が切れているので無効
・ROOT→BBBのCA証明書は基本制約にcA=TRUEが無いため無効

SunのCertPathBuilderでパス構築した場合の、パス構築で辿ってみた順序を図説したのが以下です。

cpb02



Sun実装ではエンドエンティティ証明書から始めてトラストアンカであるルート証明書まで深さ優先探索でパス構築を試みます。深さ優先とは、ぶっちゃけちゃうと「行ける所まで行く」、
踏み出せばその一足が道となり、その一足が道となる。迷わず行けよ。行けばわかるさ。ありがとう!
的なアントニオ猪木の名言の様なアルゴリズムです。

(1) エンドエンティティ証明書HHH←GGGからパス構築開始
(2) CA証明書 GGG←EEE、EEE←BBB を辿る。
   その際に都度、個々の追加される証明書に対して署名値、
   識別名前の一致、鍵用途、各種制約の処理失効検証などを行う。
(3) BBB←ROOT証明書の基本制約拡張にcA=TRUEが無いのでGGGに戻る
(4) CA証明書 GGG←DDD、DDD←AAA を辿る。
(5) BBB←ROOT証明書は期限切れなのでGGGに戻る。
(6) CA証明書 GGG←FFF、FFF←CCC、CCC←ROOT を辿る。
(7) トラストアンカまで辿れたので(6)が有効なパスとなり
   パス構築・検証は完了。

幅優先の場合、途中選択肢を全て抱えたまま探索するので多くメモリを消費すると言われています。そうした意味では深さ優先はまぁ、妥当かなと、、、

Sun PKIX CertPathBuilder実装の例外のイケズ



ただ、次のようなケースの場合、パス構築を試みて結局は有効なパスが見つからないため「unable to find valid certification path to requested target(要求された対象(証明書)に対する有効な認証パスが見つかりません)」ということになります。

cpb03



この際のCertPathBuilderException例外のgetCause()には何も入っていないので、何故パス構築・検証に失敗したのかは(普通は)謎のままです。せめてパス構築失敗の元を最後に起こした例外がgetCause()に設定されていれば結構理由がわかるもんなんですけど、残念ながらそうなってません。深さ優先探索なので「前の例外なんかいちいち保存しておくかぁ!!」という実装なんでしょう。

最後に発生した例外をgetCause()に設定するだけだと下のようなケースでは本質的な問題がわからないケースもあるんですが、まぁ、それは現状の何も無いよりはましみたいな感じで残しておいて欲しいなぁ、、、、

cpb04



パス構築で辿った順序が何故わかるか?



では、何故上の方の図でSun PKIX CertPathBuilderが辿ったパス構築順序がわかったかというとログを見たからなんです。CertPathBuilderやCertPathValidatorでは、以下のように "-Djava.security.debug=certpath"のオプションを付ければデバッグログが標準出力に表示されます。
% java -Djava.security.debug=certpath [CertPathを使ったJavaプログラム]


ただ、ここから出てくるログは今まで見てきたログの中では「中の下」ぐらいのダメさ加減で、多分観てもうんざりするだけだと思います。

で、パス構築で辿った順を観るには以下ようなdepthFirstSearchForwardの部分を見ればよいです。


certpath: SunCertPathBuilder.depthFirstSearchForward(CN=HHH, C=JP, State [
issuerDN of last cert: null
traversedCACerts: 0
init: true
keyParamsNeeded: false
subjectNamesTraversed:
[]]
)
・・・中略・・・
certpath: SunCertPathBuilder.depthFirstSearchForward(CN=ROOT, C=JP, State [
issuerDN of last cert: CN=ROOT, C=JP
traversedCACerts: 3
init: false
keyParamsNeeded: false
subjectNamesTraversed:
[CN=GGG, C=JP, CN=HHH, C=JP, CN=CCC, C=JP, CN=FFF, C=JP]]
)


これを観ていくとHHH→GGG→EEE→BBB×後戻り→DDD→AAA×後戻り→FFF→CCC→ROOT○と二度の失敗にもめげずに辿ったんだぁなぁ、、、頑張ったなぁ、、、、よしよし、、、という風に感慨深いものがあります。

ログを観てもAAA←ROOTやBBB←ROOTの証明書で基本制限に違反しているからとか、期限切れだからとか、そうした理由はログには全く書かれていません。

3本パスがあるうち何故その順序で選んだか?



HHH→GGGと辿って、次にEEE、DDD、FFFのどれを選んでも良いような気がしますが、必ずEEE、DDD、FFFの順序になりました。

証明書を探す際の入れ物としてArrayListをベースにしたCollectionCertStoreを使っていたんですが、そこに中間証明書を加える順序に依存しているのかもしれません。

結局 CertPathBuilder が出す例外は全く役に立たない



というわけで、Sun PKIX CertPathBuilderの吐く例外やログはというのは、どのような理由でパス構築・検証の失敗したのかわからないため使い物にならず、、ただ「unable to find valid certification path to requested target」の例外メッセージが空しくも得られるだけということがおわかり頂けたかと思います。

ではパス構築・検証がダメ理由を知りたきゃどうするか?



SSLサーバー認証では多くの場合SSLサーバー証明書・必要な中間CA証明書・ルート証明書がチェーンの形でごっそり送られてきますし、S/MIME署名メールやCAdES/XAdES長期署名の場合なんかも検証すべき証明書チェーンが概ねわかっている時があります。

自分で証明書チェーンを
・主体者・発行者の名前の一致で
・署名値の一致で
作ることもできることがあります。

そんなときは、自分で簡易パス構築で証明書チェーン作ってCertPathオブジェクトを生成し、CertPathValidatorで(狭義の)パス検証だけを行うのがいいように思います。

例えば、今回のケースで言えば

自前でパス構築だけして、パス検証はCertPathValidatorを使えば、

HHH→GGG→DDD→AAA→ROOTのパスの期限切れエラーなら
×失敗: timestamp check failed
×失敗理由詳細: NotAfter: Sun Jan 02 09:00:00 JST 2000


HHH→GGG→EEE→BBB→ROOTのパスの基本制約のcA=TRUE不足エラーなら
×失敗: basic constraints check failed: this is not a CA certificate


と、例外CertPathValidatorExceptionのgetMessage()で理由がわかってスッキリします。

今回はこれまたマニアックなネタでごめんなさいね。

自分でもパス構築してみたくなったら今回の証明書セットはこちらからダウンロードできます。

<参考リンク>
Sun J2SE 6 - Java PKI APIプログラマーズガイド
Sun J2SE 6 - Java暗号化アーキテクチャー(JCA) リファレンスガイド
Sun J2SE 6 - java.security.certパッケージ
IPA: 電子政府情報セキュリティ相互運用支援技術の開発
 ・GPKIアプリケーション実装ガイド報告書 (PDF 831KB)
   5章:Javaによる証明書パス構築・パス検証の実装の説明