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) | |
|---|---|---|
| Encoding | JSON object with base64url members | base64 of DER between -----BEGIN …----- and -----END …----- |
| Public key structure | kty plus type-specific members (crv, x, y, n, e) | SubjectPublicKeyInfo (BEGIN PUBLIC KEY) |
| Private key | Same object plus d (and RSA CRT members) | PKCS #8 (BEGIN PRIVATE KEY), or PKCS #1 and SEC 1 legacy forms |
| Metadata | kid, alg, use, key_ops, x5c, x5t#S256 | None (labels only) |
| Collections | JWK Set: "keys" array | Multiple PEM blocks in one file |
| Size, P-256 public key (measured) | 126 characters of JSON | 178 characters |
| Identifier hash | JWK thumbprint (RFC 7638) | SPKI SHA-256 fingerprint, certificate fingerprint |
| Consumers | JOSE libraries, WebCrypto jwk, OIDC jwks_uri, COSE converters | OpenSSL, 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.
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
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
xandyeach 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 aDataError. - Private keys in public places. A JWK with
dis a private key. Publishing it in a JWKS or pasting it into a tool exposes the key; this site's tools refuse JWKs withdwhere only a public key is needed. - Wrong PEM label.
BEGIN RSA PUBLIC KEY(PKCS #1) andBEGIN PUBLIC KEY(SPKI) are different structures; many JWK converters accept only SPKI. - Lost metadata. Converting a JWK to PEM drops
kidandalg; keep them in configuration so verifiers still pin the algorithm. - Ed25519 private keys. JWK
dis 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 PEMBEGIN CERTIFICATEblock 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
- 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.
- Convert with libraries:
node:crypto(createPublicKeyandexport), Pythoncryptographywithjwcryptoorjoserfc,josein JavaScript, Nimbus in Java, or the JWK converter, which runs locally. - Set
kid,alganduseexplicitly when you create JWKs for a JWKS; derivekidfrom the JWK thumbprint if you want stable identifiers. - Validate after conversion: sign with the private key and verify with the converted public key before publishing.
- Keep both representations in sync during rotation, since a JWKS and a PEM-configured gateway may otherwise disagree about the current key.