本文へスキップ
ecdsa.com

JWK と PEM の比較|JSON の鍵か ASN.1 の鍵か

JWK は鍵を JSON で表したもの、PEM は鍵を DER にして base64 で包んだものです。中身の数値は同じで、いくつかの細部に注意すれば、相互に変換しても情報は失われません。

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 種類の識別子をどちらも計算します。

Node.js:PEM → JWK → PEM の変換と、サムプリントと SPKI ハッシュの比較
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)に決め、ドキュメントに明記してください。

移行時の注意点

  1. 鍵は、保管先となるキーストアの中で生成します。そのうえで、各利用側が必要とする形式で公開鍵の部分をエクスポートします。秘密鍵の形式変換は、必要以上に行わないでください。
  2. 変換にはライブラリを使います:node:crypto(createPublicKey と export)、Python の cryptography と jwcrypto または joserfc、JavaScript の jose、Java の Nimbus、あるいはローカルで動作する JWK 変換ツールです。
  3. JWKS 用の JWK を作成するときは、kid、alg、use を明示的に設定します。安定した識別子が必要な場合は、kid を JWK サムプリントから導出してください。
  4. 変換後に確認します:公開する前に、秘密鍵で署名し、変換した公開鍵で検証してください。
  5. 両方の表現を同期させます。鍵のローテーション中にこれを怠ると、JWKS と、PEM で設定したゲートウェイとで、現在の鍵についての認識が食い違うおそれがあります。

参考資料

すべての比較