Skip to content
Lesson 3 of 14

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.

python

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 H(Km)H(K \,\|\, m) — 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 H(Km)H(K \,\|\, m) and the length of KK, an attacker resumes from that state and forges a digest for KmpaddinganythingK \,\|\, m \,\|\, \text{padding} \,\|\, \text{anything} without learning KK. 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:

HMAC(K,m)=H((Kopad)H((Kipad)m))\mathrm{HMAC}(K, m) = H\Big((K' \oplus \text{opad}) \,\|\, H\big((K' \oplus \text{ipad}) \,\|\, m\big)\Big)

KK' 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:

  1. Split on . into exactly three segments.
  2. Recompute HMAC-SHA256 over segment0 + "." + segment1the 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.
  3. Compare with the decoded third segment, in constant time.

The trap that leaks your signature one byte at a time

python
python

== 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 22562^{256}. 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"} gives KQVAm0QluVr3pnq1g0wPZe3FzJDmI5OZIc8LEz_Aimo. Encode first, then sign.
  • hexdigest() where digest() belongs. Encoding the 64-character hex string gives an 86-character segment starting NWRhYzliYTk4MGE5MWJk.... The signature is 32 raw bytes.

VERIFY hands you that token under the wrong key and expects BAD.

Up nextRS256: RSA Asymmetric SigningSigning & Verification

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…