Skip to content
ecdsa.com

ES256 vs EdDSA (Ed25519) for JWTs

Both produce 64-byte signatures and near-identical tokens. The decision is about which libraries, key stores and consumers support each, and about the new Ed25519 alg name.

ES256 vs EdDSA for JWTs is a narrower question than ECDSA vs EdDSA in general. Token sizes are the same, both are fast, and both are recommended over RS256 for new issuers. What differs is support: ES256 is required or expected by many platforms and key stores, while Ed25519 is simpler to implement correctly and is now named Ed25519 rather than EdDSA in JOSE.

The ES256 vs RS256 guide covers the RSA side of the same decision; this page compares the two elliptic-curve options.

At a glance

ES256EdDSA / Ed25519
JOSE algES256 (RFC 7518 §3.4)Ed25519 (RFC 9864); older EdDSA (RFC 8037), now deprecated
Curve and hashP-256 with SHA-256Edwards25519 with SHA-512
JWKkty: EC, crv: P-256, x, ykty: OKP, crv: Ed25519, x
Public JWK (measured, compact JSON)126 characters79 characters
Signature in the token64 bytes raw r‖s, 86 characters64 bytes, 86 characters
Example token (measured below)232 characters234 characters (longer alg value)
DeterministicNo (random nonce in most libraries)Yes
JWA requirement levelRecommended+Optional
COSE id−7 (or −9 ESP256 in RFC 9864)−19 (older −8)
Cloud KMSAWS, Google, Azure: yesAWS (since November 2025) and Google: yes; Azure: no
Browser WebCryptoAll browsersFirefox 129, Safari 17, Chrome 137 and later

Size and speed

Both signatures are 64 bytes, so tokens are the same length except for the header. The script below, run with Node.js 26.10, signs the same claims both ways with node:crypto only (real code should use a JOSE library).

Node.js: ES256 and Ed25519 JWTs over the same payload
import { generateKeyPairSync, sign } from "node:crypto";

const b64url = (value) => Buffer.from(value).toString("base64url");
const payload = b64url(JSON.stringify({ iss: "https://auth.example.com", sub: "user-42", aud: "api", exp: 1792500000 }));

const es = generateKeyPairSync("ec", { namedCurve: "P-256" });
const esInput = `${b64url(JSON.stringify({ alg: "ES256", kid: "es-1" }))}.${payload}`;
const es256 = `${esInput}.${b64url(sign("sha256", Buffer.from(esInput), { key: es.privateKey, dsaEncoding: "ieee-p1363" }))}`;

const ed = generateKeyPairSync("ed25519");
const edInput = `${b64url(JSON.stringify({ alg: "Ed25519", kid: "ed-1" }))}.${payload}`;
const ed25519 = `${edInput}.${b64url(sign(null, Buffer.from(edInput), ed.privateKey))}`;

console.log("ES256   token", es256.length, "chars, public JWK", JSON.stringify(es.publicKey.export({ format: "jwk" })).length, "chars");
console.log("Ed25519 token", ed25519.length, "chars, public JWK", JSON.stringify(ed.publicKey.export({ format: "jwk" })).length, "chars");
Output
ES256   token 232 chars, public JWK 126 chars
Ed25519 token 234 chars, public JWK 79 chars

Speed is not a deciding factor: on one Apple M4 Max core, OpenSSL 3.6.5 makes about 87,000 P-256 and 60,000 Ed25519 signatures per second. A JWKS with Ed25519 keys is smaller because OKP keys have one coordinate.

Encoding and correctness

ES256 signatures in JWS must be the raw 64-byte r‖s form (RFC 7518 §3.4). Many ECDSA APIs default to DER: OpenSSL, Java's SHA256withECDSA, Go's ecdsa.SignASN1, Node's crypto.sign() without dsaEncoding, and the sign APIs of AWS KMS and Google Cloud KMS. Hand-rolled ES256 code that forgets the conversion produces 70–72-byte signatures that conforming verifiers reject, and the opposite mistake breaks verification of correct tokens. ECDSA verifiers must also reject r or s equal to zero; Java 15 to 18 did not (CVE-2022-21449), letting anyone forge ES256 tokens until patched.

Ed25519 has a single 64-byte encoding, deterministic signing and no nonce to get wrong. Its traps are elsewhere: some libraries treat the private key as a 32-byte seed and others as 64 bytes, and older JOSE libraries only recognize "alg": "EdDSA".

The alg name changed

RFC 8037 (2017) registered EdDSA as a polymorphic JOSE algorithm whose curve is determined by the key (crv: Ed25519 or Ed448). RFC 9864 (October 2025) deprecates polymorphic identifiers and registers Ed25519 and Ed448, so the alg alone determines the operation, as ES256 always did. Support is uneven in 2026: jose 6 accepts both EdDSA and Ed25519, joserfc and Nimbus JOSE+JWT support the new names, while PyJWT 2.15 and golang-jwt v5 still accept only EdDSA. For tokens consumed by many parties, check each consumer before switching the header value; verifiers can accept both names for the same key during a transition.

Platform and key-store support

ES256 is the lowest common denominator. It is the usual choice for DPoP proofs (RFC 9449), one of three algorithms FAPI 2.0 permits (PS256, ES256, or EdDSA with Ed25519), required for Apple's App Store Server API and APNs provider tokens, supported by every OpenID Connect provider and the default COSE algorithm in WebAuthn. Every HSM, TPM, Secure Enclave and cloud KMS offers P-256 keys.

Ed25519 has caught up in software: Node.js, Go, Java 15 and later, .NET through libraries, Python's cryptography, browser WebCrypto, and the main JOSE libraries. In key stores it is newer: AWS KMS added Ed25519 signing keys in November 2025, Google Cloud KMS offers EC_SIGN_ED25519, while some HSM models and Azure Key Vault do not. If tokens must be signed inside a managed key store, check its algorithm list first.

When to choose which

  • Public identity provider, many relying parties: ES256 (with RS256 kept for old consumers if needed).
  • Keys in HSM or KMS across several clouds: ES256 for uniform support.
  • Internal services, both ends in your control, modern libraries: Ed25519 with alg: Ed25519 if all verifiers support it, otherwise EdDSA.
  • Sender-constrained tokens (DPoP) from browsers and mobile apps: ES256, which every platform keystore can generate non-exportably.
  • Profiles that name algorithms (FAPI, government, financial): whatever the profile lists.

Migration notes

  1. Add the new key to the JWKS with its own kid and an explicit alg, and give consumers time to refresh caches.
  2. Configure verifiers per key: each key accepts exactly one algorithm (and, for Ed25519, both EdDSA and Ed25519 header values if your consumers differ). Never accept the token's alg without that binding.
  3. Switch the issuer to the new key; existing tokens keep verifying with the old one until they expire.
  4. Remove the old key after the longest token lifetime.
  5. Test encodings end to end: an ES256 signature from a KMS must be converted from DER to raw before it goes into a token; the DER and raw converter and the JWT debugger show what a token contains.

References

All comparisons