How JWT Signatures Work: HMAC, RSA, and ECDSA

The signature is the most important part of a JSON Web Token. Without it, a JWT is just a Base64url-encoded JSON object that anyone can read and modify. The signature provides two critical guarantees: integrity (the token has not been tampered with) and authenticity (the token was created by a trusted party). This guide explains the cryptographic algorithms behind JWT signatures, how each one works, and how to choose the right algorithm for your application.

Why Signatures Matter

A JWT payload is not encrypted — it is simply Base64url-encoded and can be decoded by anyone. The signature is what prevents tampering. When a server receives a JWT, it recomputes the signature using the token's header and payload, then compares the result to the signature in the token. If they match, the server knows that the token has not been modified since it was signed and that it was created by someone who possesses the signing key.

Signing:    signature = SIGN(base64url(header) + "." + base64url(payload), key)
Verifying:  expected  = SIGN(base64url(header) + "." + base64url(payload), key)
            valid     = (expected === token.signature)

If an attacker modifies a claim in the payload — for example, changing "role": "user" to "role": "admin" — the signature will no longer match, and the server will reject the token.

HMAC: HS256, HS384, HS512

HMAC (Hash-based Message Authentication Code) algorithms use a shared secret — the same key is used for both signing and verification. This makes HMAC a symmetric algorithm.

How HMAC Works

HMAC combines a cryptographic hash function (SHA-256, SHA-384, or SHA-512) with a secret key. The process works in two passes:

  • The secret key is padded and XORed with an inner padding constant, then concatenated with the message and hashed.
  • The result is XORed with an outer padding constant and hashed again.
HMAC(key, message) = HASH((key XOR opad) || HASH((key XOR ipad) || message))

// For HS256:
signature = HMAC-SHA256(secret, base64url(header) + "." + base64url(payload))

The double-hash construction ensures that even if vulnerabilities are found in the underlying hash function, the HMAC construction remains secure. The number in the algorithm name refers to the SHA variant: HS256 uses SHA-256 (producing a 256-bit output), HS384 uses SHA-384, and HS512 uses SHA-512.

Key requirement: The secret must be at least as long as the hash output (32 bytes for HS256, 48 for HS384, 64 for HS512). Use a cryptographically random secret, not a human-readable password.

Use case: HMAC is ideal when the same server or trusted system both signs and verifies tokens. It is the simplest and fastest algorithm family.

RSA: RS256, RS384, RS512

RSA algorithms use asymmetric cryptography with a public/private key pair. The private key signs tokens, and the public key verifies them. This separation is the critical advantage of RSA over HMAC: you can distribute the public key to any service that needs to verify tokens without giving them the ability to create new ones.

How RSA Signing Works

RSA signing with PKCS#1 v1.5 (used by RS256/384/512) follows these steps:

  • Hash the signing input (base64url(header).base64url(payload)) using the corresponding SHA algorithm.
  • Pad the hash using the PKCS#1 v1.5 padding scheme to match the RSA key size.
  • Apply the RSA private key operation (modular exponentiation) to produce the signature.
// Signing (private key)
hash      = SHA-256(base64url(header) + "." + base64url(payload))
padded    = PKCS1_v1.5_PAD(hash)
signature = padded ^ d (mod n)    // RSA private key operation

// Verification (public key)
decrypted = signature ^ e (mod n) // RSA public key operation
expected  = PKCS1_v1.5_PAD(SHA-256(base64url(header) + "." + base64url(payload)))
valid     = (decrypted === expected)

Key sizes: Use at least 2048-bit RSA keys. For long-term security, 4096-bit keys are recommended, though they are slower. RSA signatures are significantly larger than HMAC outputs — a 2048-bit key produces a 256-byte signature.

ECDSA: ES256, ES384, ES512

Elliptic Curve Digital Signature Algorithm (ECDSA) provides the same asymmetric signing capability as RSA but with much smaller keys for equivalent security. A 256-bit ECDSA key provides roughly the same security as a 3072-bit RSA key.

Key Size Advantages

Algorithm  | Curve  | Key Size  | Equivalent RSA | Signature Size
-----------|--------|-----------|----------------|---------------
ES256      | P-256  | 256 bits  | ~3072 bits     | 64 bytes
ES384      | P-384  | 384 bits  | ~7680 bits     | 96 bytes
ES512      | P-521  | 521 bits  | ~15360 bits    | 132 bytes

ECDSA signing involves generating a random nonce for each signature, computing a point on the elliptic curve, and using the private key to produce two values (r, s) that form the signature. The random nonce is critical — if the same nonce is ever reused with the same key, the private key can be recovered. Modern implementations use deterministic nonce generation (RFC 6979) to eliminate this risk.

Use case: ECDSA is preferred when bandwidth or storage is a concern (mobile applications, IoT) or when you need asymmetric signing with smaller tokens than RSA produces.

EdDSA: Ed25519

EdDSA (Edwards-curve Digital Signature Algorithm) using Curve25519 is the newest signing algorithm supported by the JWT ecosystem. It offers several advantages over both RSA and ECDSA:

  • Fast: Ed25519 is significantly faster than RSA for both signing and verification, and faster than ECDSA for verification.
  • Deterministic: Unlike ECDSA, Ed25519 does not require a random nonce, eliminating an entire class of implementation vulnerabilities.
  • Compact: 256-bit keys and 64-byte signatures with approximately 128 bits of security.
  • Resistant to side-channel attacks: The algorithm was designed from the ground up to be implementable in constant time.
{
  "alg": "EdDSA",
  "typ": "JWT",
  "crv": "Ed25519"
}

Ed25519 is the recommended choice for new systems that need asymmetric signatures, assuming all your infrastructure supports it. Library support has become widespread, with the JOSE ecosystem (used by this tool) providing full EdDSA support.

PSS Variants: PS256, PS384, PS512

RSA-PSS (Probabilistic Signature Scheme) is a more modern RSA padding scheme than PKCS#1 v1.5. It has a formal security proof and is resistant to certain theoretical attacks against the older padding scheme. PS256, PS384, and PS512 use the same RSA key pairs as their RS counterparts but apply PSS padding instead. If you are already using RSA and can update your infrastructure, PSS is the preferred variant.

Algorithm Selection Guide

Choosing the right algorithm depends on your architecture and requirements:

Symmetric vs Asymmetric

If the same system signs and verifies tokens, use HMAC (HS256) for simplicity and speed. If different systems need to verify tokens without the ability to create them, use an asymmetric algorithm (RS256, ES256, or EdDSA). Asymmetric algorithms are essential for microservices where an authentication service signs tokens and multiple downstream services verify them.

Performance

Operation     | HS256  | RS256     | ES256    | EdDSA
--------------|--------|-----------|----------|----------
Sign          | Fast   | Slow      | Moderate | Fast
Verify        | Fast   | Moderate  | Moderate | Fast
Key gen       | N/A    | Very slow | Fast     | Fast
Signature size| 32 B   | 256 B     | 64 B     | 64 B

For high-throughput systems, HMAC and EdDSA offer the best performance. RSA verification is faster than RSA signing, which is beneficial when tokens are signed infrequently but verified on every request.

Key Distribution

HMAC requires secure distribution of the shared secret to every service that needs to verify tokens. With asymmetric algorithms, you only distribute the public key, which can be shared openly without security concerns.

JWK and JWKS Endpoints

JSON Web Key (JWK) is a standard format (RFC 7517) for representing cryptographic keys as JSON objects. A JWK Set (JWKS) is a JSON document containing an array of JWKs, typically served at a well-known URL:

// JWKS endpoint: https://auth.example.com/.well-known/jwks.json
{
  "keys": [
    {
      "kty": "RSA",
      "kid": "key-2024-01",
      "use": "sig",
      "alg": "RS256",
      "n": "0vx7agoebGcQSuu...",
      "e": "AQAB"
    },
    {
      "kty": "EC",
      "kid": "key-2024-02",
      "use": "sig",
      "alg": "ES256",
      "crv": "P-256",
      "x": "f83OJ3D2xF1Bg8vub...",
      "y": "x_FEzRu9m36HLN_tue..."
    }
  ]
}

Services that need to verify JWTs fetch the JWKS document from the authentication server and use the kid (Key ID) in the JWT header to select the correct key from the set. This enables seamless key rotation — new keys can be added to the JWKS before they are used, and old keys can be removed after all tokens signed with them have expired. Services should cache the JWKS document and refresh it periodically or when they encounter a kid they do not recognize.

Understanding how JWT signatures work is essential for making informed decisions about your authentication architecture. The algorithm you choose affects security, performance, key management complexity, and token size — all of which have real implications in production systems.

Related Guides