PEM 与 JWK 互转

OpenSSL 的 PEM 和 JOSE 体系里的 JWK 互换。左边贴 PEM 得到 JWK,右边贴 JWK 得到 PEM,RSA、EC、Ed25519 都支持。

  • 不做网络请求。RSA 用 node-forge 解析,EC 和 Ed25519 交给浏览器原生的 WebCrypto。私钥只在内存里停留,页面不写文件也不上传。
  • 导出的 JWK 会剔除 alg、key_ops、ext。这三个字段是浏览器自动补的,其中 alg 常常和实际用途矛盾——通用 RSA 密钥会被标成 PS256,实际它也能用于 RS256 和 OAEP。要判断用法请看 kty 和具体参数,别信 alg。

6 个转换时会遇到的事

JWK 里的数字是 Base64URL,不是 Base64。JWK 的所有二进制字段都用 Base64URL 编码:把 Base64 的 + 和 / 换成 - 和 _,并且去掉末尾的 = 填充。这两处差异都会让普通 Base64 解码器失败——带 / 的字符串会被当成路径分隔符处理,长度不是 4 的倍数时标准解码器直接抛异常。另外 JWK 的整数是「最短大端表示」:前导零必须去掉,所以同一个模数在不同工具里导出的字符串长度可能差一个字符,含义完全相同。

JWK 里的 alg 字段常常是错的。alg 表示「这把密钥打算用于哪个算法」,它是一个提示而不是约束。WebCrypto 导出 JWK 时会自动填上 alg,一把通用 RSA 密钥会被标成 PS256,但同一把密钥用于 RS256 签名或 RSA-OAEP 加密都没有问题。所以看到 alg 和实际用法不一致时不要慌,它只是导出方的偏好。真正重要的是 kty 和具体的参数字段。

同一个私钥有四种 PEM 写法。PKCS#1(BEGIN RSA PRIVATE KEY)是最老的写法,内容直接是 RSAPrivateKey。PKCS#8(BEGIN PRIVATE KEY)在外面套了一层 PrivateKeyInfo,多记录了算法标识,所以一种文件格式能装下所有算法。SEC1(BEGIN EC PRIVATE KEY)是 EC 专用的老写法,和 PKCS#1 是同一个年代的东西。上面三种再配上密码,就又多了 PKCS#8 加密和传统 DEK-Info 加密两个变体。本页能读全部六种,写出去的一律是标准的 SPKI 和 PKCS#8,除非你特意选了 PKCS#1。

证书转 JWK 会丢掉一多半信息。X.509 证书是「公钥 + 主体 + 签发者 + 有效期 + CA 签名」的打包,而 JWK 只能表达密钥本身。所以证书转成 JWK 之后,域名、有效期、签发链全都不见了,这通常也是你要的——比如把证书里的公钥塞进 KMS 或 K8s 的 Secret。反过来,想从 JWK 恢复一张证书是做不到的,证书必须由 CA 重新签发。

Ed25519 的浏览器支持还在路上。EC(P-256 / P-384 / P-521)和 RSA 在 WebCrypto 里支持得很早,随便一个现代浏览器都能处理。Ed25519 和 X25519 是后来才加进规范的:Chrome 137、Safari 17、Firefox 129 之后才默认可用。如果你的浏览器版本偏旧,用这个页面处理 Ed25519 密钥会得到一句明确的「不支持」提示,而不是一个错误的结果——换用新版浏览器就能继续。

JWKS 里的 kid 才是选择密钥的依据。JWKS 是一个「密钥集合」,靠每个密钥的 kid(Key ID)来区分。验签方拿 JWT 头部里的 kid 去 JWKS 里找对应的公钥,选中之后才开始验签。这个页面做转换时会保留原 JWK 里的 kid、use 等附加字段,但单个 PEM 文件本身并不携带 kid 信息,所以从 PEM 转出来的 JWK 里不会有 kid——需要的话得自己补上,而且要保证和签发方使用的一致。

和命令行的对应关系

把 PEM 转成 JWK 在命令行里没有现成的一条命令——openssl 不认识 JOSE 格式,通常要写脚本调库。这也是这类转换放在网页上做最省事的原因:不用装 JDK、不用记 Node 的 crypto 参数。

反过来 JWK 转 PEM 就简单多了:把 JWK 里的 n、e 还原成 RSA 公钥,用 openssl rsa -pubin 之类的工具都能读。本页输出的就是标准 PEM,可以直接丢给 Nginx、Java KeyStore 的导入工具或者任何读 PEM 的程序。