Aller au contenu
ecdsa.com

JWS vs JWE : jetons JOSE signés ou chiffrés

Un JWS prouve qui a créé une charge utile, mais n’importe qui peut la lire. Un JWE cache la charge utile à tous sauf à son destinataire, mais, avec des algorithmes à clé publique, ne dit rien de qui l’a créée.

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

JWSJWE
SpécificationRFC 7515RFC 7516
ProtègeL’intégrité et l’origineLa confidentialité (et l’intégrité via l’AEAD)
Forme compacteheader.payload.signature (3 parties)header.encrypted_key.iv.ciphertext.tag (5 parties)
Algorithmes de l’en-têtealg (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 visibleOui, seulement encodée en base64urlNon
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 typiquesJetons d’accès et jetons d’ID JWT, DPoP, requêtes signées, SD-JWTJetons 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.

jose : les mêmes claims en JWS et en JWE
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"));
Sortie
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_5 est vulnérable aux attaques par oracle de remplissage, PBES2 avec un nombre d’itérations p2c énorme peut servir à un déni de service, et des implémentations d’ECDH-ES qui acceptaient des points hors de la courbe ont laissé fuir des clés privées en 2017 (RFC 8725 §2.5). Préférez ECDH-ES+A256KW ou RSA-OAEP-256 avec A256GCM, 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 alg et enc qu’il attend pour chaque clé.
  • Compression. Le zip: DEF de 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+A256KW et A256GCM, ou formats fondés sur HPKE à mesure qu’ils sont normalisés pour JOSE.

Notes de migration

  1. 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.
  2. Introduisez progressivement les jetons imbriqués : émettez des JWE cty: JWT pour les clients capables de les déchiffrer, et laissez le JWS interne inchangé pour réutiliser la logique de vérification.
  3. Distribuez les clés de chiffrement séparément des clés de signature : les entrées du JWKS devraient porter use: enc ou use: sig et des valeurs kid distinctes.
  4. Épinglez les algorithmes des deux côtés et rejetez RSA1_5, PBES2 sans limite et la compression.
  5. 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.

Références

Tous les comparatifs