跳到主要内容
ecdsa.com

JWK vs PEM:JSON 密钥还是 ASN.1 密钥

JWK 是 JSON 形式的密钥;PEM 是用 base64 包装的 DER 形式的密钥。两者承载的是相同的数值,只要注意几个细节,相互转换就不会丢失任何信息。

选 JWK 还是 PEM,取决于密钥要交给谁。JOSE、OAuth 和 WebCrypto 使用的是 JSON Web Key(RFC 7517);OpenSSL、TLS 服务器、SSH 工具、Java 和大多数 PKI 软件需要的是 PEM 文件(按 RFC 7468 对 SubjectPublicKeyInfo、PKCS #8 等 DER 结构做的文本编码)。两者的密码学内容完全相同,因此选择取决于工具链和元数据。

概览

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" 数组一个文件中的多个 PEM 块
P-256 公钥的大小(实测)126 个字符的 JSON178 个字符
标识用的哈希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(单一算法)、use(sig 或 enc)、kid,以及对证书的引用。

PEM 公钥是 SubjectPublicKeyInfo 的 DER 编码:一个算法标识符(例如 id-ecPublicKey 加上 P-256 的 OID),以及一个存放公钥点的位串。私钥是 PKCS #8 PrivateKeyInfo 结构,可以选择加密。PEM 除了标签之外不携带任何元数据,因此密钥可以配合哪种算法使用,必须在别处配置。

相互转换

转换不会丢失任何信息。下面的脚本用 Node.js 26.10 运行,把一个 P-256 公钥从 PEM 转换为 JWK 再转换回来,并计算两种标识符。

Node.js:PEM 转 JWK 再转回,JWK 指纹与 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 做哈希,因此同一个密钥有两个不同的“指纹”。各个系统必须约定所指的是哪一种,例如 DPoP 的 cnf.jkt 是 JWK 指纹,而证书固定使用的是 SPKI 哈希。

常见陷阱

  • 填充与长度:JWK 中的值是不带填充的 base64url,EC 坐标必须保持完整长度(P-256 为 32 字节,P-521 为 66 字节),包括开头的零字节。手写的转换器如果去掉了这些零,生成的密钥在 P-256 和 P-384 上大约每 128 个就有一个会被拒绝,因为 x 和 y 以零字节开头的概率各约为 1/256;在 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 中引用证书链,但其中的条目是标准 base64 编码的 DER 证书,而不是 base64url,而且验证方必须先验证证书链,才能信任其中的密钥;同样,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 则把元数据和密钥保存在一起。
  • 公钥固定和密钥标识符:选定一种哈希定义(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;如果需要稳定的标识符,可以从 JWK 指纹派生 kid。
  4. 转换后校验:发布前先用私钥签名,再用转换后的公钥验证签名。
  5. 密钥轮换期间,保持两种表示形式同步,否则 JWKS 和用 PEM 配置的网关可能对当前密钥的认定不一致。

参考资料

全部对比