PEMから動くスタックへ — 第1部:証明書がどうしても合わなかった理由
「証明書をさっとはめ替えるだけ」のはずが丸一日の探求に変わった四部作——X.509、Javaキーストア、Dockerについて学んだこと。
root.pem という自己署名ルート証明書があった。新たにセットアップしたDockerアプリ(KeycloakをAuthサーバーとするERPスタック)はroot.p12を要求してきた。最初の考えは「ただのファイル形式の違いだ——一度変換すれば終わり」だった。
結果はブログ記事4本分になった。この道のりが参考になるのは、一つのトリッキーなコマンドのせいではない。「証明書」は見た目は同じでも動作がまるで異なる六種類ものものの総称だという気づきにある。それを体得してしまえば、こうした問題を何時間もかけずに数分でデバッグできるようになる。
まず:何が何なのか?
何かを変換する前に、扱うことになるフォーマットを把握しておく価値がある。
- PEM(
.pem、.crt、.cer):テキスト形式。-----BEGIN CERTIFICATE-----という行で判別できる。証明書一枚、複数枚、秘密鍵、またはそれらすべてを一緒に保持できる。 - DER:PEMと同一だが、テキストではなくバイナリ。エディタで開くとデータの羅列に見える。
- PKCS#12(
.p12、.pfx):証明書と秘密鍵をセットでまとめたパスワード保護付きのバイナリコンテナ。Windowsでは.pfx、Linux界隈では.p12と呼ぶ。まったく同じフォーマットだ。
最初の気づき:Windowsからの.pfxはそのままroot.p12にリネームできる。変換不要。それだけで序盤の多くの手間が省けたはずだった。
肝心の問い:中身は何か?
何かを変換する前に、ソースファイルが何を含んでいるかを知るべきだ。トラストストア(他者を信頼するためのもの)には公開証明書だけが必要。サーバーのキーストア(自己を証明するためのもの)にはさらに秘密鍵も必要になる。
PEMファイルの最速確認:
grep -E "BEGIN (CERTIFICATE|PRIVATE KEY|RSA PRIVATE KEY)" meine-datei.pem
CERTIFICATEだけが表示されるなら、鍵は入っていない。PRIVATE KEYも出てくれば、両方が含まれている。
そこで変換を済ませてKeycloakを起動した。受け取ったのはこれだ:
ERROR: Failed to initialize truststore: /certificates/root.p12, type: PKCS12 ERROR: toDerInputStream rejects tag type 45
tag type 45は暗号めいた響きだが、読み方さえわかれば素晴らしいヒントだ:45は文字-のASCIIコードだ。Javaはバイナリ形式のPKCS#12ファイルを読もうとしたのに、テキスト——つまり-----BEGIN CERTIFICATE-----の冒頭——を見つけてしまった。
言い換えれば、私のroot.p12は.p12という名前をつけられているだけのテキストファイル(PEM)だった。確認手順:
証拠: 45は-のASCIIコード——-----BEGIN CERTIFICATE-----のダッシュだ。
file root.p12 # says "ASCII text" instead of "data"? -> not real PKCS#12 head -c 60 root.p12 # shows -----BEGIN...? -> it's PEM
結論: ファイル拡張子は中身について何も語らない。fileとheadがもっとも頼りになる。
さらに奇妙なことが起きた。別の証明書ファイルはこんな見た目だった:
-----BEGIN CERTIFICATE----- LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlH...
一見正常に見える——BEGIN CERTIFICATE、続いてBase64。しかしopenssl x509は拒否した:
Could not find certificate ... wrong tag ... nested asn1 error
カラクリは2行目にある。LS0tLS1CRUdJTiBD...はそれ自体がまたBase64だ——つまり-----BEGIN CERTIFICATE-----のBase64エンコードだ。このファイルは二重エンコードされていた:誰かが完成したPEMをさらにBase64でエンコードし、その上に新たなBEGIN/END行を手書きで追加したのだ。
これは思う以上に起きやすい。たとえば、すでにBase64になっているものに誤ってcertutil -encodeを適用した場合などだ。
修復手順——ヘッダーとフッターを除去し、Windowsの改行を取り除き、一度デコードする:
grep -v "CERTIFICATE-----" kaputt.crt | tr -d '\r' | base64 -d > sauber.crt openssl x509 -in sauber.crt -noout -subject -issuer
結論: 最後のコマンドがsubject=とissuer=をきれいに出力すれば、ファイルは救済されている。2行目のLS0tLS1パターンが二重Base64のサインだ。
ストーリー全体を通じて繰り返し現れたトラブルメーカーがCRLF改行コード(\r\n)だ。ファイルをWindows上で編集またはコピーした瞬間に紛れ込む。これを可視化するには:
cat -A datei.pem | head -5
証拠: 行末に^M$が現れたら、CRLFが入り込んでいる。
除去するには:
sed -i 's/\r$//' datei.pem
結論: この目に見えない\rバイトには何度も何時間も消耗させられた。コマンドやパスワードをじわじわと狂わせるのに、決して見えないのだから。
第1部で学んだこと
- ファイル拡張子 ≠ フォーマット。
fileとheadで実際の中身を確認せよ。 .pfxと.p12は同一だ——リネームだけで済むことが多い。- 「tag type」や「asn1」に関するJavaのエラーはほぼ常にフォーマット問題を意味する、内容の問題ではない。
tag type 45= バイナリが期待されたところにテキストがあった(BEGINの-)。- 二重Base64エンコードは2行目の
LS0tLS1パターンで判別できる。 - CRLF改行コードは見えないが致命的——
cat -Aがそれを暴く。
次のパートでは、これらのきれいな素材からJavaが実際に受け入れるキーストアを構築する方法を見ていく——そして「OpenSSLがファイルを開ける」ことが「Javaも開ける」からは程遠い理由を探る。