JWK vs PEM dépend de l’endroit où va une clé. Les JSON Web Keys (RFC 7517) sont le format que parlent JOSE, OAuth et WebCrypto ; les fichiers PEM (l’encodage texte de la RFC 7468 pour des structures DER telles que SubjectPublicKeyInfo et PKCS #8) sont ce qu’attendent OpenSSL, les serveurs TLS, les outils SSH, Java et la plupart des logiciels de PKI. Le contenu cryptographique est identique : le choix porte donc sur l’outillage et les métadonnées.
Vue d’ensemble
| JWK (RFC 7517) | PEM (RFC 7468) | |
|---|---|---|
| Encodage | Objet JSON dont les membres sont en base64url | base64 du DER entre -----BEGIN …----- et -----END …----- |
| Structure de la clé publique | kty plus les membres propres au type (crv, x, y, n, e) | SubjectPublicKeyInfo (BEGIN PUBLIC KEY) |
| Clé privée | Le même objet plus d (et les membres CRT de RSA) | PKCS #8 (BEGIN PRIVATE KEY), ou les anciennes formes PKCS #1 et SEC 1 |
| Métadonnées | kid, alg, use, key_ops, x5c, x5t#S256 | Aucune (seulement des libellés) |
| Collections | JWK Set : tableau "keys" | Plusieurs blocs PEM dans un même fichier |
| Taille d’une clé publique P-256 (mesurée) | 126 caractères de JSON | 178 caractères |
| Condensat servant d’identifiant | Empreinte JWK (RFC 7638) | Empreinte SPKI SHA-256, empreinte du certificat |
| Consommateurs | Bibliothèques JOSE, jwk de WebCrypto, jwks_uri d’OIDC, convertisseurs COSE | OpenSSL, nginx, Apache, X509EncodedKeySpec de Java, x509 de Go, cryptography de Python |
Ce que contient chaque format
Une JWK écrit directement les nombres de la clé. Une clé publique EC se compose de kty: EC, de crv: P-256 et des coordonnées x et y sous forme de chaînes base64url de longueur fixe ; une clé RSA, de kty: RSA avec n et e ; les clés Ed25519 et X25519, de kty: OKP avec crv et x (RFC 8037) ; les clés privées ajoutent d. Des membres facultatifs lient la clé à un usage : alg (un seul algorithme), use (sig ou enc), kid et des références de certificats.
Une clé publique PEM est l’encodage DER d’une structure SubjectPublicKeyInfo : un identifiant d’algorithme (par exemple id-ecPublicKey avec l’OID de P-256) et une chaîne de bits qui contient le point. Les clés privées sont des structures PKCS #8 PrivateKeyInfo, éventuellement chiffrées. Un PEM ne porte aucune métadonnée en dehors de son libellé : l’algorithme avec lequel une clé peut être utilisée doit donc être configuré ailleurs.
Conversion entre les deux formats
La conversion est sans perte. Le script ci-dessous, exécuté avec Node.js 26.10, convertit une clé publique P-256 de PEM en JWK puis inversement, et calcule les deux types d’identifiant.
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
La dernière ligne est la surprise habituelle : une empreinte JWK hache du JSON canonique, une empreinte SPKI hache du DER, si bien que la même clé a deux « empreintes » différentes. Les systèmes doivent s’accorder sur celle dont ils parlent : par exemple, le cnf.jkt de DPoP est une empreinte JWK, tandis que l’épinglage de certificat utilise des condensats SPKI.
Pièges
- Remplissage et longueur. Les valeurs d’une JWK sont en base64url sans remplissage, et les coordonnées EC doivent conserver leur longueur complète (32 octets pour P-256, 66 pour P-521), octets nuls de tête compris. Les convertisseurs écrits à la main qui suppriment ces zéros produisent des clés qui échouent environ une fois sur 128 pour P-256 et P-384, puisque
xetycommencent chacune par un octet nul environ une fois sur 256 ; pour P-521, dont l’octet de poids fort ne contient qu’un seul bit, environ trois clés sur quatre échouent ; Node signale des JWK invalides, et WebCrypto lève une erreurDataError. - Clés privées dans des emplacements publics. Une JWK avec
dest une clé privée. La publier dans un JWKS ou la coller dans un outil expose la clé ; les outils de ce site refusent les JWK avecdlà où seule une clé publique est nécessaire. - Mauvais libellé PEM.
BEGIN RSA PUBLIC KEY(PKCS #1) etBEGIN PUBLIC KEY(SPKI) sont des structures différentes ; de nombreux convertisseurs JWK n’acceptent que SPKI. - Métadonnées perdues. Convertir une JWK en PEM fait disparaître
kidetalg; conservez-les dans la configuration pour que les vérificateurs continuent d’épingler l’algorithme. - Clés privées Ed25519. Le
dd’une JWK est la graine de 32 octets ; les clés privées brutes de certaines bibliothèques font 64 octets (graine plus clé publique). - Les certificats ne sont pas des clés. Une JWK peut référencer une chaîne de certificats dans
x5c, mais ces entrées sont des certificats DER en base64 standard, pas en base64url, et les vérificateurs doivent valider la chaîne avant de faire confiance à la clé qu’elle contient ; de même, un bloc PEMBEGIN CERTIFICATEcontient une clé publique, mais l’extraire (openssl x509 -pubkey -noout) est une étape distincte de la décision de lui faire confiance.
Quand choisir l’un ou l’autre
- Clés de vérification de jetons publiées par un émetteur : JWK Set à l’adresse
jwks_uri. - Clés transmises à des bibliothèques JOSE ou à WebCrypto dans les navigateurs : JWK (WebCrypto accepte aussi SPKI et PKCS #8 en DER).
- Clés et certificats de serveurs TLS, CSR, workflows OpenSSL : PEM.
- Code Java, .NET et Go qui utilise les classes de clés standard : PEM ou DER (SPKI et PKCS #8), avec une prise en charge de JWK par les bibliothèques JOSE.
- Fichiers de configuration et gestionnaires de secrets : l’un ou l’autre ; PEM est plus largement pris en charge par les outils, JWK conserve les métadonnées avec la clé.
- Épinglage et identifiants de clé : choisissez une seule définition du condensat (empreinte JWK ou SPKI SHA-256) et documentez-la.
Notes de migration
- Générez les clés dans le magasin qui les conservera, puis exportez la partie publique au format dont chaque consommateur a besoin ; ne faites pas passer les clés privées d’un format à l’autre plus souvent que nécessaire.
- Convertissez avec des bibliothèques :
node:crypto(createPublicKeyetexport),cryptographyen Python avecjwcryptooujoserfc,joseen JavaScript, Nimbus en Java, ou le convertisseur JWK, qui s’exécute localement. - Définissez
kid,algetuseexplicitement lorsque vous créez des JWK pour un JWKS ; dérivezkidde l’empreinte JWK si vous voulez des identifiants stables. - Validez après la conversion : signez avec la clé privée et vérifiez avec la clé publique convertie avant de publier.
- Gardez les deux représentations synchronisées pendant la rotation, car, sinon, un JWKS et une passerelle configurée en PEM peuvent ne pas s’accorder sur la clé courante.