By AndyPublished
PS256 vs RS256: RSA-PSS or RSA-PKCS1 for JWT Signing?
Same Key, Different Padding
A practical truth that surprises people: PS256 and RS256 can use the same RSA key pair. The algorithm identifier in the JWT header ("alg":"PS256" vs"alg":"RS256") selects the padding scheme applied during signing and verification, the underlying key material is identical. This is why most JWKS endpoints can serve one key that supports either, controlled by the alg field on the JWK.
The difference is what happens between "I have a SHA-256 hash of the signing input" and "I have an RSA signature". RS256 wraps the hash in PKCS#1 v1.5 deterministic padding before the RSA private-key operation. PS256 generates a random salt, runs the hash and salt through MGF1 (a mask generation function), and produces a randomised padding block, same RSA operation, different (and probably better) input.
Why PKCS#1 v1.5 Is Suspect
PKCS#1 v1.5 isn't "broken" for digital signatures in the sense that you can directly forge a signature without the private key. But it has a 30-year history of edge-case attacks:
- ·Bleichenbacher's e=3 attack (2006): exploits implementations that don't fully verify the padding structure on signature verification. The classic "RSA signature forgery via a low exponent" attack.
- ·ROBOT (2017): a Bleichenbacher-style timing oracle resurrection that affected major TLS implementations 19 years after the original was published.
- ·Marvin attack (2023): yet another timing variant, still finding implementations that hadn't constant-timed padding parsing.
None of these directly forge a JWT signature, and a correctly-implemented RS256 verifier is safe against the known attacks. The pattern matters, though: PKCS#1 v1.5 keeps yielding new variants of old attacks because its deterministic structure leaves room for implementation mistakes. PSS is mathematically harder to misimplement in an exploitable way.
What PSS Buys You
- ·Provable security: PSS has a tight security reduction to the RSA problem under standard assumptions. PKCS#1 v1.5 famously does not.
- ·Randomised signatures: signing the same JWT twice produces two different signatures. RS256 produces an identical signature each time.
- ·No "low exponent" concern: the salt and MGF1 step eliminate the attack surface that low-exponent PKCS#1 v1.5 has been bitten by.
iat/ jti claims that already make repeated signings produce different tokens. The benefit is purely cryptographic.Interoperability: Where PS256 Wins, Where RS256 Wins
RS256 wins on legacy reach
Every JWT library ever shipped supports RS256. Old enterprise IAM products (older ADFS, legacy Java SSO frameworks, embedded device firmware) often only support RS256. If you're integrating with any unknown identity ecosystem, RS256 is the highest-confidence default.
PS256 wins on modern stacks
Every modern library, node-jose, jose-jwt, python-jose, golang-jwt, .NET Microsoft.IdentityModel, PyJWT, Auth0's auth0/node-jsonwebtoken (v9+), supports PS256 first-class. OpenID Connect's self-issued OPs, FAPI 2.0 profiles, and most fintech compliance regimes mandate PS256 (or PS384/PS512) and forbid RS256 in new deployments.
Signature and Key Sizes
Both produce identical-size signatures for a given RSA key size: 256 bytes for 2048-bit RSA, 512 bytes for 4096-bit. The base64url-encoded signature in the JWT is ~342 characters (2048-bit) regardless of which padding is used. No bandwidth tradeoff between the two, only ES256/EdDSA save bytes.
Migrating from RS256 to PS256
Because the key pair is shared, the migration is a "header swap" not a "key rotation":
- ·Update the issuer to sign tokens with
alg: PS256using the existing private key. - ·Update every verifier's allow-list to accept BOTH RS256 and PS256 during the transition window.
- ·After all in-flight tokens expire (one access-token lifetime), remove RS256 from the allow-list.
- ·Update the JWKS endpoint's
algfield on the key to"PS256".
The Algorithm Confusion Pitfall (Applies to Both)
Both PS256 and RS256 are vulnerable to algorithm-confusion attacks if your verifier picks the verification algorithm from the JWT's own alg header rather than from a server-side allow-list. The classic attack: attacker takes a token meant to be RS256-signed, changes the header to HS256, and "signs" it using the public key as the HMAC secret. A naive verifier reads alg: HS256, fetches the RSA public key (treated as a byte string), runs HMAC-SHA256, and accepts.
The fix is universal: always pick the verification algorithm from your code or configuration, not from the token header. Switching from RS256 to PS256 doesn't help here, the attack works the same way against PS256 verifiers that trust the header.
Decision Guide
- ·New system, full control, no legacy partners → ES256 first, PS256 if RSA infrastructure is already in place.
- ·Mixed legacy ecosystem, need broadest interop → RS256, with intent to migrate to PS256 later.
- ·Fintech, FAPI, OIDC self-issued, compliance regime mandate → PS256 (or whatever the regime mandates explicitly).
- ·HSM-based signing → Confirm the HSM supports RSA-PSS before choosing it; some older HSMs only do PKCS#1 v1.5.
Verifying a PS256 Token
jwtdecode.app verifies PS256 alongside RS256 using the browser's Web Crypto API (SubtleCrypto.verify("RSA-PSS", ...)). Paste any token withalg: PS256 and the matching public key into the JWT decoder, verification runs locally, the token never leaves your browser.
Summary
PS256 is RS256's modern replacement at the same key size: provably secure, randomised, and free of the recurring PKCS#1 v1.5 implementation pitfalls. They share key infrastructure, so migration is a header swap not a key rotation. RS256 stays the right choice when broad legacy interop is the constraint; PS256 is the right choice when compliance or threat-modelling pushes for the stronger padding. Either way, allow-list algorithms server-side, algorithm confusion attacks bite both.