Skip to content
ecdsa.com

JWK vs PEM: JSON keys or ASN.1 keys

A JWK is a key as JSON; a PEM is a key as base64-wrapped DER. They carry the same numbers, and converting between them is lossless if you respect a few details.

JWK vs PEM is a question of where a key is going. JSON Web Keys (RFC 7517) are what JOSE, OAuth and WebCrypto speak; PEM files (RFC 7468 text encoding of DER structures such as SubjectPublicKeyInfo and PKCS #8) are what OpenSSL, TLS servers, SSH tools, Java and most PKI software expect. The cryptographic content is identical, so the choice is about tooling and metadata.

At a glance

JWK (RFC 7517)PEM (RFC 7468)
EncodingJSON object with base64url membersbase64 of DER between -----BEGIN …----- and -----END …-----
Public key structurekty plus type-specific members (crv, x, y, n, e)SubjectPublicKeyInfo (BEGIN PUBLIC KEY)
Private keySame object plus d (and RSA CRT members)PKCS #8 (BEGIN PRIVATE KEY), or PKCS #1 and SEC 1 legacy forms
Metadatakid, alg, use, key_ops, x5c, x5t#S256None (labels only)
CollectionsJWK Set: "keys" arrayMultiple PEM blocks in one file
Size, P-256 public key (measured)126 characters of JSON178 characters
Identifier hashJWK thumbprint (RFC 7638)SPKI SHA-256 fingerprint, certificate fingerprint
ConsumersJOSE libraries, WebCrypto jwk, OIDC jwks_uri, COSE convertersOpenSSL, nginx, Apache, Java X509EncodedKeySpec, Go x509, Python cryptography

What is inside each

A JWK spells out the key's numbers directly. An EC public key is kty: EC, crv: P-256, and the coordinates x and y as fixed-length base64url strings; an RSA key is kty: RSA with n and e; Ed25519 and X25519 keys are kty: OKP with crv and x (RFC 8037); private keys add d. Optional members bind the key to a purpose: alg (one algorithm), use (sig or enc), kid, and certificate references.

A PEM public key is the DER encoding of a SubjectPublicKeyInfo: an algorithm identifier (for example id-ecPublicKey with the P-256 OID) and a bit string holding the point. Private keys are PKCS #8 PrivateKeyInfo structures, optionally encrypted. PEM carries no metadata beyond its label, so the algorithm a key may be used with must be configured elsewhere.

Converting between them

The conversion is lossless. The script below, run with Node.js 26.10, converts a P-256 public key from PEM to JWK and back and computes both kinds of identifier.

Node.js: PEM to JWK and back, thumbprint versus SPKI hash
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);
Output
JWK members: kty, x, y, crv
PEM length: 178 JWK JSON length: 126
PEM -> JWK -> PEM identical: true
JWK thumbprint equals SPKI hash: false

The last line is the common surprise: a JWK thumbprint hashes canonical JSON, an SPKI fingerprint hashes DER, so the same key has two different "fingerprints". Systems must agree on which one they mean, for example DPoP's cnf.jkt is a JWK thumbprint, while certificate pinning uses SPKI hashes.

Pitfalls

  • Padding and length. JWK values are base64url without padding, and EC coordinates must keep their full length (32 bytes for P-256, 66 for P-521) including leading zero bytes. Hand-written converters that strip zeros create keys that fail about once in 128 cases on P-256 and P-384, since x and y each start with a zero byte about once in 256; on P-521, whose top byte holds a single bit, about three keys in four fail; Node reports invalid JWKs, and WebCrypto throws a DataError.
  • Private keys in public places. A JWK with d is a private key. Publishing it in a JWKS or pasting it into a tool exposes the key; this site's tools refuse JWKs with d where only a public key is needed.
  • Wrong PEM label. BEGIN RSA PUBLIC KEY (PKCS #1) and BEGIN PUBLIC KEY (SPKI) are different structures; many JWK converters accept only SPKI.
  • Lost metadata. Converting a JWK to PEM drops kid and alg; keep them in configuration so verifiers still pin the algorithm.
  • Ed25519 private keys. JWK d is the 32-byte seed; some libraries' raw private keys are 64 bytes (seed plus public key).
  • Certificates are not keys. A JWK may reference a certificate chain in x5c, but those entries are standard base64 DER certificates, not base64url, and verifiers must validate the chain before trusting the key inside; a PEM BEGIN CERTIFICATE block likewise contains a public key, but extracting it (openssl x509 -pubkey -noout) is a separate step from trusting it.

When to choose which

  • Token verification keys published by an issuer: JWK Set at jwks_uri.
  • Keys handed to JOSE libraries or WebCrypto in browsers: JWK (WebCrypto also accepts SPKI and PKCS #8 DER).
  • TLS server keys and certificates, CSRs, OpenSSL workflows: PEM.
  • Java, .NET and Go code using standard key classes: PEM or DER (SPKI and PKCS #8), with JWK supported through JOSE libraries.
  • Configuration files and secrets managers: either; PEM is more widely supported by tools, JWK keeps metadata with the key.
  • Pinning and key identifiers: pick one hash definition (JWK thumbprint or SPKI SHA-256) and document it.

Migration notes

  1. Generate keys in the store that will hold them, then export the public part in the format each consumer needs; do not move private keys between formats more often than necessary.
  2. Convert with libraries: node:crypto (createPublicKey and export), Python cryptography with jwcrypto or joserfc, jose in JavaScript, Nimbus in Java, or the JWK converter, which runs locally.
  3. Set kid, alg and use explicitly when you create JWKs for a JWKS; derive kid from the JWK thumbprint if you want stable identifiers.
  4. Validate after conversion: sign with the private key and verify with the converted public key before publishing.
  5. Keep both representations in sync during rotation, since a JWKS and a PEM-configured gateway may otherwise disagree about the current key.

References

All comparisons