PKCS#12 と PEM のどちらを使うかは、鍵と証明書を Unix サーバーと独自のキーストアを持つプラットフォームとの間で移すたびに問題になります。PEM ファイルは人が読める別々のブロックで、OpenSSL ベースのソフトウェアがそのまま読み込みます。PKCS#12 ファイル(.p12 または .pfx)は暗号化された 1 つのコンテナーで、OS や Java が一度の操作でインポートします。鍵と証明書は同じで、違うのは包み方です。そして、インポートが成功するかどうかは、包み方に使われたアルゴリズムで決まります。
比較表
| PEM | PKCS#12(PFX) | |
|---|---|---|
| 仕様 | RFC 7468(DER のテキストエンコーディング) | RFC 7292(PKCS #12 v1.1)、RFC 9579(PBMAC1) |
| 形式 | テキスト。1 つ以上の BEGIN … END ブロック | 「セーフバッグ」を収めた DER のバイナリコンテナー |
| 格納できるもの | ファイルに入れたものすべて:鍵、証明書、CRL | 秘密鍵、証明書、CRL、シークレット(フレンドリ名と鍵 ID 付き) |
| 保護 | 鍵ごとに任意(暗号化された PKCS#8) | バッグのパスワードベース暗号化と、完全性を保護する MAC |
| OpenSSL 3.6 のデフォルトの保護 | 暗号化した鍵には AES-256-CBC による PBES2 | PBES2、PBKDF2-HMAC-SHA256、AES-256-CBC、反復 2,048 回、HMAC-SHA256 の MAC |
OpenSSL の -legacy | 該当なし | 証明書は RC2-40、鍵は 3DES、MAC は HMAC-SHA1 |
| 一般的な拡張子 | .pem、.crt、.key | .p12、.pfx |
| 利用側 | OpenSSL、Web サーバー、プロキシ、Go、Python、Node.js | Windows の証明書ストア、macOS のキーチェーン、Java の KeyStore、ブラウザー、signtool |
.pfx の中身
PKCS#12 ファイルは PFX 構造です。中身は複数のコンテンツブロックからなる認証付きセーフで、各ブロックにバッグが入っています。秘密鍵は pkcs8ShroudedKeyBag エントリ(暗号化された PKCS#8)に、証明書は certBag エントリ(こちらも暗号化されたブロックに入っていることが多い)に格納され、バッグには鍵とその証明書を結び付ける friendlyName や localKeyId などの属性が付きます。ファイル全体の完全性は、パスワードから導出した MAC で保護されます。RFC 9579(2024年)は PBMAC1 を追加しました。PBMAC1 は、PKCS#12 の旧来の KDF の代わりに PBKDF2 で MAC 鍵を導出します。
PEM にはコンテナーがありません。サーバーには鍵ファイルと証明書(チェーン)ファイル、または複数のブロックを含む 1 つのファイルを設定します。保護は鍵ファイル自体が持つものがすべてで、サーバーでは通常は保護されていません。人がパスワードを入力しなくても、プロセスが鍵を読み込める必要があるからです。
変換
次のスクリプトは OpenSSL 3.6.5 で実行したもので、使い捨ての鍵と証明書を PKCS#12 ファイルにまとめ、再び取り出します。
#!/bin/sh
dir=$(mktemp -d)
cd "$dir" || exit 1
openssl req -new -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout key.pem \
-subj "/CN=example.com" -days 30 -out cert.pem 2>/dev/null
openssl pkcs12 -export -inkey key.pem -in cert.pem -name demo -passout pass:demo -out bundle.p12
openssl pkcs12 -in bundle.p12 -passin pass:demo -info -noout 2>&1 | grep -E "MAC:|Shrouded|PKCS7 Encrypted"
openssl pkcs12 -in bundle.p12 -passin pass:demo -nodes -out extracted.pem 2>/dev/null
grep -E "^-----BEGIN" extracted.pem
cd / && rm -rf "$dir"
MAC: sha256, Iteration 2048
PKCS7 Encrypted data: PBES2, PBKDF2, AES-256-CBC, Iteration 2048, PRF hmacWithSHA256
Shrouded Keybag: PBES2, PBKDF2, AES-256-CBC, Iteration 2048, PRF hmacWithSHA256
-----BEGIN CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
中間証明書を含めるには -certfile chain.pem で渡します。証明書だけを取り出すには -nokeys、鍵だけなら -nocerts を使います。-nodes フラグ(新しいリリースでは -noenc という名前)は鍵を暗号化せずに書き出すため、出力の扱いには注意してください。
互換性:最新のアルゴリズムと旧来のアルゴリズム
OpenSSL 3.0 では、エクスポート時のデフォルトが、RC2-40 と 3DES に HMAC-SHA1 の MAC という組み合わせから、AES-256 と HMAC-SHA256 の MAC に変わりました。多くの利用側が追随したのはその後です。
- macOS:Apple の Security フレームワークが最新形式の PKCS#12 ファイルをインポートできるのは macOS 15 からです。macOS 14 以前では、パスワードが正しくても「MAC verification failed during PKCS12 import (wrong password?)」というエラーになります。
- Java:JDK 9 で PKCS#12 がデフォルトのキーストアタイプになりました。JDK 16(および 2021年に公開された 8 と 11 のアップデート)では、Java 自身のデフォルトが AES-256 による PBES2 と HMAC-SHA256 の MAC に切り替わり、現行のバージョンは OpenSSL 3 のファイルを読み込めます。
- Windows:現行のリリースは AES-256 の PFX ファイルをインポートできます。非常に古いバージョンの Windows Server では、旧形式が必要になる場合があります。
- OpenSSL 3 で古いファイルを読む場合:証明書が RC2-40 で暗号化されたファイル(OpenSSL 1.1 のデフォルトで、RC2 が組み込まれているときに
-legacyでエクスポートした場合もこれになります)は、レガシープロバイダーを読み込む-legacyを付けないと読み込めません。付けない場合、OpenSSL 3.6.5 はRC2-40-CBCについてerror:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupportedを出力し、Node.js 26 はUnsupported PKCS12 PFX dataでファイルを拒否します。3DES と HMAC-SHA1 の MAC だけを使うファイルは、レガシープロバイダーなしで読み込めます。
古いことが分かっているインポート先で最新形式のファイルが読み込めない場合は、openssl pkcs12 -export -legacy … でエクスポートするか、アルゴリズムを明示的に指定し(-keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES -macalg SHA1)、そのファイルは転送中の保護が弱いものとして扱ってください。
ここではエラーメッセージが誤解を招きます。「MAC verification failed」や「wrong password」は、たいていパスワードが間違っていることではなく、読み込む側が MAC や暗号化のアルゴリズムに対応していないことを意味します。パスワードを当て推量で試す前に、openssl pkcs12 -info -noout(古いファイルには -legacy を追加)でファイルを調べ、どのアルゴリズムが使われているかを確認してください。
キーストアとしての PKCS#12
Java は PKCS#12 ファイルをキーストアとして扱います。keytool -list -keystore bundle.p12 -storetype PKCS12 でエントリが表示され、friendlyName がエイリアスになります。キーストアは複数の鍵エントリと信頼済みの証明書を保持できますが、PEM ファイルでは命名規則に頼らない限りこれを表現できません。トラストストアは特殊なケースです。証明書だけを含む PKCS#12 のトラストストアでは、証明書を信頼済みとしてマークするために Java 固有の属性が必要なので、確実に作成するには keytool -importcert を使います。ほかの大半のプラットフォームはこうした Java の属性を無視し、localKeyId を照合して鍵と証明書をインポートします。
どちらを選ぶべきか
- Linux サーバー、リバースプロキシ、Kubernetes の Ingress での TLS:PEM の鍵とフルチェーンの証明書を使います。
- Windows の IIS、Azure App Service、Key Vault への証明書のインポート:PKCS#12 を使います。
- Java と Spring Boot の TLS:PKCS#12 のキーストアを使います(最近の Spring Boot は PEM もそのまま受け付けます)。
- ブラウザーとモバイルデバイス向けのクライアント証明書:強力なパスワードを付けた PKCS#12 を使い、ファイルとは別の経路でパスワードを届けます。
- signtool や jarsigner によるコード署名:鍵をファイルで持つのであれば PKCS#12 を使います。ただし、2023年以降、公的に信頼されるコード署名鍵はハードウェアに保管しなければなりません。
- 調査とデバッグ:PEM を使うか、PFX インスペクターを使います。このツールは鍵を表示することなく、バッグとアルゴリズムをローカルで一覧表示します。
移行時の注意点
- チェーン全体を含める:サーバーやクライアント向けに PKCS#12 を作成するときは、リーフ証明書を先頭にした順序でチェーン全体を含めます。中間証明書の欠落は、インポート後に起こる失敗で最も多い原因です。
- 鍵と証明書を照合する:まとめる前に確認します。鍵・証明書照合ツールは両者の公開鍵を比較します。
- 最も古い利用側に合わせてアルゴリズムを選ぶ:その後で引き上げます。古いプラットフォームがなくなったら、最新のデフォルトでエクスポートし直してください。
- 強力なランダムパスワードを使う:受け渡しする PKCS#12 ファイルはオフラインで攻撃される可能性があるためです。OpenSSL のデフォルトである PBKDF2 の反復 2,048 回は、パスワード保存の基準からすると少ない回数です。
- 取り出した鍵を放置しない:サーバー用に
.pfxを PEM に変換したら、ファイルのパーミッションを制限し、一時的なコピーを削除してください。