JWE : chiffrer un jeton JSON sans se tromper
- Accueil
- Développement & chaîne logicielle
- JWE : chiffrer un jeton JSON sans se tromper
JSON Web Encryption (JWE)
Section intitulée « JSON Web Encryption (JWE) »JWE (JSON Web Encryption) est le format normalisé (RFC 7516) pour transporter une charge utile chiffrée dans un jeton compact, cousin de JWS (JSON Web Signature). Il chiffre, il authentifie l’intégrité du contenu, et il ne dit rien sur l’identité de l’émetteur. Cette nuance décide de la moitié des architectures qui l’utilisent mal.
Imagine un colis fermé par un cadenas, dont la clé est elle-même glissée dans une enveloppe réservée au destinataire. Le paquet voyage sans livrer son contenu aux curieux. Ce format sert à envoyer ou conserver des informations sensibles en laissant leur lecture à la seule bonne personne.
→ Lire l’explication complète de la marmotte
Structure d’un JWE compact et rôle de la CEK
Section intitulée « Structure d’un JWE compact et rôle de la CEK »Le format compact aligne cinq segments séparés par des points : en-tête protégé, clé chiffrée, vecteur d’initialisation, texte chiffré, tag d’authentification. Deux étages de clés cohabitent. La CEK (Content Encryption Key) chiffre réellement la charge utile avec un algorithme symétrique authentifié (champ enc). Cette CEK est elle-même protégée par la gestion de clé indiquée dans alg : chiffrement asymétrique, wrapping symétrique, ou accord de clé.
Conséquence directe : un JWE ne chiffre jamais la charge utile avec la clé publique du destinataire. La clé publique ne sert qu’à transporter une CEK aléatoire de 128, 192 ou 256 bits. C’est ce qui permet à un même texte chiffré d’être adressé à plusieurs destinataires dans la sérialisation JSON générale (un objet recipients par destinataire, une seule copie du ciphertext).
L’en-tête protégé est intégré dans le calcul du tag comme AAD (Additional Authenticated Data), après encodage base64url. Modifier enc, kid ou alg casse donc la vérification. En revanche, l’en-tête reste en clair : kid, alg, enc et tout paramètre applicatif qu’on y glisse fuient intégralement. Y mettre un identifiant de tenant ou un e-mail revient à publier la donnée qu’on croyait protéger.
Choisir alg et enc : ce qui se recommande, ce qui se bannit
Section intitulée « Choisir alg et enc : ce qui se recommande, ce qui se bannit »Pour enc, deux familles seulement méritent d’être déployées. A256GCM (AES en mode Galois/Counter, 256 bits) est le défaut raisonnable : rapide, largement accéléré en matériel, tag de 128 bits. A128CBC-HS256 et ses variantes restent acceptables pour l’interopérabilité avec des piles anciennes, mais imposent un Encrypt-then-MAC dont les implémentations ont historiquement fourni des oracles de padding. Si le choix est libre, prendre GCM et ne pas y revenir. XC20P (XChaCha20-Poly1305) est une option sérieuse hors AES matériel, avec une réserve : il n’est pas dans la RFC 7518 d’origine et l’interopérabilité se vérifie avant de s’engager.
Pour alg, la ligne est plus tranchée. RSA1_5 (RSAES-PKCS1-v1_5) est à interdire sans discussion : la RFC 8725 le déconseille, et l’attaque de Bleichenbacher sur le déchiffrement de la CEK a suffisamment de variantes pour qu’aucune implémentation ne soit crédible en défense. RSA-OAEP-256 fonctionne mais paie le coût RSA et produit des jetons épais (256 octets de clé chiffrée pour une clé de 2048 bits). Le bon défaut moderne est ECDH-ES+A256KW sur P-256 ou X25519 : accord de clé éphémère-statique, clé chiffrée compacte, et une propriété appréciable, la clé privée du destinataire ne déchiffre rien sans l’epk (ephemeral public key) présente dans l’en-tête. Les détails d’échange de clé sont traités dans établir une clé avec Diffie-Hellman.
dir (direct) et A256KW supposent un secret partagé préexistant. C’est parfait pour un cookie de session chiffré par le serveur qui l’a émis, catastrophique dès qu’il y a plus d’un consommateur : le déchiffrement devient une capacité de forge, puisque quiconque peut déchiffrer peut aussi produire un jeton valide.
Le piège central : JWE ne prouve rien sur l’émetteur
Section intitulée « Le piège central : JWE ne prouve rien sur l’émetteur »Le compromis à nommer explicitement : JWE achète la confidentialité et l’intégrité de la charge utile, il ne fournit aucune authentification d’origine. Avec RSA-OAEP-256 ou ECDH-ES, la clé publique du destinataire est publique par définition. N’importe qui peut fabriquer un JWE parfaitement valide, dont le tag AEAD se vérifie, et qui contient les revendications de son choix.
D’où la règle : un JWE qui porte des décisions d’autorisation doit encapsuler un JWS. Le motif nested JWT est normalisé, signalé par cty: JWT dans l’en-tête du JWE, et l’ordre compte : signer d’abord, chiffrer ensuite. Signer un texte chiffré ne prouve que la possession du texte chiffré, pas la connaissance du contenu. La logique de vérification des signatures est détaillée dans JWS.
Corollaire opérationnel : déchiffrer n’est pas valider. Un service qui déchiffre puis lit les revendications sans vérifier la signature interne, ni iss, ni aud, ni exp, accorde des droits à un attaquant qui n’a eu besoin que d’un fichier JWKS public.
Rotation, kid et échecs de déchiffrement
Section intitulée « Rotation, kid et échecs de déchiffrement »Le déchiffrement n’a pas d’échappatoire : contrairement à une signature qu’on peut retenter sur plusieurs clés à coût nul, un JWE mal adressé produit un échec AEAD indistinguable d’une altération. Le kid de l’en-tête est donc structurant, pas décoratif. Sans lui, le destinataire essaie toutes ses clés privées, et la latence de déchiffrement devient proportionnelle au nombre de clés en rotation.
Autre règle générale à intégrer dès la conception : la rotation d’une paire de déchiffrement n’est jamais instantanée. Une clé retirée du JWKS publié continue d’être utilisée par les émetteurs qui ont un cache, souvent plusieurs minutes. La clé privée doit donc rester chargée côté destinataire pendant au moins la durée de vie du cache JWKS plus la durée de vie maximale du jeton. Supprimer la clé privée au moment où on publie la nouvelle clé publique génère une panne différée, difficile à corréler. Voir exploiter les clés et la rotation pour le cadre général.
Dernier point, souvent oublié : le zip: DEF (compression DEFLATE avant chiffrement) existe dans la RFC et doit être évité. Compresser avant de chiffrer fait fuiter de l’information par la taille du texte chiffré, exactement le mécanisme exploité par CRIME et BREACH sur TLS. Et côté destinataire, décompresser une charge avant validation ouvre un vecteur de bombe de décompression.
Exemple reproductible : nested JWT avec jose
Section intitulée « Exemple reproductible : nested JWT avec jose »Le code suivant produit un JWS signé en ES256, l’encapsule dans un JWE ECDH-ES+A256KW / A256GCM, puis fait le chemin inverse. Testé avec Node 20 et le paquet jose v5 (npm i jose).
import { generateKeyPair, SignJWT, EncryptJWT, jwtDecrypt, jwtVerify } from 'jose';
// Deux paires distinctes : signature (émetteur) et chiffrement (destinataire).// Réutiliser une seule paire pour les deux usages est une faute de conception.const sig = await generateKeyPair('ES256');const enc = await generateKeyPair('ECDH-ES+A256KW');
// 1) Signature d'abord : c'est elle qui porte l'authentification d'origine.const inner = await new SignJWT({ sub: 'svc-facturation', scope: 'read' }) .setProtectedHeader({ alg: 'ES256', kid: 'sig-2024-11' }) .setIssuer('https://idp.interne').setAudience('https://api.interne') .setExpirationTime('5m') .sign(sig.privateKey);
// 2) Chiffrement ensuite. cty=JWT signale au destinataire un jeton imbriqué.const token = await new EncryptJWT({ nested: inner }) .setProtectedHeader({ alg: 'ECDH-ES+A256KW', enc: 'A256GCM', cty: 'JWT', kid: 'enc-2024-11' }) .encrypt(enc.publicKey);
// 3) Déchiffrement : donne la confidentialité, PAS l'identité de l'émetteur.const { payload } = await jwtDecrypt(token, enc.privateKey);
// 4) Vérification de la signature interne : sans cette étape, n'importe qui// ayant la clé publique de chiffrement peut forger le jeton entier.const { payload: claims } = await jwtVerify(payload.nested, sig.publicKey, { issuer: 'https://idp.interne', audience: 'https://api.interne',});console.log(claims.sub); // svc-facturationRetirer l’étape 4 laisse le programme fonctionner sans erreur. C’est précisément ce qui rend l’omission fréquente.
Quand JWE est le bon outil, et quand il ne l’est pas
Section intitulée « Quand JWE est le bon outil, et quand il ne l’est pas »JWE se justifie quand la charge utile doit rester confidentielle vis-à-vis d’un intermédiaire qui la transporte : un jeton stocké dans un cookie côté navigateur, un jeton relayé par un tiers, un état OAuth qui contient des données personnelles. Là, TLS ne suffit pas parce que le point terminal du canal n’est pas le destinataire final des données. Le fonctionnement du canal est traité dans sécuriser un canal avec TLS.
JWE ne se justifie pas pour cacher des revendications sur un canal déjà chiffré de bout en bout entre deux services de confiance. Le gain est nul, le coût réel : opacité au débogage, dépendance à une distribution de clés supplémentaire, et surface d’erreur dans le choix de alg. Pour ce cas, un JWS suffit et reste inspectable. La règle simple : chiffrer un jeton quand on ne fait pas confiance à qui le porte, pas quand on ne fait pas confiance au réseau.
Enfin, JWE n’est pas un mécanisme de protection au repos. Un jeton chiffré stocké en base garde la même clé pendant toute la durée de vie de la clé de chiffrement, sans les propriétés de séparation qu’apporte une hiérarchie de clés dédiée ; voir protéger les données au repos.
🐝 Ce qui arrive si on s’en passe
Section intitulée « 🐝 Ce qui arrive si on s’en passe »Sans chiffrement des jetons, leur contenu (droits d’accès, identifiants, attributs personnels) se lit par simple décodage Base64URL partout où le jeton passe : journaux de requêtes, outils de débogage, caches, en-têtes relayés par un mandataire. Une signature seule n’y change rien, puisqu’elle garantit l’origine sans rien cacher. Chaque maillon qui voit transiter le jeton devient alors un point de fuite qu’aucune règle d’accès ne couvre, et un jeton intercepté livre au passage la liste des droits de son porteur, c’est-à-dire la carte des comptes qui valent la peine d’être visés. Les données personnelles copiées dans ces journaux échappent en outre à toute durée de conservation maîtrisée.
🦫 Explication de la marmotte 🍫
Section intitulée « 🦫 Explication de la marmotte 🍫 »Le mécanisme repose sur deux opérations distinctes. D’abord, le contenu à protéger est chiffré avec une clé de contenu créée pour l’occasion. Cette clé permet de transformer les données lisibles en données inutilisables sans le secret correspondant. Le chiffrement du contenu est rapide, ce qui compte dès que les messages deviennent nombreux ou volumineux.
Ensuite, cette clé de contenu est elle aussi protégée. Elle est chiffrée avec la clé publique du destinataire, ou obtenue à partir d’un échange de clés selon l’algorithme choisi. Seul le détenteur de la clé privée associée peut retrouver la clé de contenu. Il peut alors déchiffrer le message.
Le résultat regroupe un en-tête, la clé de contenu protégée, une valeur d’initialisation, le contenu chiffré et une valeur de contrôle. L’en-tête annonce les algorithmes employés. La valeur de contrôle permet de détecter une modification du message : un octet bricolé, et le déchiffrement doit échouer. C’est précisément le genre de détail qui évite de transformer une erreur réseau en très mauvaise surprise.
À lire aussi
- Formats, bibliothèques et API : rendre la crypto utilisableDonner une structure web-first pour choisir des API sûres, concevoir des formats robustes et éviter les ambiguïtés de sérialisation.
- Sécuriser un contrat API-first dès la conceptionConcevoir un contrat d’API comme une frontière de confiance, avec authentification, autorisation, validation et exploitation sécurisées.
- Chiffrer et authentifier en une seule constructionFaire de l’AEAD le modèle par défaut, choisir une primitive adaptée et maîtriser nonces, données associées, tags, rotation et limites de volume.
- Attributs du cookie de sessionCe que garantissent et ne garantissent pas les attributs Secure, HttpOnly, SameSite et le préfixe __Host- d'un cookie de session, avec la durée de vie et la révocation côté serveur.
Pages voisines
La citent
- Formats, bibliothèques et API : rendre la crypto utilisable
- Courbes elliptiques : compacité, validation et pièges
- Transformer un bloc en chiffrement de message
JWE : chiffrer un jeton JSON sans se tromper
Elle cite
- Lien dans les deux sens :JWS : signer et sécuriser vos échanges JSON
- Établir une clé : Diffie-Hellman, KEM et HPKE
- Exploiter les clés : rotation, KMS et secrets
- Frontières : cacher, coder, compresser et détruire
- Pièges OAuth 2.1 : PKCE, redirections et scopes
- Protéger les données au repos : disque, base, sauvegarde
- Sécuriser un canal: TLS, HTTPS et mTLS
: les deux pages se citent l’une l’autre.
