选 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 个字符的 JSON | 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(单一算法)、use(sig 或 enc)、kid,以及对证书的引用。
PEM 公钥是 SubjectPublicKeyInfo 的 DER 编码:一个算法标识符(例如 id-ecPublicKey 加上 P-256 的 OID),以及一个存放公钥点的位串。私钥是 PKCS #8 PrivateKeyInfo 结构,可以选择加密。PEM 除了标签之外不携带任何元数据,因此密钥可以配合哪种算法使用,必须在别处配置。
相互转换
转换不会丢失任何信息。下面的脚本用 Node.js 26.10 运行,把一个 P-256 公钥从 PEM 转换为 JWK 再转换回来,并计算两种标识符。
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,而且验证方必须先验证证书链,才能信任其中的密钥;同样,PEMBEGIN 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),并写进文档。
迁移说明
- 在最终保存密钥的存储中生成密钥,再按每个使用方需要的格式导出公钥部分;除非确有必要,不要在不同格式之间转换私钥。
- 用库来转换:
node:crypto(createPublicKey和export)、Python 的cryptography配合jwcrypto或joserfc、JavaScript 的jose、Java 的 Nimbus,或者在本地运行的 JWK 转换器。 - 为 JWKS 创建 JWK 时,明确设置
kid、alg和use;如果需要稳定的标识符,可以从 JWK 指纹派生kid。 - 转换后校验:发布前先用私钥签名,再用转换后的公钥验证签名。
- 密钥轮换期间,保持两种表示形式同步,否则 JWKS 和用 PEM 配置的网关可能对当前密钥的认定不一致。