証明書の議論には二つの主張が流布している。一つは .p12 が強力な選択肢で .pem は緩いものだという主張、もう一つは読めるのだからテキストファイルは安全に送れるはずだという主張だ。どちらも一分の検証に耐えない。

PKCS12、PEM、DER は証明書オブジェクトをどう梱包し符号化するかを説明するものである。相手が誰か、発行者が信頼できるか、ファイルにパスワードが必要かは決めない。

クライアントが何を受け入れるかは互換性の問題だ。ファイルが何を含むかは身元の問題だ。この二つを分けておけば、ほとんどのインポートエラーは謎ではなくなる。

三つのコンテナ、一つのオブジェクトモデル

実際に手元にあるものから始めよう。証明書は公開鍵に関する署名済みの表明だ。秘密鍵は、その鍵を自分が制御していることを証明する別の素材だ。チェーンは、証明書をクライアントが信頼するルートにつなぐ発行者の集合だ。三つとも梱包方法とは無関係に存在する。

PKCS12 ファイルは通常 .p12 または .pfx で、RFC 7292 で定義されたコンテナであり、秘密鍵、一つ以上の証明書、チェーンを一つのパスワード保護されたブロックにまとめて運べる。身元を名付けるエイリアスも持つ。PEM ファイルは RFC 7468 で定義されたテキストの枠組みで、BEGIN 行と END 行の間に base64 の本体が入り、一つのファイルに複数のブロックを並べられる。DER は同じ ASN.1 構造のバイナリ符号化であり、PEM の本体は文字通り base64 で包んだ DER だ。

最後の一文は覚えておく価値がある。PEM を DER に変換しても変わるのは枠組みであり中身ではなく、戻せば元のバイト列になる。

ContainerCan holdDoes not decide
PKCS12 (.p12, .pfx)Key, leaf certificate, chain, alias, passwordWhich root the client trusts
PEM (.pem, .crt, .cer)Any number of base64 blocksWhether the private key is present
DER (.der, .cer)One binary structureThe object type, whatever the extension says

符号化は暗号化ではない

base64 は輸送用の文字集合だ。何も隠さず、誰でもシェルで PEM ブロックを復号できる。DER ファイルはバイナリで、同じくらい公開されている。どちらの性質も保護ではなく、パスワードの存在を示唆もしない。

本当の保護は鍵素材にある。PKCS12 バンドルはパスワードから導いた鍵で中身を暗号化できる。PEM の秘密鍵は旧来のヘッダブロックで暗号化するか、PKCS#8 EncryptedPrivateKeyInfo に収められる。証明書単体は何も秘密に保たない。だから証明書だけを出力したエクスポートでは、どの形式で包んでもクライアントは認証できない。

拡張子はヒントであり契約ではない。.cer と .crt で終わるファイルは両方の符号化で現れ、機器は DER 証明書を .pem 名で十分頻繁に書き出すため、末尾で推測すると半日を無駄にする。代わりに先頭のバイトを読もう。PEM ファイルは BEGIN 行で始まり、DER は外側の構造が SEQUENCE なので 0x30 で始まる。

インポートが実際に失敗する理由

クライアントのエラーメッセージは、いくつもの異なる問題を「無効な証明書」という一文に押し込むので、原因ごとに切り分けよう。最も多いのは符号化の不一致で、PEM を解析するクライアントは DER ファイルで解析エラーを返す。修正は新しい証明書ではなく変換だ。二つ目は鍵の欠落で、証明書は正しいが身元を証明できない。三つ目はチェーンで、中間証明書のないバンドルはインポート画面では正しく見え、ハンドシェイクで失敗する。

暗号化パラメータが四つ目の原因で、見た目は完全に間違ったパスワードと同じだ。古い機器は PKCS12 ファイルを RC2-40 と 3DES で書き出し、OpenSSL 3 は legacy プロバイダを読み込まない限り拒否する。機器から AES-256-CBC と PBKDF2 で再出力するのが恒久的な修正だ。一つのファイルのためにクライアント機で古い暗号を恒久的に有効にしておくのは割に合わない。

五つ目は曖昧さだ。鍵二つと証明書二つを持つコンテナや、CERTIFICATE ブロックを三つ持つ PEM ファイルはクライアントに選択を強いるが、その選択は多くの場合たんに最初の項目だ。トンネルが誤った身元で上がるのは、たいていこれが理由だ。

変換する前に調べる

秘密を表示せずに構造を読むには、openssl pkcs12 -in bundle.p12 -info -noout を使う。これは暗号化アルゴリズムとエイリアスを報告し、鍵と証明書の出力を抑える。フラグは openssl-pkcs12 のマニュアル に記載されている。-noout を外して -nodes を足すのは、秘密鍵を平文で書き出すため、保存するつもりのファイルを作るときの話であって、ちょっと見るためではない。

証明書については、openssl x509 -in cert.pem -noout -subject -issuer -dates がインポート画面では答えられないことに答え、-ext subjectAltName を足すと実際に有効な名前が表示される。ファイルが DER なら -inform DER を足す。この一つのフラグで大半の unable-to-load エラーが解決する。

鍵と証明書の照合は機械的な作業で、どんなインポートの前にもやっておく価値がある。

openssl x509 -in cert.pem -noout -pubkey | openssl sha256
openssl pkey -in key.pem -pubout | openssl sha256

ダイジェストが一致すればその組は対になっている。違えば鍵が間違っており、形式の変換ではどうにもならない。

形式が決して変えないもの

信頼の判断はコンテナの上で行われる。クライアントはチェーンをたどって自分のストアのルートまで行き、有効期間を確認し、クライアント認証のための拡張鍵用途を確認し、サーバ側では接続した名前が証明書に現れるかを確認する。これらの手順はどれもファイル拡張子を読まない。

だからエラーが消えるまでファイル名を変えるのは、これほど確実な時間の無駄づかいになる。インポートは成功し、身元は依然として誤りで、失敗はハンドシェイクの最中に、より分かりにくいメッセージで再び現れる。二年前に書き出したバンドルがきれいにインポートできてなお拒否されるのも同じ理由だ。有効期限はファイル名ではなく証明書の中を旅していた。

手戻りを避ける順序

クライアントの文書でどの符号化を受け入れ、クライアント身元にどのコンテナを期待するかを確認し、そのうえで渡されたものを見る。クライアントが一つのインポート可能な身元を求め、手元にばらばらの PEM ファイルがあるなら、自分が管理するパスワードで一度バンドルを作り、元ファイルは残しておく。PEM を求めるなら鍵とチェーンを取り出し、その後はチームがすでに使っている資格情報の扱いの手順で中間ファイルを削除する。

インポート後はダイアログではなく身元を検証する。サブジェクト、発行者、有効期限、そしてクライアントが提示する名前だ。一覧の分かりやすい名前はラベルであって、検証済みの身元ではない。その後にトンネルが失敗するなら、梱包ではなく認証やルーティングを調べていることになり、話はずっと短く済む。

関連記事