Ce que signifient vraiment les noms alg
Un JWT est un JWS (JSON Web Signature) dont la charge utile est du JSON : header.payload.signature, chaque partie étant encodée en base64url. L’en-tête alg nomme le schéma de signature, et le registre se trouve dans la RFC 7518 (JSON Web Algorithms). Les quatre valeurs que vous rencontrez en pratique :
| alg | Schéma | Type de clé | Taille de la signature |
|---|---|---|---|
HS256 | HMAC avec SHA-256 | Un secret partagé (symétrique) | 32 octets |
RS256 | RSASSA-PKCS1-v1_5 avec SHA-256 | Paire de clés RSA | 256 octets (RSA-2048) |
PS256 | RSASSA-PSS avec SHA-256 | Paire de clés RSA | 256 octets (RSA-2048) |
ES256 | ECDSA sur P-256 avec SHA-256 | Paire de clés EC | 64 octets (r‖s brut) |
Ce tableau cache deux points structurels. Premièrement, HS256 est fondamentalement différent : il est symétrique, donc toute partie capable de vérifier un jeton peut aussi en fabriquer un. Il ne convient que lorsque l’émetteur et le vérificateur sont le même service. RS256, PS256 et ES256 sont asymétriques : les vérificateurs ne détiennent que la clé publique, qu’on peut leur remettre librement, par exemple via un point de terminaison JWKS.
Deuxièmement, les noms ES fixent la courbe : ES256 signifie P-256 avec SHA-256, ES384 signifie P-384 avec SHA-384, et ES512 signifie – attention aux chiffres – la courbe P-521 de 521 bits avec SHA-512. PS256 utilise le même matériel de clé RSA que RS256, avec le remplissage PSS, plus récent ; si votre écosystème le prend en charge, PSS est le meilleur mode RSA. (Pour une comparaison plus approfondie des algorithmes, au-delà des JWT, voir ECDSA vs RSA.)
Taille du jeton : 64 octets contre 256 octets de signature
Une signature ES256 fait 64 octets bruts, soit environ 86 caractères base64url. Une signature RS256 avec la clé courante de 2048 bits fait 256 octets, soit environ 342 caractères, et 384 octets avec RSA-3072. Comme l’en-tête et la charge utile sont identiques dans les deux cas, passer de RS256 à ES256 retire généralement environ 250 octets à chaque jeton. Cela compte quand les jetons voyagent dans des cookies (budget de 4 Ko), dans des en-têtes Authorization à chaque requête, dans des codes QR ou dans le trafic mobile et IoT. La clé publique ES256 est aussi bien plus petite, ce qui garde les documents JWKS compacts.
La vitesse suit la tendance décrite dans le guide ECDSA vs RSA : ES256 signe plus vite ; RS256 vérifie plus vite. Pour un émetteur de jetons qui signe en grand volume, ES256 est le côté le moins coûteux du compromis.
La règle qui évite la plupart des catastrophes JWT : épingler l’algorithme
L’erreur de configuration JWT classique consiste à laisser le jeton décider de la manière dont il sera vérifié. L’en-tête alg est une entrée contrôlée par l’attaquant : il arrive à l’intérieur même du jeton que vous n’avez pas encore vérifié. Un vérificateur qui lit alg dans l’en-tête et s’en sert pour aiguiller la vérification, avec un seul objet clé servant à plusieurs familles d’algorithmes, ruine ses propres garanties. Le cas de confusion bien connu : un service vérifie des jetons RS256 avec une clé publique, mais sa bibliothèque accepte aussi HS256 avec le même objet clé, en traitant les octets de la clé RSA publique comme un secret HMAC. Comme la clé publique est publique par conception, n’importe qui peut calculer un MAC HS256 correspondant et fabriquer des jetons que le service acceptera.
Le correctif est mécanique, et il est codifié dans la RFC 8725 (JWT Best Current Practices) :
- Vérifiez avec une liste d’autorisation explicite des algorithmes, idéalement réduite à une seule valeur, configurée côté serveur et jamais déduite du jeton.
- Liez les clés aux algorithmes. Dans un JWKS, donnez à chaque clé ses
algetkid; une clé publiée pour ES256 ne doit jamais pouvoir servir à autre chose. Les contrôles du type de clé aident aussi : une clé EC P-256 ne peut physiquement pas vérifier du RS256, ce qui est un argument de plus en faveur d’ES256 dans les environnements mixtes. - Validez explicitement les claims (émetteur, audience, expiration) dans le même appel.
import { createRemoteJWKSet, jwtVerify } from "jose";
const jwks = createRemoteJWKSet(new URL("https://issuer.example/.well-known/jwks.json"));
const { payload } = await jwtVerify(token, jwks, {
algorithms: ["ES256"], // fixed by the verifier, not by the token
issuer: "https://issuer.example",
audience: "api://orders",
});
import jwt
payload = jwt.decode(
token,
public_key_pem,
algorithms=["ES256"], # required parameter in modern PyJWT — for good reason
audience="api://orders",
issuer="https://issuer.example",
)
Pourquoi « none » n’est jamais acceptable
La RFC 7518 enregistre aussi "alg": "none", le « JWS non sécurisé » (unsecured JWS), c’est-à-dire un jeton dont la partie signature est vide. Il existe pour le cas rare où l’intégrité est garantie par une couche externe. Un vérificateur qui accepte none accepte des jetons non signés : n’importe qui peut fabriquer n’importe quelle charge utile et la présenter. Les premières bibliothèques JWT (vers 2015) traitaient notoirement none comme un algorithme parmi d’autres, si bien que des jetons non signés passaient la vérification par défaut. Les bibliothèques modernes le rejettent sauf activation explicite, et avec une liste d’autorisation ["ES256"], il ne peut jamais être sélectionné. Si vous auditez une base de code, tout chemin d’exécution qui peut atteindre none ou une liste d’autorisation vide est une anomalie à signaler.
Le piège d’ES256 : les signatures JWS sont en r‖s brut, pas en DER
Mathématiquement, une signature ECDSA est un couple d’entiers (r, s), et il existe deux encodages courants de ce couple en octets. La RFC 7518 §3.4 est explicite : dans un JWS, la signature ES256 est formée de r et s, deux entiers big-endian de 32 octets, concaténés, soit exactement 64 octets, puis encodée en base64url. Ce n’est pas la SEQUENCE ASN.1 DER que produisent par défaut OpenSSL, la classe Signature de Java et crypto.sign() de Node.
C’est l’une des causes les plus courantes de « signature invalide » dans le code JWT fait maison : le jeton transporte un bloc DER d’environ 70 octets (ou le vérificateur passe une signature brute de 64 octets à une API qui attend du DER), et la vérification échoue alors que la clé, le message et l’algorithme sont tous corrects. Si vous signez avec le module crypto intégré de Node, demandez directement le format JWS :
import { createPrivateKey, sign } from "node:crypto";
const key = createPrivateKey(pem); // P-256 private key
const signingInput = Buffer.from(`${headerB64}.${payloadB64}`);
const signature = sign("sha256", signingInput, {
key,
dsaEncoding: "ieee-p1363", // raw r||s — what JWS requires (default is DER)
});
console.log(signature.length); // 64
Vous tombez en conditions réelles sur une signature qui n’a pas la bonne forme ? Le convertisseur DER ⇄ brut convertit dans les deux sens et affiche les valeurs r et s ; les détails au niveau de l’octet, ainsi que la subtilité high-S, sont traités dans Formats de signature ECDSA : DER ou brut. Et pour vérifier un vrai jeton (en-tête, claims et vérification ES256 avec votre clé publique), collez-le dans le débogueur JWT.
Choisir, et migrer sans casse
Migrer un déploiement RS256 existant vers ES256 est un exercice de rotation de clé, pas un basculement brutal du jour au lendemain :
- Publiez la nouvelle clé EC dans votre JWKS à côté de la clé RSA, chacune avec des valeurs
kidetalgcorrectes, et laissez aux consommateurs le temps de rafraîchir leurs caches JWKS. - Commencez à émettre des jetons ES256 avec le nouveau
kid. Les vérificateurs qui sélectionnent les clés parkidet imposent un algorithme par clé continuent d’accepter les deux générations de jetons, chacune avec exactement une clé. - Une fois que tous les jetons RS256 ont expiré, retirez la clé RSA du JWKS et ramenez la liste d’autorisation du vérificateur à
["ES256"].
La phrase à retenir : c’est le vérificateur, et non le jeton, qui choisit l’algorithme. Respectez cette règle, et le choix entre ES256 et RS256 redevient ce qu’il devrait être : un simple compromis d’ingénierie sur des octets et des millisecondes.