Choosing Algorithms That Hold Up

Cryptographic Hygiene

Back to all posts

Posted on January 16, 2026

 This article is part of a series on understanding the hows and whys of JSON Web Signatures (JWS).

 There's accompanying code: it's refered to and linked throughout the content. But if you'd rather just read raw code, head over here.

You've implemented JWS signing and verification. Your code works in development. Tests pass. But then you see this in a code review:

  
  
    
{
  "alg": "HS256",
  "typ": "JWT"
}

    
  

Your teammate chose HS256 for your payments API. Is this fine? Or did they just introduce a catastrophic vulnerability that destroys non-repudiation?

This pattern repeats in security audits: authorization companies using RS256 with 1024-bit keys for "internal microservices" processing millions in daily transactions. These keys can be factored in hours with cloud compute. Teams believe they're "doing JWT security" by using signatures, not realizing their algorithm choice leaves the vault door wide open.

Algorithm choice isn't academic: it determines whether your signatures hold up under legal scrutiny, survive security audits, and protect against real-world attacks. Let's decode the naming conventions, understand the security properties, learn what attacks to defend against, and build a decision framework for choosing algorithms that actually work in production.

Decoding the Algorithm Names

First, a question: what does PS384 mean? If you can't answer that confidently, you're not alone. These algorithm names look like cryptographic alphabet soup, but thankfully they follow a systematic pattern.

The JOSE Family: Where These Names Come From

These algorithm identifiers come from the JOSE (JavaScript Object Signing and Encryption) family of specifications:

The algorithm names we're discussing live in RFC 7518 and are registered in the IANA JOSE registry. This registry is the authoritative source for alg values you can use in JWS headers.

The General Naming Pattern

Here's the key insight: most JWS algorithm names follow a consistent two-part pattern:

  
  
    
[Signature Algorithm Family][Hash Function]

    
  

Let's break down RS256 as an example:

  • RS: RSASSA-PKCS1-v1_5 (the signature algorithm family)
  • 256: SHA-256 (the hash function's output size in bits)

So RS256 means: "Use RSASSA-PKCS1-v1_5 signatures with SHA-256 as the hash function."

Similarly:

  • ES384 = ECDSA with P-384 curve using SHA-384
  • PS512 = RSASSA-PSS using SHA-512
  • HS256 = HMAC using SHA-256

The number almost always refers to the SHA-2 hash variant. The letters tell you which signature scheme is being used.

The Major Algorithm Family Prefixes

Prefix Full Name Type Example
HS HMAC with SHA-2 Symmetric HS256, HS384, HS512
RS RSASSA-PKCS1-v1_5 Asymmetric RS256, RS384, RS512
PS RSASSA-PSS Asymmetric PS256, PS384, PS512
ES ECDSA Asymmetric ES256, ES384, ES512
EdDSA EdDSA Asymmetric EdDSA

Notice that EdDSA breaks the pattern and doesn't have a number: you'll see why shortly.

The Hash Function Number

The number refers to the output size in bits of the SHA-2 hash function:

  • SHA-256: Produces a 256-bit (32-byte) hash
  • SHA-384: Produces a 384-bit (48-byte) hash
  • SHA-512: Produces a 512-bit (64-byte) hash

SHA-256 is basically fine for everything. You don't need SHA-512 unless you're defending against quantum computers with 256-bit security requirements: SHA-256 provides 128-bit security, which will remain sufficient for decades.

The exception: some compliance frameworks require specific hash sizes (e.g., FIPS 140-2 at higher security levels may require SHA-384 or SHA-512).

Never use SHA-1. It's cryptographically broken. Don't use it.

Algorithm Families – What Those Letters Mean

Now let's dive into what each prefix actually signifies and why you might choose one over another.

HS: HMAC (Symmetric Signing)

Full name: HMAC with SHA-2

HMAC (Hash-based Message Authentication Code) is a symmetric message authentication code. The same secret key is used for both signing and verification. The letters HS stand for "HMAC with SHA."

  
  
    
{
  "alg": "HS256",
  "typ": "JWT"
}

    
  

Strengths:

  • Provably secure: HMAC has a formal security proof
  • Extremely fast: 5-10 microseconds per signature
  • Simple implementation: harder to get wrong than asymmetric schemes
  • No random numbers needed: completely deterministic

Weaknesses:

  • Key distribution problem: both parties need the same secret
  • No non-repudiation: anyone who can verify can also sign
  • Key compromise results in total failure: if the key leaks, key holders can forge indefinitely

Known attacks:

  
  
    
case Plug.Crypto.secure_compare(conn.assigns[:token], token) do
  true -> :ok
  false -> :access_denied
end

    
  

When to use:

  • Internal microservices where you control both signer and verifier

When NOT to use:

  • Mobile app authentication: keys can be extracted, resulting in compromise
  • Third-party API integrations: key distribution nightmare
  • Scheme APIs: destroys non-repudiation

HMAC is fast and simple, but its symmetric nature limits applicability. Use only when you control both signer and verifier. For APIs requiring non-repudiation, never use HS256.

RS: RSASSA-PKCS1-v1.5 (RSA with Old Padding)

Full name: RSA Signature Scheme with Appendix - PKCS #1 v1.5

This is the original RSA signature algorithm, using the padding scheme from PKCS #1 version 1.5 (published in 1998). The RS stands for "RSA Signature."

  
  
    
{
  "alg": "RS256",
  "typ": "JWT"
}

    
  

How it works:

  1. Signing: signature = (Hash(message))^d mod N where d is private key, N is modulus
  2. Verification: Hash(message) == signature^e mod N where e is public exponent (usually 65537)

Strengths:

  • Universal support: every crypto library, every Hardware Security Module (HSM)
  • Asymmetric: private key signs, public key verifies freely
  • Deterministic: same message = same signature
  • No randomness required: simpler than schemes needing RNG (Random Number Generation)

Weaknesses:

  • Outdated padding scheme: PKCS1-v1.5 has known theoretical weaknesses
  • Implementation traps: Bleichenbacher's attack (see below) exploits incorrect verification
  • Large signatures: 256 bytes for 2048-bit keys, 384 bytes for 3072-bit keys
  • Slow signing: 2-10ms per signature depending on key size

Known attacks:

  • Bleichenbacher's signature forgery (2006): Many libraries incorrectly parsed the PKCS1-v1.5 padding, allowing attackers to forge signatures. This isn't a flaw in the algorithm itself, but the algorithm is easy to implement incorrectly. If you're verifying RS256 signatures from untrusted parties, you're trusting their libraries implement verification correctly.
  • Small key attacks:
    • 1024-bit RSA: Broken, don't use (factored with moderate resources)
    • 2048-bit RSA: Avoid: ~112-bit security, below 128-bit minimum
    • 3072-bit RSA: 128-bit security, minimum for new systems
    • 4096-bit RSA: 140-bit security, good long-term choice

When to use:

  • Legacy OAuth compatibility (Google, Microsoft only offer RS256)
  • High-compatibility requirements (lowest common denominator)
  • (Be careful) Internal APIs with migration plan

When NOT to use:

  • New high-security systems (use PS256 or ES256 instead)

RS256 works and is universally supported, but it's based on a 25-year-old padding scheme with known implementation traps. Use for compatibility, but prefer PS256 for new systems.

PS: RSASSA-PSS (RSA with Modern Padding)

Full name: RSA Signature Scheme with Probabilistic Signature Scheme

PSS is the modern, probabilistic version of RSA signatures. The PS stands for "Probabilistic Signature."

  
  
    
{
  "alg": "PS256",
  "typ": "JWT"
}

    
  

How it works:

  1. Generate salt: Random value unique to this signature
  2. Create padding: padding = MGF(Hash(concat(message, salt))) where MGF is Mask Generation Function (which basically "stretches" the short input to make it satisfy length requirements)
  3. Sign: Same RSA operation as PKCS1-v1.5, but with PSS padding

Strengths:

  • Security proof: formal security reduction to RSA problem
  • Simpler to implement correctly: fewer edge cases than PKCS1-v1.5
  • No known attacks: even theoretical ones
  • Same keys as PKCS1-v1.5: can use existing RSA key for both RS256 and PS256
  • Same signature size: no bandwidth penalty vs RS256

Weaknesses:

  • Slightly newer: not supported by some legacy systems (though adoption is widespread)
  • Still RSA: same performance characteristics as RS256 (slow signing, large signatures)
  • Requires randomness: need CSPRNG for salt generation

Known attacks: None of significance. PSS is considered secure when used with adequate key sizes (3072-bit minimum).

When to use:

  • Regulated financial services (Open Banking, PSD2 mandate PSS over PKCS1-v1.5)
  • Migrating from RS256 (same keys, better security)
  • High-compliance environments (auditors understand RSA)
  • New systems without constraints (ES256 is faster, but PS256 is fine)

PS256 is what RSA signatures should be. If you must use RSA (compliance, legacy, organizational reasons), use PSS, not PKCS1-v1.5.

ES: ECDSA (Elliptic Curve Signatures)

Full name: Elliptic Curve Digital Signature Algorithm

ECDSA uses elliptic curve cryptography. The ES stands for "Elliptic Curve Signature."

  
  
    
{
  "alg": "ES256",
  "typ": "JWT"
}

    
  

Important: With ECDSA, the number specifies both the hash function AND the elliptic curve:

  • ES256: P-256 (secp256r1) curve with SHA-256
  • ES384: P-384 (secp384r1) curve with SHA-384
  • ES512: P-521 (secp521r1) curve with SHA-512 (Note: P-521, not P-512)

How it works:

  1. Key generation: Random private key d, public key Q = d·G (G = generator point)
  2. Signing:
    • Generate random nonce k
    • Compute r = (k·G).x mod n
    • Compute s = k^(-1)(Hash(message) + r·d) mod n
    • Signature is (r, s)
  3. Verification: Mathematical check using public key

The critical detail: That random nonce k must be unique for every signature! Reuse once = private key recovery.

Strengths:

  • Small keys: 256-bit EC key ≈ 3072-bit RSA key in security
  • Small signatures: 64 bytes for ES256 vs 384 bytes for 3072-bit RSA
  • Fast verification: comparable to RSA, but signing is faster
  • Well-studied: analyzed extensively since 1990s
  • HSM support: excellent support across cloud KMS and hardware HSMs

Weaknesses:

  • Nonce management: random k must be unique for every signature
  • Implementation complexity: more moving parts than RSA
  • Probabilistic in a bad way: randomness is critical to security (unlike PSS)

Known attacks:

The Sony PlayStation 3 hack (2010): Sony used a static nonce for all PS3 signatures. Attackers recovered the private key from two signatures and could sign arbitrary code, enabling piracy and custom firmware.

Bitcoin wallet thefts (2013): Android's SecureRandom bug caused Bitcoin wallets to reuse nonces. Private keys were recovered and bitcoins stolen, affecting thousands of users.

The nonce reuse problem in detail:

Given two signatures (r, s1) and (r, s2) of messages m1 and m2 with the same nonce:

k = (Hash(m1) - Hash(m2)) / (s1 - s2) mod n
d = (s1·k - Hash(m1)) / r mod n

With just two signatures that reused a nonce, the private key is completely compromised. No brute force needed, just algebra.

When to use:

  • Modern API authentication
  • High-throughput systems (>10k signatures/second)
  • Mobile and IoT (smaller signatures = less bandwidth/battery)
  • Requirements: Use reputable libraries (OpenSSL, BoringSSL, libsodium)

When NOT to use:

  • Without CSPRNG (if you can't guarantee cryptographically secure RNG, use RSA-PSS or EdDSA)

ECDSA is the current standard: fast, small signatures, excellent HSM support. The nonce management risk is real but manageable with proper libraries. Don't implement it yourself, and you'll be fine.

ES256K: ECDSA with Koblitz Curves

A variant that trips people up: ES256K uses the secp256k1 Koblitz curve instead of NIST P-256.

  
  
    
{
  "alg": "ES256K",
  "typ": "JWT"
}

    
  

What's different: The NIST curves (P-256, P-384, P-521) were designed by the NSA, and some folks are uncomfortable with unexplained "random" parameters. Koblitz curves like secp256k1 don't have this issue.

Where you've seen it: Bitcoin, Ethereum, and FIDO2 WebAuthn use secp256k1.

The compatibility trap: Not all HSMs and crypto libraries support secp256k1. Production incidents commonly occur when teams "upgrade" from ES256 to ES256K without verifying HSM support. Code compiles successfully, but runtime signature verification fails; often discovered during deployment or first production use. This creates emergency rollbacks and incident response overhead.

When to use:

  • Blockchain interoperability
  • Concerns about using NIST curves

When NOT to use:

  • HSM required (verify support across all infrastructure first)
  • Maximum compatibility (ES256 remains safer default)

Organizational consideration: ES256K adoption requires buy-in from Security, Infrastructure, and any teams managing HSMs. Verify support across your entire stack before committing.

EdDSA: The One That Breaks the Pattern

Full name: Edwards-curve Digital Signature Algorithm

EdDSA is special. It doesn't have a number suffix, just EdDSA. The curve is specified in the key itself, not in the algorithm name.

  
  
    
{
  "alg": "EdDSA",
  "typ": "JWT"
}

    
  

Why different? The key specifies which curve via the crv parameter in the JWK:

  
  
    
{
  "kty": "OKP",
  "alg": "EdDSA",
  // Curve determines hash (Ed25519 uses SHA-512)
  "crv": "Ed25519",
  "x": "60mR98SQlHUSeLeIu7TeJBTLRG10qlcDLU4AJjQdqMQ"
}

    
  

The two curves:

  • Ed25519: 128-bit security, 32-byte keys, 64-byte signatures (most common)
  • Ed448: 224-bit security, 57-byte keys, 114-byte signatures (rarely needed)

How it works:

  1. Key generation: Private key is 32 random bytes for Ed25519
  2. Signing:
    • Deterministically derive nonce: k = Hash(private_key || message) (The magic)
    • Compute R = k·G
    • Compute S = k + Hash(R || public_key || message) · private_key
    • Signature is (R, S)
  3. Verification: Mathematical check

The critical difference: The nonce is derived deterministically from the private key and message, not randomly generated. Same message = same signature. No nonce reuse vulnerability.

Strengths:

  • Deterministic nonces: no random number generation during signing
  • No nonce reuse vulnerability: automatic uniqueness
  • Fastest: faster than ECDSA at signing and verification
  • Small signatures: 64 bytes for Ed25519
  • Side-channel resistant: designed from ground up for constant-time
  • Simple implementation: fewer parameters, fewer mistakes
  • Security proof: formal analysis with concrete security bounds

Weaknesses:

  • Newer spec: only standardized for JOSE in RFC 8037 (2017)
  • Limited HSM support (as of this writing)
  • Library support gaps: not all JWT libraries fully support EdDSA

Known attacks: None of significance. EdDSA was designed by Daniel J. Bernstein (djb), who learned from RSA's and ECDSA's mistakes.

When to use:

  • New systems with modern infrastructure
  • Performance-critical systems (millions of signatures/second)
  • Security-paranoid environments

When NOT to use:

  • HSM/KMS required (not available on major cloud platforms yet)
  • Maximum compatibility (ES256 is safer)

EdDSA is technically the best algorithm available. It fixes ECDSA's weaknesses while being faster. The only reasons not to use it are practical: HSM support and library compatibility. As these improve, EdDSA will likely become the standard.

Security Attacks You Must Defend Against

Understanding algorithms isn't enough. You need to defend against real-world attacks.

Attack 1: The "none" Algorithm Catastrophe

The none algorithm means "no signature." It exists for testing, but historically many libraries accepted it in production.

  
  
    
{
  "alg": "none",
  "typ": "JWT"
}

    
  

The attack:

  1. Attacker intercepts a valid JWT signed with RS256
  2. Decodes the payload and modifies critical fields (e.g., changes the merchant_id to redirect funds)
  3. Changes header from "alg": "RS256" to "alg": "none"
  4. Removes the signature
  5. Vulnerable server accepts the unsigned, modified token as valid

Defense:

  
  
    
# DO THIS
defmodule AuthorizationVerifier do
  # Explicit whitelist, never includes 'none'
  @allowed_algs ["ES256"]

  def verify_token(jws, public_key) do
    case JOSE.JWS.verify_strict(public_key, @allowed_algs, jws) do
      {true, payload, _jws} -> {:ok, payload}
      {false, _, _} -> {:error, :invalid_signature}
    end
  end
end

    
  

See it in action in the accompanying code and related test.

In summary:

  • Explicitly specify allowed algorithms
  • Never include "none" in your allowed list
  • Test with a token that has "alg": "none" to ensure rejection

Attack 2: Algorithm Substitution (RS256 → HS256)

This is the attack that has compromised major platforms:

  
  
    
# VULNERABLE, don't do this!
defmodule VulnerableVerifier do
  def verify_token(jws_string, key) do
    # Decode without verification to read header
    {_payload, jws} = JOSE.JWS.peek_payload(jws_string)
    %{"alg" => alg} =
      JOSE.JWS.peek_protected(jws_string)
      |> elem(0)

    # Attacker controls the algorithm,
    # since it was read from a client-submitted value!
    JOSE.JWS.verify_strict(key, [alg], jws_string)
  end
end

    
  

The attack:

  1. Attacker obtains a public key (which is, well, public)
  2. Changes "alg": "RS256" to "alg": "HS256" in token header
  3. Creates HMAC signature using the server's public key as the HMAC secret
  4. Verification succeeds because the library uses the public key as HMAC key (instead of the server's private key)

Defense:

  
  
    
# DO THIS
defmodule AuthorizationVerifier do
  # Never trust the alg field from the token
  # Explicit whitelist
  @allowed_algs ["ES256"]

  def verify_token(jws, public_key) do
    case JOSE.JWS.verify_strict(public_key, @allowed_algs, jws) do
      {true, payload, _jws} -> {:ok, payload}
      {false, _, _} -> {:error, :invalid_signature}
    end
  end
end

    
  

See how the accompanying code does verification and tests it.

Attack 3: Weak Keys

1024-bit RSA: Broken. Can be factored in hours with cloud compute.

2048-bit RSA: Provides only ~112-bit security, below 128-bit minimum.

Defense: Use 3072-bit RSA minimum for new deployments, or switch to ES256.

The Decision Framework

How do you choose a suitable algorithm? Here's a decision framework balancing technical merit with organizational reality:

Of course, your specific organizational context may place additional constraints on your algorithm choice:

  • Need RSA for compliance (e.g., security team prefers RSA familiarity over ECDSA unknowns)? Use PS256 (but never RS256)

  • Need blockchain compatibility? Use ES256K (verify HSM support first!), but bear in mind it requires infrastructure AND security team buy-in

  • Maximum compatibility? Use ES256 (or RS256 if supporting legacy systems with no other options). Make sure to consider partner capabilities and upgrade cycles

  • Need the highest security? Algorithms rank EdDSA > ES256 > PS256, you'll need to balance against operational complexity and team expertise

Algorithm Comparison Matrix

Here's everything side-by-side:

Algorithm Security Performance Signature Size HSM Support Nonce Risk Best For
HS256 High Excellent 32 bytes Excellent None Internal services
RS256 Good Moderate 256+ bytes Excellent None Legacy compatibility
PS256 Excellent Moderate 256+ bytes Excellent Low Modern RSA
ES256 Excellent Good 64 bytes Excellent Medium Default choice
ES256K Excellent Good 64 bytes Moderate Medium Blockchain
EdDSA Excellent Excellent 64 bytes Limited None Best security

Implementation Checklist

Your security checklist before going live:

For all algorithms:

  • Explicitly whitelist allowed algorithms in verification
  • Reject "alg": "none" tokens
  • Use constant-time comparison for signature verification
  • Log verification failures with details
  • Monitor for spikes in verification failures

For HMAC (HS256/384/512):

  • Key is at least 256 bits of true randomness
  • Key stored in secrets manager, not in code
  • Different keys per environment
  • Constant-time comparison used

For RSA (RS/PS):

  • Key size is 3072 bits minimum
  • Private key stored in HSM/KMS
  • Library actively maintained and patched
  • Test with known-bad signatures

For ECDSA (ES256/384/512):

  • Using reputable crypto library (OpenSSL, BoringSSL)
  • Library uses cryptographically secure RNG
  • Verify signatures are different for same message
  • Test nonce uniqueness

For EdDSA:

  • Library fully implements RFC 8037
  • Test with RFC test vectors
  • Verify deterministic signatures

Wrapping Up

Algorithm choice matters. Not just for performance or signature size, but for security. Here's what we've learned:

  1. Algorithm names follow patterns: [Family][Hash] encodes critical information
  2. HS256 is fast but symmetric - destroys non-repudiation for third parties
  3. RS256 is universal but outdated - use only for legacy compatibility
  4. PS256 is what RSA should be - use if you need RSA for compliance
  5. ES256 is the current standard - best balance of security, performance, support
  6. EdDSA is technically superior - use when HSM support isn't required

Most often, ES256 will prove a suitable choice.

If you control your infrastructure and don't need HSM support, EdDSA offers technical advantages.

If you must use RSA (compliance, legacy, or organizational constraints), use PS256, never RS256.

If someone suggests HS256 for your multi-party scheme API, you now understand why that destroys non-repudiation and what to do instead.

From technical choice to organizational reality: The "best" algorithm on paper must survive your security review process, work with your existing HSM infrastructure, gain approval from your compliance team, and integrate with issuers who may have their own constraints.

You now understand how to choose and implement the right cryptographic algorithm for JWS signatures. But there's a critical architectural question we haven't addressed: if signature verification is expensive (1-5ms per request), how do you prevent attackers from weaponizing it? The answer requires separating authentication from non-repudiation—a fundamental principle that shapes how production financial APIs are built.

 This article is part of a series on understanding the hows and whys of JSON Web Signatures (JWS).

 There's accompanying code: it's refered to and linked throughout the content. But if you'd rather just read raw code, head over here.