JWK と PEM のどちらを使うかは、鍵をどこで使うかで決まります。JSON Web Key(RFC 7517)は JOSE、OAuth、WebCrypto が扱う形式で、PEM ファイル(SubjectPublicKeyInfo や PKCS #8 などの DER 構造を RFC 7468 の方式でテキストにエンコードしたもの)は、OpenSSL、TLS サーバー、SSH のツール、Java、大半の PKI ソフトウェアが前提とする形式です。暗号学的な中身は同じなので、選択を左右するのはツールとメタデータです。
比較表
| JWK(RFC 7517) | PEM(RFC 7468) | |
|---|---|---|
| エンコーディング | base64url の値をメンバーに持つ JSON オブジェクト | -----BEGIN …----- と -----END …----- で囲んだ DER の base64 |
| 公開鍵の構造 | kty と鍵タイプ固有のメンバー(crv、x、y、n、e) | SubjectPublicKeyInfo(BEGIN PUBLIC KEY) |
| 秘密鍵 | 同じオブジェクトに d(と RSA の CRT メンバー)を追加 | PKCS #8(BEGIN PRIVATE KEY)、または旧来の PKCS #1 形式と SEC 1 形式 |
| メタデータ | kid、alg、use、key_ops、x5c、x5t#S256 | なし(ラベルのみ) |
| 複数の鍵 | JWK セット:"keys" 配列 | 1 つのファイルに複数の PEM ブロック |
| P-256 公開鍵のサイズ(実測) | JSON で 126 文字 | 178 文字 |
| 識別用のハッシュ | JWK サムプリント(RFC 7638) | SPKI の SHA-256 フィンガープリント、証明書のフィンガープリント |
| 利用側 | JOSE ライブラリ、WebCrypto の jwk、OIDC の jwks_uri、COSE との変換ツール | OpenSSL、nginx、Apache、Java の X509EncodedKeySpec、Go の x509、Python の cryptography |
それぞれの中身
JWK は、鍵の数値をそのまま書き表します。EC の公開鍵は kty: EC、crv: P-256 と、固定長の base64url 文字列で表した座標 x と y で構成されます。RSA の鍵は kty: RSA と n、e、Ed25519 と X25519 の鍵は kty: OKP と crv、x で構成され(RFC 8037)、秘密鍵ではこれに d が加わります。オプションのメンバーは、鍵を特定の用途に結び付けます。alg(1 つのアルゴリズム)、use(sig または enc)、kid、そして証明書への参照です。
PEM の公開鍵は、SubjectPublicKeyInfo を DER でエンコードしたものです。中身は、アルゴリズム識別子(たとえば P-256 の OID を伴う id-ecPublicKey)と、曲線上の点を格納したビット列です。秘密鍵は PKCS #8 の PrivateKeyInfo 構造で、暗号化することもできます。PEM はラベル以外のメタデータを持たないため、その鍵をどのアルゴリズムで使ってよいかは、別の場所で設定しなければなりません。
相互の変換
変換で情報が失われることはありません。次のスクリプトは Node.js 26.10 で実行したもので、P-256 の公開鍵を PEM から JWK に変換して PEM に戻し、2 種類の識別子をどちらも計算します。
import { createHash, createPublicKey, generateKeyPairSync } from "node:crypto";
const { publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
// PEM (SubjectPublicKeyInfo) -> JWK -> PEM again.
const pem = publicKey.export({ type: "spki", format: "pem" });
const jwk = publicKey.export({ format: "jwk" });
const roundTrip = createPublicKey({ key: jwk, format: "jwk" }).export({ type: "spki", format: "pem" });
// JWK thumbprint (RFC 7638): SHA-256 over the required members in lexicographic order.
const canonical = JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y });
const thumbprint = createHash("sha256").update(canonical).digest("base64url");
// SPKI fingerprint: SHA-256 over the DER-encoded SubjectPublicKeyInfo.
const spki = createHash("sha256").update(publicKey.export({ type: "spki", format: "der" })).digest("base64url");
console.log("JWK members:", Object.keys(jwk).join(", "));
console.log("PEM length:", pem.length, "JWK JSON length:", JSON.stringify(jwk).length);
console.log("PEM -> JWK -> PEM identical:", pem === roundTrip);
console.log("JWK thumbprint equals SPKI hash:", thumbprint === spki);
JWK members: kty, x, y, crv
PEM length: 178 JWK JSON length: 126
PEM -> JWK -> PEM identical: true
JWK thumbprint equals SPKI hash: false
よく驚かれるのが最後の行です。JWK サムプリントは正規化した JSON をハッシュし、SPKI フィンガープリントは DER をハッシュするため、同じ鍵に 2 つの異なる「フィンガープリント」が存在します。どちらを指すのかは、システム間で合意しておく必要があります。たとえば DPoP の cnf.jkt は JWK サムプリントですが、証明書のピンニングでは SPKI のハッシュを使います。
落とし穴
- パディングと長さ:JWK の値はパディングなしの base64url で、EC の座標は先頭のゼロバイトも含めて全長(P-256 では 32 バイト、P-521 では 66 バイト)を保たなければなりません。ゼロを取り除いてしまう自作の変換ツールは、P-256 と P-384 では約 128 回に 1 回の割合で失敗する鍵を作ります。
xとyがそれぞれ約 256 回に 1 回の割合でゼロバイトから始まるためです。P-521 では先頭バイトに値が 1 ビットしか入らないため、約 4 個に 3 個の鍵が失敗します。Node は無効な JWK としてエラーを報告し、WebCrypto はDataErrorをスローします。 - 公開の場所に置かれた秘密鍵:
dを含む JWK は秘密鍵です。JWKS で公開したりツールに貼り付けたりすると、鍵が漏洩します。このサイトのツールは、公開鍵だけが必要な場面ではdを含む JWK を受け付けません。 - PEM ラベルの誤り:
BEGIN RSA PUBLIC KEY(PKCS #1)とBEGIN PUBLIC KEY(SPKI)は別の構造です。JWK 変換ツールの多くは SPKI しか受け付けません。 - 失われるメタデータ:JWK を PEM に変換すると
kidとalgが失われます。検証者が引き続きアルゴリズムを固定できるよう、これらは設定に残しておいてください。 - Ed25519 の秘密鍵:JWK の
dは 32 バイトのシードですが、ライブラリによっては生の秘密鍵が 64 バイト(シードと公開鍵)になります。 - 証明書は鍵とは別物:JWK は
x5cで証明書チェーンを参照できますが、その各要素は base64url ではなく標準の base64 でエンコードした DER 証明書です。検証者は、中の鍵を信頼する前にチェーンを検証しなければなりません。同様に、PEM のBEGIN CERTIFICATEブロックにも公開鍵が含まれていますが、それを取り出すこと(openssl x509 -pubkey -noout)と信頼することは別のステップです。
どちらを選ぶべきか
- 発行者が公開するトークンの検証鍵:
jwks_uriで JWK セットとして公開します。 - JOSE ライブラリやブラウザーの WebCrypto に渡す鍵:JWK を使います(WebCrypto は SPKI と PKCS #8 の DER も受け付けます)。
- TLS サーバーの鍵と証明書、CSR、OpenSSL のワークフロー:PEM を使います。
- 標準の鍵クラスを使う Java、.NET、Go のコード:PEM または DER(SPKI と PKCS #8)を使います。JWK は JOSE ライブラリを通じて扱えます。
- 設定ファイルとシークレットマネージャー:どちらでもかまいません。PEM はツールの対応が広く、JWK は鍵と一緒にメタデータを保持できます。
- ピンニングと鍵識別子:ハッシュの定義を 1 つ(JWK サムプリントか SPKI の SHA-256)に決め、ドキュメントに明記してください。
移行時の注意点
- 鍵は、保管先となるキーストアの中で生成します。そのうえで、各利用側が必要とする形式で公開鍵の部分をエクスポートします。秘密鍵の形式変換は、必要以上に行わないでください。
- 変換にはライブラリを使います:
node:crypto(createPublicKeyとexport)、Python のcryptographyとjwcryptoまたはjoserfc、JavaScript のjose、Java の Nimbus、あるいはローカルで動作する JWK 変換ツールです。 - JWKS 用の JWK を作成するときは、
kid、alg、useを明示的に設定します。安定した識別子が必要な場合は、kidを JWK サムプリントから導出してください。 - 変換後に確認します:公開する前に、秘密鍵で署名し、変換した公開鍵で検証してください。
- 両方の表現を同期させます。鍵のローテーション中にこれを怠ると、JWKS と、PEM で設定したゲートウェイとで、現在の鍵についての認識が食い違うおそれがあります。