Réponse courte. Signez avec ES256 quand les jetons sortent de votre contrôle : toutes les bibliothèques JOSE que nous avons testées le vérifient, tous les grands KMS cloud et HSM peuvent héberger la clé, et des normes comme FAPI 2.0 l’inscrivent dans leur liste. Choisissez Ed25519 quand vous contrôlez chaque vérificateur et pouvez conserver la clé en logiciel ou dans AWS KMS : ses signatures sont déterministes et ne peuvent pas être transformées en une seconde forme valide, ce qui élimine deux catégories d’erreurs propres à ECDSA. Si vous choisissez Ed25519, publiez-le pour l’instant sous la forme "alg": "EdDSA" et acceptez à la fois EdDSA et le nouveau nom Ed25519 côté vérification. À ce jour, moins de la moitié des bibliothèques testées comprennent le nouveau nom.
Tous les jetons et toutes les clés de cette page ont été générés pour elle. Les résultats des bibliothèques proviennent d’exécutions dans les images Docker officielles le 20 octobre 2026 ; les versions sont indiquées avec chaque résultat.
Ce que signifient les deux noms
| ES256 | EdDSA / Ed25519 | |
|---|---|---|
| Schéma | ECDSA sur P-256 avec SHA-256 | EdDSA sur Edwards25519 (RFC 8032 §5.1) |
| Définition JOSE | RFC 7518 §3.4 | RFC 8037, renommé par la RFC 9864 |
| Statut dans le registre JOSE de l’IANA | Recommandé+ | EdDSA : obsolète ; Ed25519 : facultatif |
| JWK | "kty": "EC", crv, x, y | "kty": "OKP", "crv": "Ed25519", x |
| Signature | r ‖ s, 64 octets | R ‖ S, 64 octets |
| Approuvé par FIPS 186-5 | oui | oui |
Le changement de nom compte plus qu’il n’y paraît. La RFC 8037 a enregistré une seule valeur EdDSA pour Ed25519 comme pour Ed448, si bien que l’alg seul n’indique pas quelle courbe utiliser ; c’est le crv de la clé qui le fait. La RFC 9864 (octobre 2025) a rendu obsolète ce nom polymorphe et enregistré les noms entièrement spécifiés Ed25519 et Ed448. Le format de la clé ne change pas ; seule la chaîne alg change. ES256 était déjà entièrement spécifié dans JOSE et n’a pas besoin d’être renommé. Dans COSE, la RFC 9864 a fait de même pour ECDSA : ES256 (-7) est rendu obsolète au profit de ESP256 (-9), ce qui concerne WebAuthn et les identifiants fondés sur COSE, pas les JWT.
Taille : identique là où ça compte
Les deux signatures font 64 octets, soit 86 caractères base64url. Les seules différences portent sur la chaîne d’en-tête et sur la clé publique :
ES256 token 253 chars, signature 64 bytes (86 chars)
EdDSA token 253 chars, signature 64 bytes (86 chars)
Ed25519 token 256 chars, signature 64 bytes (86 chars)
es public JWK 137 chars: {"kty":"EC","x":"B8UtJ0UT7zKuYn6NgB7h0E-L1-UF85FC-aoxjO7a2AE","y":"gwHfmffA6Vv9eWTKD_XCwGoLj5JtI1DQtGVq27icAU4","crv":"P-256","kid":"es"}
ed public JWK 90 chars: {"kty":"OKP","crv":"Ed25519","x":"NzIuH6R_919ooSpy2wU9iMMhGjaS3yXnjLF0AiqAC04","kid":"ed"}
Une clé publique Ed25519 fait 32 octets, une clé P-256 se compose de deux coordonnées de 32 octets, si bien qu’un JWKS diminue d’environ 47 caractères par clé. Personne ne devrait choisir un algorithme sur ce seul critère. Face à RS256, la situation est différente : une signature RSA-2048 fait 256 octets, ce qui explique pourquoi les deux courbes l’emportent pour les jetons placés dans des cookies et des en-têtes (voir ES256 vs RS256).
Déterminisme : là où ECDSA peut échouer et EdDSA non
ECDSA a besoin d’un nombre secret neuf pour chaque signature. Réutilisez-le une seule fois, ou laissez-le devenir légèrement prévisible, et la clé privée peut être calculée à partir de signatures publiques ; l’article Réutilisation de nonce ECDSA détaille l’algèbre et les incidents réels. La plupart des bibliothèques JOSE tirent ce nombre du générateur aléatoire du système, si bien que la même entrée donne une signature différente à chaque fois. La RFC 6979 définit une manière déterministe de le dériver, mais que votre bibliothèque, votre HSM ou votre KMS l’utilise ou non échappe à votre contrôle.
EdDSA dérive par construction la valeur propre à chaque signature à partir de la clé privée et du message (RFC 8032 §5.1.6). Même clé, même message, même signature, sans aucun générateur aléatoire. Cela supprime le mode de défaillance à l’origine des fuites de clés de la PlayStation 3 et, en 2013, des portefeuilles Android. Cela ne supprime pas tous les risques : la signature déterministe est plus exposée à l’injection de fautes sur du matériel auquel un attaquant a physiquement accès, ce qui explique pourquoi certaines conceptions plus récentes réintroduisent de l’aléa. Pour les émetteurs de JWT qui fonctionnent sur des serveurs, le déterminisme est un net avantage.
Malléabilité : un second jeton valide sans effort
Une signature ECDSA (r, s) a un jumeau : (r, n − s), où n est l’ordre de la courbe, se vérifie avec la même clé et le même message. Les bibliothèques JOSE ne rejettent pas ce jumeau. Ed25519, tel que le spécifie la RFC 8032, exige que le vérificateur rejette tout S supérieur ou égal à l’ordre du groupe L, si bien qu’ajouter L à S ne produit pas une seconde signature valide. Les deux affirmations, testées :
// Determinism and malleability of ES256 vs EdDSA tokens, checked with jose and jsonwebtoken.
import { readFileSync } from "node:fs";
import { SignJWT, generateKeyPair, importJWK, jwtVerify } from "jose";
import jwt from "jsonwebtoken";
const claims = { sub: "user-42", exp: 1924992000 };
const es = await generateKeyPair("ES256");
const ed = await generateKeyPair("Ed25519");
const sig = (t) => t.split(".")[2].slice(0, 16) + "...";
const signTwice = async (alg, key) => [
await new SignJWT(claims).setProtectedHeader({ alg }).sign(key),
await new SignJWT(claims).setProtectedHeader({ alg }).sign(key),
];
const [es1, es2] = await signTwice("ES256", es.privateKey);
const [ed1, ed2] = await signTwice("EdDSA", ed.privateKey);
console.log(`ES256 same input twice: ${sig(es1)} vs ${sig(es2)} -> identical: ${es1 === es2}`);
console.log(`EdDSA same input twice: ${sig(ed1)} vs ${sig(ed2)} -> identical: ${ed1 === ed2}`);
// Second valid signature for the same ES256 token: (r, n - s).
const n = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551n;
const tokens = JSON.parse(readFileSync("out/tokens.json", "utf8"));
const keys = JSON.parse(readFileSync("out/keys.json", "utf8"));
const flipS = (token) => {
const [h, p, s] = token.split(".");
const raw = Buffer.from(s, "base64url");
const sValue = BigInt("0x" + raw.subarray(32).toString("hex"));
const flipped = Buffer.from((n - sValue).toString(16).padStart(64, "0"), "hex");
return `${h}.${p}.${Buffer.concat([raw.subarray(0, 32), flipped]).toString("base64url")}`;
};
// For Ed25519, S + L encodes the same scalar mod L; RFC 8032 requires S < L.
const L = 2n ** 252n + 27742317777372353535851937790883648493n;
const addL = (token) => {
const [h, p, s] = token.split(".");
const raw = Buffer.from(s, "base64url");
const S = BigInt("0x" + Buffer.from(raw.subarray(32)).reverse().toString("hex"));
const bumped = Buffer.from((S + L).toString(16).padStart(64, "0"), "hex").reverse();
return `${h}.${p}.${Buffer.concat([raw.subarray(0, 32), bumped]).toString("base64url")}`;
};
const check = async (label, token, alg, keyName) => {
const out = [];
try {
await jwtVerify(token, await importJWK(keys[keyName].jwk, alg), { algorithms: [alg] });
out.push("jose: accepted");
} catch (e) {
out.push(`jose: ${e.code}`);
}
if (alg === "ES256") {
try {
jwt.verify(token, keys[keyName].pem, { algorithms: [alg] });
out.push("jsonwebtoken: accepted");
} catch (e) {
out.push(`jsonwebtoken: ${e.message}`);
}
}
console.log(`${label.padEnd(30)} ${out.join(" | ")}`);
};
const esFlipped = flipS(tokens.ES256);
const edBumped = addL(tokens.EdDSA);
console.log(`ES256 token and its (r, n-s) twin differ: ${tokens.ES256 !== esFlipped}`);
await check("ES256 original", tokens.ES256, "ES256", "es");
await check("ES256 with s replaced by n-s", esFlipped, "ES256", "es");
await check("EdDSA original", tokens.EdDSA, "EdDSA", "ed");
await check("EdDSA with S replaced by S+L", edBumped, "EdDSA", "ed");
ES256 same input twice: b7-5MuAwyrsT26t_... vs ct-P2K38kCt5LlK4... -> identical: false
EdDSA same input twice: riZF8DpT02Grlv3U... vs riZF8DpT02Grlv3U... -> identical: true
ES256 token and its (r, n-s) twin differ: true
ES256 original jose: accepted | jsonwebtoken: accepted
ES256 with s replaced by n-s jose: accepted | jsonwebtoken: accepted
EdDSA original jose: accepted
EdDSA with S replaced by S+L jose: ERR_JWS_SIGNATURE_VERIFICATION_FAILED
Le jumeau ES256 porte les mêmes claims, il ne permet donc à personne de forger un jeton. En revanche, il casse toute logique qui traite la chaîne du jeton comme son identité. Si vous révoquez un jeton divulgué en stockant son condensat, ou si vous dédupliquez les requêtes d’après la chaîne du jeton, le détenteur peut présenter le jumeau et passer. Identifiez les jetons par un claim jti, jamais par leurs octets. La normalisation low-S, qu’impose Bitcoin, ne fait pas partie de JOSE, et aucune des bibliothèques testées ne l’applique.
Vitesse : pas un critère décisif
L’affirmation courante selon laquelle Ed25519 serait « deux fois plus rapide » dépend de l’implémentation. Sur la même machine (arm64, un cœur, dans Docker), signature et vérification de l’entrée de signature d’un JWT :
| Implémentation | Signature ES256 | Vérification ES256 | Signature Ed25519 | Vérification Ed25519 |
|---|---|---|---|---|
| Node.js 24.22, OpenSSL 3.5.8 | 64 840/s | 23 163/s | 60 249/s | 23 398/s |
| Go 1.27.3, bibliothèque standard | 53 877/s | 25 778/s | 75 218/s | 35 639/s |
Avec OpenSSL, les deux sont à quelques pour cent l’un de l’autre ; l’implémentation Ed25519 de Go est environ 40 % plus rapide que son implémentation P-256. Dans tous les cas, un seul cœur vérifie plus de vingt mille jetons par seconde, bien au-delà de ce que permettent le réseau et l’analyse JSON qui l’entourent. Votre matériel donnera d’autres chiffres absolus. La mesure Node :
// Single-thread sign and verify throughput of a JWS signing input, ES256 vs Ed25519, node:crypto (OpenSSL).
import { generateKeyPairSync, sign, verify } from "node:crypto";
const input = Buffer.from("eyJhbGciOiJFUzI1NiJ9." + Buffer.from(JSON.stringify({ sub: "user-42", exp: 1924992000 })).toString("base64url"));
const algs = {
ES256: { keys: generateKeyPairSync("ec", { namedCurve: "P-256" }), hash: "sha256", opts: { dsaEncoding: "ieee-p1363" } },
Ed25519: { keys: generateKeyPairSync("ed25519"), hash: null, opts: {} },
};
const rate = (fn) => {
for (let i = 0; i < 500; i++) fn(); // warm-up
let count = 0;
const start = process.hrtime.bigint();
while (process.hrtime.bigint() - start < 2_000_000_000n) {
fn();
count++;
}
return Math.round(count / 2);
};
console.log(`node ${process.version}, OpenSSL ${process.versions.openssl}, ${process.arch}`);
for (const [name, { keys, hash, opts }] of Object.entries(algs)) {
const signature = sign(hash, input, { key: keys.privateKey, ...opts });
const s = rate(() => sign(hash, input, { key: keys.privateKey, ...opts }));
const v = rate(() => verify(hash, input, { key: keys.publicKey, ...opts }, signature));
console.log(`${name.padEnd(8)} sign ${String(s).padStart(6)}/s verify ${String(v).padStart(6)}/s`);
}
Prise en charge par les bibliothèques, testée
Chaque bibliothèque a vérifié les trois mêmes jetons : un jeton ES256, un jeton Ed25519 étiqueté EdDSA et le jeton de la même clé étiqueté Ed25519. Chaque appel n’autorisait que l’algorithme propre au jeton.
| Bibliothèque | ES256 | EdDSA | Ed25519 |
|---|---|---|---|
| jose 6.2.13 (Node.js) | oui | oui | oui |
| jsonwebtoken 9.0.3 (Node.js) | oui | non : Unknown key type "ed25519". | non |
| PyJWT 2.15.1 | oui | oui | non : Algorithm not supported |
| joserfc 1.7.7 (Python) | oui | oui, avec l’avertissement EdDSA is deprecated via RFC 9864 | oui |
| golang-jwt v5.3.1 | oui | oui | non : signing method (alg) is unavailable |
| go-jose v4.1.6 | oui | oui | non : error in cryptographic primitive |
| jwx v3.3.0 (Go) | oui | oui | oui |
| Nimbus JOSE+JWT 10.10 (Java) | oui | oui, nécessite la dépendance facultative Tink | oui |
| JJWT 0.13.0 (Java, JDK 25) | oui | oui | non : Unsupported JWS header 'alg' (Algorithm) value 'Ed25519'. |
| Microsoft.IdentityModel.JsonWebTokens 8.23.0 (.NET 10) | oui | non : IDX10511: Signature validation failed. | non |
| firebase/php-jwt 7.2.1 (PHP 8.5, sodium) | oui | oui | non : Algorithm not supported |
| ruby-jwt 3.3.0 avec jwt-eddsa 1.0.0 | oui | oui ; sans l’extension, Key type OKP not supported | oui |
jsonwebtoken 11.1.0 (Rust, rust_crypto) | oui | oui | non : InvalidAlgorithmName |
ES256 a fonctionné dans 13 cas sur 13, EdDSA dans 11, Ed25519 dans 5. Les deux lacunes qui tranchent le plus souvent la question se trouvent dans les piles les plus courantes : le paquet npm jsonwebtoken ne prend pas du tout en charge EdDSA, et Microsoft.IdentityModel, sur lequel repose l’authentification JWT Bearer d’ASP.NET Core, renvoie IsSupportedAlgorithm: False pour une JWK Ed25519. L’échec côté .NET se traduit par le message générique IDX10511, qui pousse à chercher une mauvaise clé plutôt qu’un algorithme manquant. Sans Tink dans le classpath, Nimbus échoue à l’exécution avec ClassNotFoundException: com.google.crypto.tink.subtle.Ed25519Verify.
Dans les navigateurs, Ed25519 est arrivé dans la Web Cryptography API avec Firefox 129, Safari 17 et Chrome 137 (notes de version de Chrome 137, mai 2025), si bien que la signature dans le navigateur, par exemple celle des preuves DPoP, peut désormais utiliser l’une ou l’autre courbe.
Prise en charge par les KMS cloud et les HSM
Si la clé de signature réside dans un service de clés managé, c’est le service qui décide pour vous. D’après la documentation de chaque fournisseur en octobre 2026 :
| Service | P-256 (ES256) | Ed25519 |
|---|---|---|
| AWS KMS | ECC_NIST_P256, ECDSA_SHA_256 | ECC_NIST_EDWARDS25519, ED25519_SHA_512, depuis novembre 2025 |
| Google Cloud KMS | EC_SIGN_P256_SHA256 à tous les niveaux de protection, y compris HSM | EC_SIGN_ED25519, niveau de protection logiciel uniquement |
| Azure Key Vault et Managed HSM | P-256 (EC et EC-HSM) | non proposé |
| AWS CloudHSM | oui | hsm2m.medium en mode non FIPS uniquement |
Sources : référence des spécifications de clés AWS KMS, API Sign d’AWS KMS, algorithmes de Google Cloud KMS, clés Azure Key Vault, clés Managed HSM, versions du Client SDK AWS CloudHSM.
Deux détails pratiques :
- Encodage. AWS KMS et Google Cloud KMS renvoient des signatures ECDSA encodées en DER. Un JWS exige la forme brute
r ‖ sde 64 octets (RFC 7518 §3.4), donc un signataire ES256 reposant sur KMS doit effectuer la conversion. L’oublier est une cause classique d’erreurs de signature invalide, comme l’erreur ES256 signatures must be 64 bytes de jsonwebtoken ; l’article Formats de signature ECDSA explique la conversion, et le convertisseur DER vers brut la réalise pour une signature isolée. - Taille du message. Ed25519 dans AWS KMS exige
MessageType: RAW, etSignaccepte au plus 4096 octets. L’entrée de signature JWS se compose de l’en-tête et de la charge utile encodés, si bien qu’un jeton aux claims volumineux ne peut pas être signé avec Ed25519 dans KMS. ECDSA n’a pas cette limite, car vous pouvez envoyer un condensat SHA-256 avecMessageType: DIGEST.
Ce qu’exigent les normes et les fournisseurs
- OpenID Connect exige des fournisseurs qu’ils prennent en charge RS256 pour les jetons d’ID, sauf exception très limitée (OIDC Core §15.1). Aucune des deux courbes ne remplace cette exigence ; les fournisseurs qui proposent ES256 ou EdDSA le font en complément.
- FAPI 2.0 Security Profile (version finale, février 2025, §5.4.1) n’autorise que
PS256,ES256etEdDSAavec Ed25519 pour les JWT, et précise qu’un identifiant Ed25519 entièrement spécifié serait autorisé une fois enregistré, ce que la RFC 9864 a fait depuis. - La révision des bonnes pratiques JWT (draft-ietf-oauth-rfc8725bis, dans la file d’attente du RFC Editor) demande aux nouveaux déploiements de privilégier les noms entièrement spécifiés de la RFC 9864.
Aucune des deux courbes n’est post-quantique. ML-DSA dispose désormais d’identifiants JOSE (RFC 9964, mai 2026), mais pour des jetons dont la durée de vie se compte en minutes, la question est de savoir quand les vérificateurs devront rejeter les signatures classiques, et non si les jetons d’aujourd’hui pourront être lus plus tard. Garder les algorithmes configurables par clé, comme dans la section suivante, est ce qui rend cette bascule possible.
Choisir, et changer plus tard
| Situation | Choix |
|---|---|
Jetons vérifiés par des tiers, des services .NET ou le paquet npm jsonwebtoken | ES256 |
| Clé de signature dans Azure Key Vault, Managed HSM, Google Cloud HSM ou un HSM en mode FIPS | ES256 |
| Vous contrôlez chaque vérificateur, la clé est en logiciel ou dans AWS KMS | Ed25519 |
| Une API ou un profil n’admet que certains algorithmes (FAPI 2.0, la spécification d’un partenaire) | ceux de sa liste ; ES256 est celui qui y figure le plus souvent |
Si vous choisissez Ed25519 aujourd’hui, émettez "alg": "EdDSA". L’obsolescence prononcée par la RFC 9864 est une recommandation de niveau SHOULD, qui admet des obstacles opérationnels documentés, et un vérificateur qui rejette le nouveau nom en est un. Côté vérification, acceptez les deux noms, et uniquement avec une clé OKP dont le crv vaut Ed25519, afin qu’un nom obsolète ne sélectionne jamais une courbe que vous n’aviez pas prévue. Avec jose :
const { payload } = await jwtVerify(token, ed25519PublicKey, {
algorithms: ["Ed25519", "EdDSA"],
issuer: "https://id.example.com",
audience: "https://api.example.com",
});
Exécuté sur les jetons de cet article (jose 6.2.13), ce code a accepté le jeton EdDSA comme le jeton Ed25519, et rejeté le jeton ES256 avec "alg" (Algorithm) Header Parameter value not allowed. Changer d’algorithme plus tard revient à effectuer une rotation de clé : publiez la nouvelle clé avec ses propres kid et alg à côté de l’ancienne, commencez à signer avec elle, et retirez l’ancienne clé une fois le dernier ancien jeton expiré. L’article Rotation des clés JWKS sans interruption de service détaille le calendrier, et Erreurs de vérification des JWT explique comment épingler l’algorithme par clé pour que le jeton ne le choisisse jamais.
Sources
- RFC 7518 §3.4 : ECDSA dans JWS
- RFC 8032 : EdDSA, §5.1.6 (signature) et §5.1.7 (vérification,
S < L) - RFC 8037 : EdDSA et clés OKP dans JOSE
- RFC 9864 : algorithmes entièrement spécifiés pour JOSE et COSE
- Registre IANA des algorithmes JOSE et registre des algorithmes COSE
- RFC 6979 : ECDSA déterministe
- FIPS 186-5 : Digital Signature Standard, EdDSA compris
- FAPI 2.0 Security Profile, §5.4.1
- OpenID Connect Core 1.0, §15.1
- Documentation des fournisseurs, dont les liens figurent dans la section KMS