Step 1 of 5 · Reading · ~3 min
Read
Signing & Verification
HS256 Signing
HS256 is HMAC with SHA-256: one secret key both creates and checks the signature. The simplest thing that works, and the one algorithm every JWT library must ship.
What exactly gets signed?
Not the JSON, not the token. The signing input is the first two segments, already base64url-encoded, joined by the dot that will separate them:
signing_input = base64url(header) + "." + base64url(payload)
signature = HMAC-SHA256(secret, signing_input)
token = signing_input + "." + base64url(signature)
The dot is signed too — that is what stops an attacker shortening the header a character and lengthening the payload to match.
Output:
signing input: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiJ9
signature : XaybqYCpG9G33sm03ZZRA3x1axtm8cRBBlsVnp8L6FI
token : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiJ9.XaybqYCpG9G33sm03ZZRA3x1axtm8cRBBlsVnp8L6FI
Why HMAC, and not just hash the secret with the message?
The obvious construction is — glue the secret on the front and
hash. It is broken. SHA-256 is Merkle–Damgård, so its output is its internal
state after the last block: given and the length of , an
attacker resumes from that state and forges a digest for
without learning .
A length-extension attack — and appending ,"role":"admin" is exactly that
shape.
HMAC hashes twice with two key-derived pads, so there is no state to resume:
is the key fitted to the hash's 64-byte block: zero-padded if shorter, hashed down if longer — so a 100-byte key and its SHA-256 digest give identical signatures.
Verifying is recomputing
You cannot "decrypt" an HMAC — nothing is inside it. Verification is the identical computation, then a comparison:
- Split on
.into exactly three segments. - Recompute HMAC-SHA256 over
segment0 + "." + segment1— the bytes you received, never a re-serialization of the parsed JSON. RFC 7519 §7.1 allows whitespace there and mandates no canonicalization, so only the sender's exact bytes reproduce their signature. - Compare with the decoded third segment, in constant time.
The trap that leaks your signature one byte at a time
== returns as soon as it finds a mismatch, so a signature sharing the first byte
takes measurably longer to reject. An attacker submits 256 guesses for byte 0, keeps
the slowest, moves to byte 1, and rebuilds a valid 32-byte signature in a few
thousand requests instead of . hmac.compare_digest always reads every
byte.
What symmetric costs you
Anyone who can verify can also sign. Thirty services holding the secret means thirty places that can mint an admin token. When the verifier must not be able to forge, you need an asymmetric algorithm.
Use 32+ CSPRNG bytes: a memorable passphrase is a password, and cracking tools grind wordlists against a captured token with no rate limit.
Your exercise
You will implement sign and verify.
The visible SIGN case must produce exactly
XaybqYCpG9G33sm03ZZRA3x1axtm8cRBBlsVnp8L6FI. Two near-misses the grader catches:
- Signing the JSON, not the segments. HMAC over
{"alg":"HS256","typ":"JWT"}.{"sub":"42"}givesKQVAm0QluVr3pnq1g0wPZe3FzJDmI5OZIc8LEz_Aimo. Encode first, then sign. hexdigest()wheredigest()belongs. Encoding the 64-character hex string gives an 86-character segment startingNWRhYzliYTk4MGE5MWJk.... The signature is 32 raw bytes.
VERIFY hands you that token under the wrong key and expects BAD.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…