By AndyPublished
ES256 Explained: ECDSA with P-256 for JWT Signing
What ES256 Means
The algorithm identifier ES256 appears in a JWT's header as"alg":"ES256". It decomposes into three parts that the JOSE specification fixes simultaneously:
- ·E: ECDSA (Elliptic Curve Digital Signature Algorithm), defined in FIPS 186-4.
- ·S: Signature (as opposed to encryption, JWE uses different identifiers).
- ·256: SHA-256 hash function AND the P-256 curve (also called secp256r1 or prime256v1). The number pins both.
That last point trips people up. The "256" in ES256 is NOT a free choice of hash length over a curve — ES256 is exactly P-256 + SHA-256. ES384 is exactly P-384 + SHA-384. ES512 is P-521 (not 512) + SHA-512. RFC 7518 §3.4 specifies these triples explicitly; an implementation that pairs P-256 with SHA-384 isn't ES256, it's non-compliant.
The Signature Format That Catches Everyone Out
ECDSA signatures are mathematically pairs of integers, conventionally called (r, s). For ES256 each integer is 256 bits, 32 bytes. There are two ways the world encodes this pair:
- ·ASN.1 DER encoding: what OpenSSL, Java, and most general-purpose crypto libraries produce by default. Variable-length (typically 70-72 bytes), wrapped in an ASN.1 SEQUENCE.
- ·Raw IEEE P1363 / R || S concatenation: what JWT mandates. Fixed-length 64 bytes for ES256 (32 bytes r, 32 bytes s, big-endian, padded with leading zeros if needed). No ASN.1 wrapper.
The browser's Web Crypto API (which jwtdecode.app uses) produces and accepts the raw P1363 format by default for ECDSA algorithms, so the JWT spec aligns with the platform. Node.js's crypto.sign() defaults to DER and needs thedsaEncoding: 'ieee-p1363' option (Node 13+) to produce JWT-compliant output.
Key Format
ES256 uses an EC key pair on the P-256 curve. In PEM/SPKI form, a P-256 public key is ~178 bytes encoded, substantially smaller than a 2048-bit RSA SPKI public key (~294 bytes) and dramatically smaller than 4096-bit RSA (~550 bytes). The private key in PKCS#8 form is ~138 bytes.
-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE... -----END PUBLIC KEY-----
In JWK form, an ES256 public key has four fields: kty:"EC",crv:"P-256", plus the affine point coordinates xand y as base64url-encoded 32-byte values. The private key addsd, the scalar private key, also 32 bytes base64url-encoded.
Why Prefer ES256 over RS256
Smaller signatures and keys
An ES256 signature is 64 bytes raw, ~86 characters in base64url. An RS256 signature with a 2048-bit key is 256 bytes, ~342 characters base64url. For high-throughput services issuing many JWTs, ES256 saves real bandwidth, a JWT bearer header on every API call multiplies that delta.
Faster verification
ECDSA verification with P-256 is typically 2-5× faster than RSA-2048 verification on modern CPUs with hardware curve support. Signing is the opposite, RSA signing is faster than ECDSA signing — but for the issuer/verifier asymmetry of OAuth/OIDC where one IdP signs and many resource servers verify, the ES256 verification speedup is the path that matters.
Equivalent security at smaller parameters
NIST estimates P-256 provides ~128 bits of classical security, roughly equivalent to RSA-3072. A P-256 key is 8-12× smaller than an RSA key at equivalent strength. For embedded clients, mobile tokens, and constrained-bandwidth scenarios, the trade is decisive.
When ES256 Is Not the Right Choice
- ·Legacy HSM constraints: older HSMs may not support ECDSA, or charge extra for elliptic-curve operations. Check before designing around it.
- ·FIPS 140-2 compliance edge cases: some compliance regimes still favour RSA for institutional reasons unrelated to the underlying math. Confirm with your auditor.
- ·Java 8 without BouncyCastle: out-of-the-box P-256 support exists, but ES256 JWT libraries on Java 8 historically had P1363/DER conversion bugs. Java 11+ is fine.
- ·Nonce-reuse risk in your signing code: ECDSA requires a per-signature nonce. Reusing it (or using a weak RNG) leaks the private key entirely. RFC 6979 deterministic ECDSA eliminates this risk; check your library uses it. RSA-PSS has the same nonce concern but is more forgiving in failure modes.
ES256 vs PS256
Both are modern asymmetric JWT algorithms. The honest comparison: ES256 keys are smaller; PS256 interoperates with RSA-only infrastructure. If you're already paying the cost of RSA key infrastructure (HSMs, key rotation tooling, JWKS endpoints), PS256 is a solid choice over RS256 because PSS padding eliminates the malleability concerns of PKCS#1 v1.5. If you're greenfield, ES256 is generally the better default. See PS256 vs RS256 for the deeper RSA-side comparison.
Verifying an ES256 Token
Verification needs the issuer's public key (in PEM, JWK, or JWKS form), the algorithm identifier from the token header (confirmed to be ES256, never trust the header without checking against an allow-list), and the standard JWT verification steps: split on dots, base64url-decode the signature to 64 raw bytes, recompute the signing input as base64url(header) + "." + base64url(payload), SHA-256 the signing input, and verify the signature with the public key.
jwtdecode.app does this entirely in the browser via SubtleCrypto.verify("ECDSA"). Paste a token and a public key in either PEM or JWK form into the JWT decoder, verification runs locally, the token never leaves your browser.
Summary
ES256 is the recommended asymmetric JWT algorithm for new systems. It produces a 64-byte raw signature (not DER), uses P-256 curve and SHA-256 strictly per RFC 7518, and offers smaller keys, faster verification, and equivalent security to RSA-3072. The two pitfalls to watch: signature encoding (raw vs DER) and nonce generation in your signing library.