EncryptCodecencryptcodec
Blog/Cryptography
CryptographyMarch 13, 2026 · 6 min read

EC Keys and ECDSA: The Faster Alternative to RSA for JWT Signing

When you generate an RSA-2048 key pair, you get a private key that's roughly 1700 bytes. The equivalent security from an elliptic curve key — P-256 — is a private key of just 121 bytes. Same security level, 14× smaller. That size difference propagates into every JWT you sign, every TLS handshake, and every API call that carries a public key.

This post explains how EC keys work, which curve to pick, and how JWT ES256/ES384/ES512 compare to the RSA RS256/RS384/RS512 family.

What Makes Elliptic Curves Different

Classical RSA security relies on the difficulty of factoring large integers. To make factoring hard enough, you need very large numbers — 2048 bits is the current minimum recommendation, and 4096 bits for long-term security.

Elliptic curve cryptography (ECC) relies on a different hard problem: the elliptic curve discrete logarithm problem (ECDLP). It's computationally harder per bit than integer factorization, which is why you get equivalent security with much smaller keys.

RSA key sizeEquivalent EC curveSecurity bits
1024-bit— (too weak)80
2048-bitP-256128
3072-bitP-256128
7680-bitP-384192
15360-bitP-521256

P-256 gives you the same 128-bit security as a 3072-bit RSA key, at roughly 1/25th the size.

EC vs RSA — same security, dramatically smaller keys

128-bit
security — equivalent for both algorithms
P-256 (EC private key)32 bytes
P-256
3072-bit RSA (RSA private key)384 bytes
3072-bit RSA
12×
smaller key
~6× faster signing
vs equivalent RSA

The Three NIST Curves: P-256, P-384, P-521

The Web Crypto API (and most cryptographic libraries) support three NIST-standardized curves:

P-256 (secp256r1)

  • 128-bit security
  • JWT algorithm: ES256 (ECDSA with SHA-256)
  • The right default for almost every application
  • Supported everywhere: browsers, Node.js, Go, Rust, Java, Python

P-384 (secp384r1)

  • 192-bit security
  • JWT algorithm: ES384 (ECDSA with SHA-384)
  • Required in some US government and financial compliance contexts (CNSA Suite)
  • Slightly slower than P-256, but not noticeably so

P-521 (secp521r1)

  • 256-bit security (note: 521 bits, not 512)
  • JWT algorithm: ES512 (ECDSA with SHA-512)
  • Overkill for most applications; 521-bit math can be awkward on hardware without native 64-bit words
  • Use only if you have a specific long-term archival security requirement

For web APIs, microservices, and mobile authentication, P-256 is the correct choice.

EC Keys in JWT: ES256 vs RS256

Both ES256 and RS256 sign JWTs. The difference is in the algorithm used to produce the signature:

  • RS256: RSA with SHA-256, PKCS#1 v1.5 padding
  • ES256: ECDSA with SHA-256 on P-256

The signed JWT is smaller with ES256 because the signature itself is smaller. An RS256 signature with a 2048-bit RSA key is 256 bytes (Base64-encoded: ~344 chars). An ES256 signature on P-256 is 64 bytes (~86 chars). This matters in HTTP headers and cookies.

Key distribution

ECDSA signatures require both the signing key and the verification key. If you're distributing public keys via JWKS (a JSON Web Key Set endpoint), EC public keys in JWK format are significantly more compact than RSA public keys.

An RSA-2048 public key in JWK has n (256 bytes, Base64url-encoded: 342 chars) and e. An EC P-256 public key in JWK has x and y (32 bytes each, 43 chars each). Much more bandwidth-friendly at scale.

Code: Signing and Verifying with ES256

import { SignJWT, jwtVerify, importPKCS8, importSPKI } from "jose";

// privateKeyPem and publicKeyPem are PEM strings from your key store
const privateKey = await importPKCS8(privateKeyPem, "ES256");
const publicKey  = await importSPKI(publicKeyPem,  "ES256");

// Sign
const token = await new SignJWT({ sub: "user_123", role: "admin" })
.setProtectedHeader({ alg: "ES256" })
.setIssuedAt()
.setExpirationTime("1h")
.sign(privateKey);

// Verify
const { payload } = await jwtVerify(token, publicKey, {
algorithms: ["ES256"],
});
console.log(payload); // { sub: "user_123", role: "admin", iat: ..., exp: ... }

PEM vs JWK Format

When you generate an EC key pair, you'll typically need the keys in one of two formats:

PEM is the format used by OpenSSL, most TLS stacks, and server-side key storage:

-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg...
-----END PRIVATE KEY-----

JWK (JSON Web Key) is the format used by JOSE libraries, JWKS endpoints, and frontend key handling:

{
  "kty": "EC",
  "crv": "P-256",
  "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
  "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0",
  "d": "jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI"
}

The d field in JWK is the private scalar — keep it secret just like a PEM private key.

ECDSA Signatures Are Randomized — Understand the Implication

Unlike RSA-PKCS#1, ECDSA signatures require a random nonce (k) per signature. Signing the same message twice will produce different signatures. This is expected and correct.

The critical security requirement: the nonce must be cryptographically random and never reused. A reused nonce with the same private key leaks the private key — this is exactly how the PlayStation 3 private key was recovered in 2010. Implementations in the Web Crypto API and established libraries handle this correctly, but it's important to understand when evaluating custom cryptographic code.

Generating EC Key Pairs in the Browser

The Web Crypto API supports ECDSA natively:

// Generate a P-256 key pair using the Web Crypto API
const keyPair = await crypto.subtle.generateKey(
{ name: "ECDSA", namedCurve: "P-256" },
true,        // extractable — set false if key never leaves the device
["sign", "verify"]
);

// Export as PKCS8 / SPKI (PEM-ready)
const pkcs8 = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey);
const spki  = await crypto.subtle.exportKey("spki",  keyPair.publicKey);

// Export as JWK
const privateJwk = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
const publicJwk  = await crypto.subtle.exportKey("jwk", keyPair.publicKey);

// Sign a message
const message = new TextEncoder().encode("hello");
const signature = await crypto.subtle.sign(
{ name: "ECDSA", hash: "SHA-256" },
keyPair.privateKey,
message
);

// Verify
const valid = await crypto.subtle.verify(
{ name: "ECDSA", hash: "SHA-256" },
keyPair.publicKey,
signature,
message
);
console.log(valid); // true

You can generate P-256, P-384, or P-521 keys directly in your browser with the EC Key Generator — no installation required.

When to Use RSA Instead

EC keys are better in almost every dimension, but there are cases where RSA is still the right call:

  • Legacy system compatibility: some older Java and .NET versions have limited EC support
  • RSA-OAEP encryption: EC keys are for signing only (ECDSA) or key exchange (ECDH). If you need asymmetric encryption (encrypt a message with a public key, decrypt with private), you need RSA-OAEP — there's no ECDSA equivalent for encryption
  • HSM / smart card constraints: some hardware security modules have RSA-only firmware

If you're building something new with full library control, P-256 + ES256 is the better default over RSA-2048 + RS256.

  • P-256 gives 128-bit security at 1/25th the key size of equivalent RSA
  • ES256 (ECDSA + P-256 + SHA-256) is the recommended JWT signing algorithm for new systems
  • Use PEM for server-side key storage, JWK for JWKS endpoints and frontend distribution
  • The Web Crypto API supports P-256, P-384, and P-521 natively — no libraries needed
  • ECDSA is signing only; use RSA-OAEP if you need asymmetric encryption

→ Generate EC key pairs instantly with the EC Key Generator

Share this post

Frequently Asked Questions

Related posts