JWS vs JWE, c’est la question de ce que protège un jeton JOSE. Une JSON Web Signature (RFC 7515) ajoute l’intégrité et l’authenticité à une charge utile qui reste lisible ; une JSON Web Encryption (RFC 7516) ajoute la confidentialité. Presque tous les JWT que vous rencontrerez sont des JWS, ce qui surprend les personnes qui s’attendent à ce que les claims soient secrets.
Vue d’ensemble
| JWS | JWE | |
|---|---|---|
| Spécification | RFC 7515 | RFC 7516 |
| Protège | L’intégrité et l’origine | La confidentialité (et l’intégrité via l’AEAD) |
| Forme compacte | header.payload.signature (3 parties) | header.encrypted_key.iv.ciphertext.tag (5 parties) |
| Algorithmes de l’en-tête | alg (par exemple ES256, RS256, HS256, Ed25519) | alg pour la gestion des clés (ECDH-ES, ECDH-ES+A256KW, RSA-OAEP-256, A256KW, dir), plus enc pour le contenu (A256GCM, A128CBC-HS256) |
| Charge utile visible | Oui, seulement encodée en base64url | Non |
| Expéditeur authentifié | Oui (signature ou MAC) | Seulement avec des clés symétriques ou un accord de clé authentifié ; pas avec ECDH-ES ou RSA-OAEP seuls |
| Taille pour un ensemble de claims de 70 octets (mesurée plus bas) | 186 caractères (ES256) | 405 caractères (ECDH-ES+A256KW avec A256GCM) |
| Usages typiques | Jetons d’accès et jetons d’ID JWT, DPoP, requêtes signées, SD-JWT | Jetons d’ID chiffrés, jetons stockés côté client, certaines réponses FAPI et EUDI |
Ce que protège chacun
Un JWS signe base64url(header) + "." + base64url(payload). Quiconque détient le jeton peut décoder et lire les claims ; le débogueur JWT fait exactement cela. La signature prouve que le jeton provient du détenteur de la clé et n’a pas été modifié, et c’est ce dont une API a besoin pour faire confiance aux claims.
Un JWE génère une clé de chiffrement du contenu (CEK) aléatoire, chiffre la charge utile avec l’AEAD désigné par enc et transmet la CEK au destinataire avec l’algorithme de gestion des clés désigné par alg : enveloppée avec RSA-OAEP, enveloppée par AES Key Wrap après un accord de clé ECDH-ES, dérivée directement d’ECDH-ES, ou partagée à l’avance (dir). L’en-tête protégé est authentifié en tant que données associées. Seul le destinataire peut lire la charge utile.
L’asymétrie importante : avec une gestion des clés à clé publique, quiconque possède la clé publique du destinataire peut créer un JWE valide. Un déchiffrement réussi ne prouve rien sur l’expéditeur. C’est pourquoi les jetons sensibles auxquels il faut aussi pouvoir faire confiance sont imbriqués : un JWT signé (JWS) placé dans un JWE avec cty: JWT, puis vérifié après le déchiffrement.
Tailles en pratique
Le script ci-dessous, exécuté avec jose 6.2.13 sous Node.js 26.10, protège les mêmes claims des deux façons.
import { CompactEncrypt, CompactSign, compactDecrypt, compactVerify, generateKeyPair } from "jose";
const claims = new TextEncoder().encode(JSON.stringify({ sub: "user-42", email: "alice@example.com", plan: "pro" }));
// JWS: integrity and origin; the payload is only base64url-encoded.
const signer = await generateKeyPair("ES256");
const jws = await new CompactSign(claims).setProtectedHeader({ alg: "ES256" }).sign(signer.privateKey);
await compactVerify(jws, signer.publicKey);
// JWE: confidentiality for the holder of the recipient's private key.
const recipient = await generateKeyPair("ECDH-ES+A256KW");
const jwe = await new CompactEncrypt(claims).setProtectedHeader({ alg: "ECDH-ES+A256KW", enc: "A256GCM" }).encrypt(recipient.publicKey);
const { plaintext } = await compactDecrypt(jwe, recipient.privateKey);
console.log("JWS parts:", jws.split(".").length, "length:", jws.length, "payload readable:", Buffer.from(jws.split(".")[1], "base64url").toString().includes("alice"));
console.log("JWE parts:", jwe.split(".").length, "length:", jwe.length, "decrypts:", new TextDecoder().decode(plaintext).includes("alice"));
JWS parts: 3 length: 186 payload readable: true
JWE parts: 5 length: 405 decrypts: true
L’essentiel du surcoût du JWE vient de la clé publique éphémère (epk) dans l’en-tête, de la clé enveloppée, de l’IV de 12 octets et du tag de 16 octets. Un jeton imbriqué, signé puis chiffré, grossit encore, puisque le JWS entier devient le texte clair du JWE.
Pièges
- Traiter les claims d’un JWS comme secrets. Ne mettez ni données personnelles, ni identifiants internes que vous ne publieriez pas, ni secrets dans un jeton seulement signé ; il finira dans des journaux, dans le stockage du navigateur et dans des tickets de support.
- Faire confiance à un JWE non signé. Un déchiffrement avec ECDH-ES ou RSA-OAEP prouve seulement que le jeton a été chiffré à votre intention.
- Algorithmes JWE faibles ou anciens.
RSA1_5est vulnérable aux attaques par oracle de remplissage,PBES2avec un nombre d’itérationsp2cénorme peut servir à un déni de service, et des implémentations d’ECDH-ESqui acceptaient des points hors de la courbe ont laissé fuir des clés privées en 2017 (RFC 8725 §2.5). PréférezECDH-ES+A256KWouRSA-OAEP-256avecA256GCM, et plafonnez les itérations de PBES2 si vous devez le prendre en charge. - Choix de l’algorithme par le jeton. Comme pour un JWS, le destinataire devrait n’accepter que les valeurs
algetencqu’il attend pour chaque clé. - Compression. Le
zip: DEFde JWE compresse avant de chiffrer, ce qui peut laisser fuir, par la longueur, des informations sur le texte clair (RFC 8725 §3.6) ; évitez-le.
Quand choisir l’un ou l’autre
- Jetons d’accès vérifiés par vos API : JWS ; réduisez les claims au minimum, ou utilisez des jetons opaques avec introspection s’ils ne doivent pas être lisibles.
- Jetons d’ID contenant des données personnelles et stockés par un navigateur ou une application mobile : JWS le plus souvent ; JWE imbriqué si votre modèle de menace inclut l’environnement du client.
- Jetons qui transitent par des intermédiaires non fiables (redirections via des tiers, jetons intégrés dans des URL ou des codes QR) : JWS imbriqué dans un JWE.
- Messages de machine à machine sur TLS avec authentification mutuelle : souvent ni l’un ni l’autre ; TLS assure déjà la confidentialité et l’intégrité en transit, et un JWS n’apporte quelque chose que si le message doit rester vérifiable après avoir été stocké ou transféré.
- Charges utiles chiffrées de bout en bout pour un destinataire connu : JWE avec
ECDH-ES+A256KWetA256GCM, ou formats fondés sur HPKE à mesure qu’ils sont normalisés pour JOSE.
Notes de migration
- Classez les claims des jetons JWS existants : tout ce qui ne doit pas être visible du détenteur quitte le jeton ou passe dans un JWE.
- Introduisez progressivement les jetons imbriqués : émettez des JWE
cty: JWTpour les clients capables de les déchiffrer, et laissez le JWS interne inchangé pour réutiliser la logique de vérification. - Distribuez les clés de chiffrement séparément des clés de signature : les entrées du JWKS devraient porter
use: encouuse: siget des valeurskiddistinctes. - Épinglez les algorithmes des deux côtés et rejetez
RSA1_5,PBES2sans limite et la compression. - Revérifiez la journalisation : une fois les jetons chiffrés, assurez-vous que les claims déchiffrés ne sont pas journalisés là où le jeton ne l’a jamais été. L’inspecteur JWE affiche l’en-tête et la structure d’un JWE sans le déchiffrer.