Reading — step 1 of 5
Learn
Algorithm Confusion: When the Public Key Becomes the Secret
RS256 let you publish the verification key: a hundred services can check a token and not one of them can mint one. That holds exactly until the verifier lets the token decide how the key gets used.
One key object, header-chosen algorithm
Nearly every vulnerable verifier has the same shape — the key loaded once at startup, the algorithm looked up per request, out of the header:
Read that as an attacker. The service holds an RSA public key — downloadable,
because that is what public means. Change alg from RS256 to HS256, dispatch
lands on the HMAC branch, and that published value becomes a shared secret.
The attacker knows the key the verifier is about to use, so they can produce the
signature it is about to expect. A dict lookup, VERIFIERS[alg](key, ...), has
the identical bug: the prefix test is not the problem, asking the header is.
The forgery, end to end
Grader note: the victim's key is stood in for by the base64 body of its PEM file and the forgery is a plain HMAC, so this block needs no crypto library and runs anywhere. With the real key only the signature value changes — the attack is bytes in, bytes out.
Nothing was stolen and nothing was broken; the verifier was persuaded to use a public value as a private one, then did its job perfectly. The attacker does have to MAC with the exact bytes the server loaded — PEM with or without its trailing newline, or the parsed DER — but that is a handful of candidates, and the server says which is right by answering 200.
Bind the algorithm to the key
A key is not a bag of bytes. It has a type, and the type decides what may be done with it:
"Pin the algorithm in your verifier" is the usual advice, and with one key it
amounts to the same thing. It stops doing so the second you hold two: an allowlist
of {"HS256", "RS256"} covering an HMAC secret and an identity provider's public
key still lets the token choose which of your keys meets which primitive. The
pairing has to come from the key.
Common mistakes
- Keys stored as raw bytes with no type tag. If a public key and an HMAC secret are the same kind of object, some path eventually crosses them.
- Treating
alg != "none"as the fix.HS256is notnone; two checks. - Reading
algbefore resolving the key. By then the decision is taken. - Letting
kidimply the algorithm. It arrives in the same unverified header, and it is a label, not a type.
Your exercise
You will write a verifier that is handed a key and its type, and must reject a token whose header asks for the other family.
The visible test gives you the forged token above while the registered key is the
pub value it was signed with, so its HMAC really does verify. Dispatch on
header["alg"] and you print OK for a token anyone who read the public key
could have made. The expected answer is REJECTED alg_mismatch, and the check
that produces it has to run before you compute anything.
A hidden case makes the point in reverse: an HS256 token MAC'd with something
that is not your public key. Naive dispatch reaches the HMAC, fails it, and
answers bad_signature — a broken signature reported where the real fault was a
misused key.
bad_alg covers what this build does not implement: none, HS512, ES256, a
missing alg, and RS256 too once its family has matched a pub key. Family
first, then algorithm, then signature.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…