Skip to content
100% in your browser. Nothing you paste is uploaded — all processing runs locally. Read more →

HS256 vs RS256

On this page
  1. What is HS256?
  2. What is RS256?
  3. HS256 vs RS256 at a glance
  4. When to use HS256
  5. When to use RS256
    1. Publishing keys with JWKS
    2. Rotating keys with kid
  6. Key sizes
  7. Signature and token size
  8. Common mistakes
    1. Short or guessable HS256 secrets
    2. Algorithm confusion (RS256 → HS256)
    3. Accepting alg: none
    4. Not pinning the algorithm on verify
    5. Treating decode as verify
  9. What about ES256 and PS256?
  10. Frequently asked questions
    1. Is HS256 secure?
    2. Can I verify an HS256 token with a public key?
    3. Is RS256 encryption?
    4. Should I use RS256 or ES256 for a new system?
    5. How do I move from HS256 to RS256?
    6. Can Tooljo’s tools check an RS256 signature?
  11. Try it
  12. Related

HS256 is HMAC with SHA-256. It signs a JWT with one shared secret, and the same secret verifies it. RS256 is an RSA signature with SHA-256. A private key signs the token and the matching public key verifies it. Both put the same kind of token on the wire. What differs is who holds which key.

The short answer: if one service signs and verifies its own tokens, HS256 is fine, as long as the secret is long and random. If other services or third parties need to verify tokens, use RS256 (or ES256) so they only ever get a public key. The table below sums it up.

You can paste any token into the JWT decoder to see its alg. It runs in your browser. Nothing is uploaded, and there’s no signup.

What is HS256?

HS256 is the JWT name for HMAC using SHA-256. It’s defined in RFC 7518, section 3.2, the JSON Web Algorithms spec. It’s the one algorithm every JWS implementation is required to support.

HMAC mixes a secret key into a SHA-256 hash of the signing input, which is base64url(header) + "." + base64url(payload). The 32-byte result is the signature. To verify, the receiver runs the same HMAC with the same secret and compares the two values. RFC 7518 says that comparison must be constant-time, which any decent library already does for you.

The key point is that there’s one key. Whoever can verify an HS256 token can also create one. RFC 7515 section 10.5 spells out what that means: a valid MAC only shows the token came from someone holding the secret. It can’t prove which party that was.

The “256” is SHA-256. HS384 and HS512 are the same algorithm with SHA-384 and SHA-512.

What is RS256?

RS256 is RSASSA-PKCS1-v1_5 using SHA-256 (RFC 7518, section 3.3). It’s the classic RSA signature scheme from PKCS #1. The issuer signs with its private key, and anyone with the matching public key can verify.

That split is the whole point. The public key can verify tokens but can’t create them, so you can hand it to every service, partner or client that needs to check a token. Only the issuer holds the private key.

HS256 vs RS256 at a glance

HS256RS256
Full nameHMAC using SHA-256RSASSA-PKCS1-v1_5 using SHA-256
Key typeOne shared secretRSA key pair
Who can signAnyone with the secretOnly the private-key holder
Who can verifyAnyone with the secretAnyone with the public key
Minimum key size (RFC 7518)256 bits2048 bits
Signature size32 bytes (43 base64url chars)Same as the key: 256 bytes for 2048-bit (342 chars)
Key distributionSecret must reach every verifier privatelyPublic key can be published, e.g. in a JWKS
Good fitOne service signs and verifies its own tokensMany services or third parties verify
JWA implementation requirementRequiredRecommended

The last row is what the JWA spec (and the IANA JOSE registry) asks libraries to support. It’s not advice on which one you should pick.

When to use HS256

HS256 fits when the signer and the verifier are the same system, or a very small set of systems you fully control:

It gets awkward as soon as the circle grows. Every verifier needs the secret, and every verifier can therefore mint tokens. A leak from any one of them compromises all of them. Rotating the secret means updating every holder at once.

When to use RS256

Reach for RS256 (or the other public-key algorithms below) when tokens cross a trust boundary:

Publishing keys with JWKS

Public keys are usually published as a JWK Set (RFC 7517, section 5). That’s a JSON document with a keys array. Each key can carry a kid (key ID), a use of "sig", and an alg:

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "2026-09",
      "use": "sig",
      "alg": "RS256",
      "n": "0vx7agoebGcQSuu…",
      "e": "AQAB"
    }
  ]
}

(n is the modulus, cut short here. e of AQAB is 65537.)

The token header names the key it was signed with:

{ "alg": "RS256", "typ": "JWT", "kid": "2026-09" }

Where do verifiers find the JWKS? That’s up to the issuer. With OpenID Connect, the issuer’s discovery document lists it as jwks_uri. RFC 8725 section 3.8 says the keys you verify with must belong to the issuer, so configure the JWKS URL yourself. Don’t take it from the token.

Rotating keys with kid

RFC 7515 describes kid as a hint that lets the signer “explicitly signal a change of key.” A typical rotation goes like this:

  1. Add the new public key to the JWKS under a new kid, next to the old one.
  2. Give verifiers time to refresh their cached copy.
  3. Start signing with the new private key.
  4. Remove the old key once every token signed with it has expired.

Treat kid as untrusted input. RFC 8725 section 3.10 warns that it can carry SQL or LDAP injection if you use it in a lookup. It also says blindly following a jku or x5u URL from the header invites SSRF.

HS256 has no public half to publish. Rotating means getting the new secret to every verifier through a private channel.

Key sizes

HS256: at least 256 bits. RFC 7518 section 3.2: “A key of the same size as the hash output (for instance, 256 bits for “HS256”) or larger MUST be used with this algorithm.” HS384 needs 384 bits and HS512 needs 512.

256 bits is 32 bytes, and it means 32 random bytes, not 32 typed characters. RFC 8725 section 3.5 says human-memorable passwords must not be used directly as an HS256 key. Generate the secret instead:

# 32 random bytes, printed as 44 base64 characters
openssl rand -base64 32

Whatever form you store it in, the signer and every verifier have to use the exact same bytes. A secret that’s base64-decoded on one side and used as a plain string on the other won’t match.

RS256: at least 2048 bits. RFC 7518 section 3.3: “A key of size 2048 bits or larger MUST be used with these algorithms.” NIST SP 800-57 Part 1 (Rev. 5) rates 2048-bit RSA at 112 bits of security and 3072-bit at 128 bits. Rev. 5 accepts 112 bits for new signatures through 2030. If your keys need to outlive that, 3072 bits is the next step.

# RSA private key (2048-bit) and its public key
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private.pem
openssl pkey -in private.pem -pubout -out public.pem

Keep private.pem on the signer only. public.pem, or its JWK form, is the part you share.

Signature and token size

The signature is the third segment of the token. Its size depends only on the algorithm and key, not on the payload:

AlgorithmSignatureBase64url in the token
HS25632 bytes43 characters
RS256, 2048-bit key256 bytes342 characters
RS256, 3072-bit key384 bytes512 characters
ES25664 bytes86 characters

An HMAC-SHA-256 output is always 32 bytes. An RSA signature is always as long as the modulus (RFC 8017), so a 2048-bit key gives 256 bytes. Switching a token from HS256 to RS256 with a 2048-bit key adds about 300 characters to every request that carries it. That usually doesn’t matter. It can matter for tokens sent in cookies or headers close to a size limit.

The decoder shows the algorithm and the signature length in bytes under any token you paste.

On speed: HMAC is a keyed hash, so it’s cheap both ways. With RSA, signing with the private key is the expensive side, and verifying with the public key is much lighter. For most APIs neither is the bottleneck. Choose on key distribution, not speed.

Common mistakes

Short or guessable HS256 secrets

This is the big one. Anyone who gets hold of a single HS256 token has everything needed to test guesses offline: the signing input and the signature. No requests to your server, no rate limits. RFC 8725 section 2.2 calls weak HMAC keys “vulnerable to offline brute-force or dictionary attacks once an attacker gets hold of such a token.” If the secret is a word, a phrase, a default like secret, or a value from a tutorial, assume it’s guessable. Use 32+ random bytes.

Algorithm confusion (RS256 → HS256)

Your service signs with RS256, and the public key is, by design, public. An attacker builds a token with "alg": "HS256" and computes an HMAC over it, using your public key as the HMAC secret. A verifier that reads alg from the header and passes “the key” to whatever algorithm the header names will run HMAC with the public key. It matches, and the forged token is accepted.

RFC 8725 section 2.1 describes exactly this, citing CVE-2015-9235. The fix in section 3.1: the caller specifies the allowed algorithms, and “each key MUST be used with exactly one algorithm.”

Accepting alg: none

none is a registered value that means “no signature” (RFC 7518, section 3.6). The spec says implementations “MUST NOT accept Unsecured JWSs by default.” Some libraries used to accept them anyway, and an attacker could strip the signature and set alg to none (RFC 8725 section 2.1). If your verifier accepts none, it isn’t verifying.

Not pinning the algorithm on verify

Both attacks above come from the same root cause: letting the token pick its own algorithm. RFC 7519 section 7.2 says it’s “an application decision which algorithms may be used,” and a token using any other algorithm should be rejected. Pass an explicit allow-list every time, even if your library’s default happens to be safe today.

// Example uses the jose library for Node.js. Most JWT libraries have an
// equivalent "algorithms" option.
import { createRemoteJWKSet, jwtVerify } from "jose";

// RS256: verify with the issuer's published public keys
const JWKS = createRemoteJWKSet(new URL("https://auth.example.com/jwks.json"));
const { payload } = await jwtVerify(token, JWKS, {
  algorithms: ["RS256"],
  issuer: "https://auth.example.com",
  audience: "api.example.com",
});

// HS256: verify with the shared secret
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const result = await jwtVerify(token, secret, { algorithms: ["HS256"] });
# Example uses PyJWT
import jwt
payload = jwt.decode(token, public_key, algorithms=["RS256"], audience="api.example.com")

Treating decode as verify

Decoding a JWT just base64url-decodes the header and payload. It checks nothing. RFC 7519 section 7.2 says a JWT that fails any validation step “MUST be rejected,” and RFC 8725 section 3.3 says every cryptographic operation must be validated. Reading sub out of a token you haven’t verified is trusting whatever the sender typed. The full breakdown is in JWT decode vs verify.

What about ES256 and PS256?

ES256 is ECDSA using the P-256 curve and SHA-256 (RFC 7518, section 3.4). It uses the same model as RS256: a private key signs and a public key verifies, and you can publish it in a JWKS the same way. The keys and signatures are much smaller. A P-256 key gives about the same security as 3072-bit RSA (NIST SP 800-57), and an ES256 signature is 64 bytes against 256 for RS256 with a 2048-bit key. The JWA spec lists it as “Recommended+”, meaning its requirement level is likely to go up. ECDSA needs a fresh random value for every signature. RFC 8725 section 3.2 asks libraries to use the deterministic form from RFC 6979. Use a maintained library and don’t write this yourself.

PS256 is RSASSA-PSS with SHA-256 (RFC 7518, section 3.5). It uses the same RSA keys (2048 bits or larger) and the same signature size as RS256, but with the newer PSS padding. Pick one per key. RFC 8725 says each key gets exactly one algorithm.

Ed25519 is also registered for JWS. The older generic EdDSA name is now marked deprecated in the IANA registry (RFC 9864). Check which name your library and your verifiers expect.

Frequently asked questions

Is HS256 secure?

Yes, when the secret is at least 256 bits of real randomness and stays private. Almost every HS256 break is a weak or leaked secret, not a flaw in HMAC-SHA-256. The other risk is structural: every verifier can also sign.

Can I verify an HS256 token with a public key?

No. HS256 has no public key. Verifying needs the same secret that signed the token. If someone asks you for a “public key” for an HS256 token, the token is using the wrong algorithm for the job.

Is RS256 encryption?

No. RS256 signs the token. It doesn’t hide anything. The header and payload are base64url-encoded and readable by anyone holding the token, whichever algorithm signed it. If the contents must be confidential, you need JWE (encrypted JWTs) or should keep sensitive data out of the token.

Should I use RS256 or ES256 for a new system?

Both use the same public/private model and both are fine. ES256 gives smaller keys and tokens. RS256 is the older and very widely supported choice. Check what your identity provider, gateways and client libraries support, then pin that one algorithm.

How do I move from HS256 to RS256?

Generate an RSA key pair and publish the public key (a JWKS with a kid works well). Then update verifiers to accept RS256 for tokens signed with the new key. Switch the issuer to RS256, and remove HS256 from every allow-list once the last HS256 token has expired. Don’t configure a single verifier to accept both algorithms with the same key material. That’s the setup algorithm confusion exploits.

Can Tooljo’s tools check an RS256 signature?

Not today. The decoder reads any JWT, whatever the algorithm. Verify checks HS256, HS384 and HS512 signatures, and Sign creates HS256 test tokens. For RS256 or ES256, verify in a server-side library with the algorithm pinned, as in the snippets above.

Try it

Everything runs in your browser with the Web Crypto API. Nothing you paste is sent anywhere. Use test secrets, not production keys.