PEMから動くスタックへ — 第2部:Javaが読もうとしなかったキーストア

OpenSSL · keytool · Docker · PKCS#12

PEMから動くスタックへ — 第2部:Javaが読もうとしなかったキーストア

シリーズ第2部。第1部で証明書フォーマットを整理した。今度はそれらからPKCS#12トラストストアを構築する——技術的に正しいファイルでも引っかかる二つのハードルに直面しながら。

Your keystore contains 0 entries

Keycloak(Javaベースの認証サーバー)はPKCS#12トラストストアを要求していた:サービスが信頼すべき公開CA証明書を保持するファイルだ。トラストストアには意図的に秘密鍵を含まない——CAの公開証明書だけだ。それによってサービスに「このCAが署名したものは信頼してよい」と伝える。

きれいなCA証明書は手元にあった。当然のコマンド:

openssl pkcs12 -export -nokeys -in ca.crt -out root.p12 -passout pass:<TRUSTSTORE_PW>

トラストストアには鍵が不要なので-nokeys。うまくいくはずだった。そして——表面上は——うまくいった。


CASE #1空のキーストア

ファイルは作成され、正しいパスワードを持ち、OpenSSLで開くことができた。しかしJavaはトラストストアが空だと報告した。Javaが提供するツールkeytoolによる証明:

keytool -list -keystore root.p12 -storetype PKCS12 -storepass <TRUSTSTORE_PW>
# -> Your keystore contains 0 entries

証拠: ゼロエントリー——OpenSSLはファイル作成時に文句を言わず、検査時にはCertificate bagを律儀に表示したにもかかわらず。

原因は微妙だ:openssl pkcs12 -export -nokeysで秘密鍵なしに素の証明書をパッケージすると、コンテナ内にエイリアス(「friendlyName」とも呼ばれる)なしのいわゆるbagとして格納される。そしてJava/keytoolはエイリアスのないエントリーを使用可能なエントリーとして単純にリストしない。Javaから見れば:空だ。

修正方法はJava自身のツールでトラストストアを構築することだ。keytool -importcertはまさにこのため——信頼された証明書をエイリアス付きでインポートするために作られている:

keytool -importcert -noprompt \
  -alias corp-ca \
  -file ca.crt \
  -keystore root.p12 \
  -storetype PKCS12 \
  -storepass <TRUSTSTORE_PW>

これでkeytool -listtrustedCertEntryとともに1 entryを律儀に表示する。Javaが証明書を認識する。

結論: Javaのトラストストアにはkeytoolが正しい選択であり、OpenSSLのエクスポートではない。OpenSSLが正しいとみなすものが、Javaの世界で使えるとは限らない。

CASE #2正しいパスワードなのに「パスワードが違う」

空のキーストアを解決したとたん、次の古典的エラーが来た:

ERROR: keystore password was incorrect
javax.crypto.BadPaddingException: Given final block not properly padded

厄介なのは:まったく同じファイルをまったく同じパスワードでOpenSSLでは問題なく開けた:

openssl pkcs12 -info -in root.p12 -passin pass:<TRUSTSTORE_PW> -noout
# runs through cleanly

証拠: OpenSSLがパスワードでファイルを開けるのにJavaが「パスワードが違う」と言う場合、問題はパスワードではない——暗号化アルゴリズムだ

背景:OpenSSL 3.xはPKCS#12ファイルをデフォルトでモダンなPBES2 / AES-256 / SHA-256でパッケージする。多くのコンテナイメージのJDK 17のような古いJavaバージョンはこのアルゴリズムを復号化時に理解できない——そして誤解を招くことに「パスワードが違う」と報告する、パスワードは完全に正しいのに。

モダンフォーマットはOpenSSLの出力から判別できる:

openssl pkcs12 -info -in root.p12 -passin pass:<TRUSTSTORE_PW> -noout
# PKCS7 Encrypted data: PBES2, PBKDF2, AES-256-CBC ...   <- Java doesn't like this
# MAC: sha256

Javaはその代わり旧来のレガシーフォーマットを求める:

PKCS7 Encrypted data: pbeWithSHA1And3-KeyTripleDES-CBC ...   <- Java understands this
MAC: sha1

結論: OpenSSLがファイルを問題なく開けるのにJavaから「パスワードが違う」が来たら、それはほぼ常にPKCS#12暗号化アルゴリズムの問題であって、パスワードの問題ではない。

ジレンマ——そしてその解決

プロジェクト全体を貫くトラップにはまった:

  • OpenSSLでレガシーフォーマットにエクスポート→ Javaで読めるが、空のキーストア(エイリアスなし)。
  • keytoolでインポート→ エイリアス付きの中身のあるキーストアだが、Javaが拒否するモダンなAESフォーマット。

両方同時に必要だった:エイリアス付きのエントリーかつレガシーフォーマット。

解決策は構築にkeytoolを使い(エイリアス問題を解決)、同時にJDKのシステムプロパティでレガシーフォーマットを強制することだった:

keytool -importcert -noprompt -alias corp-ca \
  -file ca.crt \
  -keystore root.p12 -storetype PKCS12 -storepass <TRUSTSTORE_PW> \
  -J-Dkeystore.pkcs12.certProtectionAlgorithm=PBEWithSHA1AndDESede \
  -J-Dkeystore.pkcs12.certPbeIterationCount=2048 \
  -J-Dkeystore.pkcs12.macAlgorithm=HmacPBESHA1 \
  -J-Dkeystore.pkcs12.macIterationCount=2048

四つの-J-Dオプションは「旧来の3DES/SHA1フォーマットを使え」という指示をkeytoolを実行するJVMに渡す。結果:エイリアス付きのエントリーがJavaで読めるフォーマットで。一度で両条件を満たした。

コンテナ内でファイルを構築しなければならない場合

脇道にあった実践的な落とし穴:どのkeytoolを使うかは無関係ではない。最も確実な方法は、後でそれを読むJavaとまったく同じJavaでキーストアを構築すること——つまりコンテナ内で。そうすれば生成されたフォーマットとの互換性が保証される。

コンテナ内の証明書ディレクトリはしばしば読み取り専用(read-only)でマウントされているため、/tmpでファイルを構築してストリームとして出力する:

docker compose run --rm -v /host/certs:/out:ro --entrypoint sh keycloak -c '
  keytool -importcert -noprompt -alias corp-ca \
    -file /out/ca.crt \
    -keystore /tmp/root.p12 -storetype PKCS12 -storepass <TRUSTSTORE_PW> \
    -J-Dkeystore.pkcs12.certProtectionAlgorithm=PBEWithSHA1AndDESede \
    -J-Dkeystore.pkcs12.macAlgorithm=HmacPBESHA1 && cat /tmp/root.p12
' > /host/certs/root.p12

トリック:keytoolは(常に書き込み可能な)/tmpに書き込み、catで結果を標準出力にダンプし、ホスト側のシェルが>でターゲットファイルにリダイレクトする——コンテナではなくあなたが書き込み権限を持つ場所に。

第2部で学んだこと

  1. 作成成功なのに「0 entries」= 証明書にエイリアスがない。-alias付きのkeytool -importcertを使え。
  2. パスワードが正しいのに「パスワードが違う」= 実際はアルゴリズムの問題。OpenSSL 3はAES-256で書くが、古いJavaは読めない。
  3. JavaはレガシーフォーマットpbeWithSHA1And3-KeyTripleDES-CBC/MAC: sha1を必要とする。
  4. keytool-J-Dオプションで中身ありかつレガシーを一度で実現。
  5. 常にkeytoolで検証せよ、OpenSSLだけでなく——Javaの視点を見せてくれるのはそちらだけだ。
  6. キーストアはターゲットコンテナ内で構築せよ、そうすればフォーマットが確実に合う。

次のパートでは、組織的に厄介になる:複数のサービスが同じファイルに対して異なるパスワードを期待する場合どうなるか——そして三種類の異なるパスワード変数を区別し続ける方法。

コメント (0)

まだコメントがありません。最初のコメントを書いてみましょう!

コメントを書く