JWT ES256 (ECDSA P-256)
JWT ES256 (ECDSA with P-256 and SHA-256) gives RSA-grade security with 64-byte signatures. Learn key generation, sign/verify code, the ECDSA nonce risk, and why ES256 is the recommended new-system default.
ES256 is ECDSA with the P-256 curve and SHA-256. It provides ~128-bit security: the same as a 3072-bit RSA key: with a 256-bit key and a 64-byte signature, one quarter the size of an RS256 signature. For new distributed systems you control end-to-end, ES256 is the recommended default. Defined in RFC 7518 §3.4.
Key facts and signature size
| Property | ES256 |
|---|---|
| Type | Asymmetric (ECDSA, P-256 / secp256r1) |
| Key | 256-bit EC key on P-256 |
| Signature size | 64 bytes (r + s, raw) |
| Security | ~128-bit |
| Compatibility | Modern: supported by all current JWT libraries |
When to use
Use ES256 for new distributed systems you control end-to-end: smaller tokens than RS256, faster verification, same asymmetric key distribution model. ES256 is the recommended default for greenfield systems where you do not need to match an identity provider's RS256 default.
When not to use
ES256 has slightly less library coverage than RS256 in older or FIPS-restricted environments. If you integrate with third-party systems that default to RS256, interoperability may favor RS256. ES256 also carries the ECDSA nonce-reuse risk (mitigated by every modern library's CSPRNG); if that risk is unacceptable in your threat model, use EdDSA (deterministic signing) instead.
Code examples
Generate an EC key pair (P-256)
# OpenSSL
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out ec256-private.pem
openssl pkey -pubout -in ec256-private.pem -out ec256-public.pem
// Node.js
const { generateKeyPairSync } = require("crypto");
const { privateKey, publicKey } = generateKeyPairSync("ec", {
namedCurve: "P-256",
publicKeyEncoding: { type: "spki", format: "pem" },
privateKeyEncoding: { type: "pkcs8", format: "pem" },
});
Sign and verify (Node.js: jsonwebtoken)
const token = jwt.sign(
{ sub: "user_123" },
ecPrivateKey,
{ algorithm: "ES256", expiresIn: "15m", keyid: "ec-key-v1" }
);
const payload = jwt.verify(token, ecPublicKey, {
algorithms: ["ES256"],
issuer: "https://auth.example.com",
audience: "https://api.example.com",
});
Frequently asked questions
-
What is ES256 in JWT?
ES256 is ECDSA with the P-256 curve (secp256r1) and SHA-256. It is an asymmetric JWT signing algorithm: a private EC key signs, the matching public key verifies. ES256 provides ~128-bit security: equivalent to a 3072-bit RSA key: with a 256-bit key and a 64-byte signature, one quarter the size of an RS256 signature. For new distributed systems controlled end-to-end, ES256 is the recommended default.
-
Is ES256 better than RS256 for JWT?
For new applications you control end to end, yes: ES256 signatures are 64 bytes versus 256 bytes for RS256, making tokens smaller, and a 256-bit EC key gives roughly the same security as a 3072-bit RSA key. ES256 signing is also faster. The tradeoff is compatibility: RS256 has broader library support and is the default for most identity providers (Auth0, Okta, AWS Cognito). If you integrate with third-party systems that default to RS256, RS256 may be the safer choice for interoperability.
-
What is the ECDSA nonce vulnerability in ES256?
Every ECDSA signature requires a fresh random k-value (nonce). If the nonce is ever reused across two signatures, or weakly generated, an attacker can mathematically recover the private key from those two signatures. This is how the PS3 private key was extracted in 2010 (Sony used a constant k for every signature). Modern JWT libraries generate nonces correctly via CSPRNGs, so the risk is theoretical in practice: but it is the primary reason EdDSA (deterministic signing, no nonce) was designed.