Notes › Tech Notes › The 13-Cell Algorithm Table and the 7 Verify Gates: the JWT Page Verifies Through Fewer Branches Than It Decodes, Signed and Checked for Real

The 13-Cell Algorithm Table and the 7 Verify Gates: the JWT Page Verifies Through Fewer Branches Than It Decodes, Signed and Checked for Real

The same JWT pasted into the decode page reads fine yet fails verification: the 13-cell ALGS table carries only three field kinds, a real sign-and-verify matrix over 12 algorithms and four key entrances, signature bytes tracking keys and hashes, and six of seven gates returning before any key is read.

The same JWT reads fine yet never passes verification

Paste an RS256 token into the decode page: the header and the payload still decode, but clicking verify signature answers that no key has been entered yet. Running the template's whole script inside node:vm explains it - this page builds reading and verifying as two separate sets of decisions, and the verify side is so narrow that only 13 algorithm names get through the door, while the first six gates never look at the key you typed.

The page under test is JWT decode: 768 lines and 38463 bytes, with the accounts kept in ALGS (L199–213), pemToDer (L604) and doVerify (L648); every token and key was self-signed under the demo- prefix.

Only three field kinds are written in the 13 cells

Object.keys counts out 13 keys, the left column of Figure 1. hashByte appears only in the three HS rows (32/48/64), salt only in the three PS rows (32/48/64), and curve only in the three ES rows (P-256, P-384, P-521); the remaining 10 cells carry none of the three, and none has only family.

The easiest trap is ES512: the key says 512 while the curve is P-521, nine apart; each coordinate segment is 66 bytes, so the signature is 132, which is 2×66 rather than 2×64.

13 algorithm keys, family, panel switches HS ×3 · hashByte 32/48/64 hmac HMAC box RS ×3 · no field kinds rsa public-key box PS ×3 · salt 32/48/64 pss public-key box EC ×3 · curve P-521 ec public-key box none · only the family cell none all three hidden

Figure 1: the fields decide the family, and the family decides which panels show.

The signature segment follows the key and the hash, not the algorithm name

  • The three HS levels give 32, 48 and 64 bytes, following the hash, matching the three hashByte cells one by one.
  • RS256@2048 and RS384@2048 both come out 256, and only RS512@4096 reaches 512; the PS levels keep the same books.
  • ES256 is 64, ES384 is 96 and ES512 is 132, following the curve.

Converted into base64url character counts this is the twelve-stop last row of Figure 2, equal to ceil(bytes/3)*4 minus the padding on all 12 rows.

Bytes (bar height) and characters (last row) 32 48 64 64 96 132 256 256 512 HS256 HS384 HS512 ES256 ES384 ES512 RS256 RS384 RS512 43 64 86 86 128 176 342 342 683

Figure 2: the two bars of equal height come from RS256 and RS384 under a 2048-bit key.

12 algorithms × 4 entrances, and only two outcomes

In every row the kid is demo-key-1 and the payload carries lvl as 3, the HS key is demo-shared-key-2026-b22, and the asymmetric side freshly generates RSA-2048/4096 plus PEMs on the three P curves. Across the four entrances there are only two outcomes: with the right key and exported as JWK each passes 12/12; changing lvl from 3 to 4 while leaving the signature segment untouched, or swapping in a wrong key of the same parameters, matched none of 12/12.

A key is sized up by its first character

importKey trims first, then checks whether the first character is {: parseable goes to jwk, otherwise hmac falls to raw and everything else to spki. Writing the same key material as raw and as JWK recomputes byte-identical signatures, over 32 bytes and 64 hex characters. The pitfall is {"a":1}: the parse succeeds, the jwk branch throws a DataError whose message carries no localized wording, and the panel falls back to the generic notice that the pasted key content is incomplete or does not match the algorithm declared in the header.

Three rejections in pemToDer and one rescue

Mismatched BEGIN/END reports that no PEM BEGIN/END markers were recognized, and a raw base64 string lands on the same line; a label other than PUBLIC KEY echoes back the name recognized, CERTIFICATE and RSA PUBLIC KEY each getting their own; when the BEGIN and END labels do not pair up the backreference fails and the message is still the first one. The body is stripped of whitespace, then - becomes + and _ becomes / before atob, and failure reports Base64 cannot be restored with the length N, where N counts the characters left after stripping whitespace. The rescue: the SPKI DER of an RSA-2048 demo public key, 294 bytes encoded as 392 characters, with its 19 plus and slash signs rewritten as base64url characters still produced byte-identical output both times; a second key gave 15. The character-table difference is laid out on the Base64 page.

Seven gates, six of them ahead of reading the key

GateTriggerMessage
L650No tokentoast only: paste a token first
L651A five-segment JWEcannot verify the signature
L654alg is nonethere is no signature to verify
L658Family outside the fournot within what this machine can verify
L663No WebCryptobrowser-side condition, not measured (pure-Node harness)
L667Missing third segmentthe signature segment is missing
L675Empty key boxno key has been entered yet

All seven were run with the key box left empty: except the last, none echoed any key wording, because the first six return before L672 reads keyText. The fourth gate was fed six inputs - HS224, ES256K, EdDSA, hs256, A128GCM and a header without alg - and all six land on the same line, since the table lookup is case sensitive.

The two branches hand back different things on failure

HMAC recomputes and compares: sign produces expect, equalBytes takes over, first on length, then a fixed-length XOR-OR fold; on failure both segments print their first eight bytes, always 16 hexadecimal characters plus one ellipsis. RS, PS and EC go through verify, which only returns a boolean, and "signature N bytes" appears only in the success wording; the truncated signatures - 255 bytes for RS256 and 63 bytes for ES256 - return false on the Node side and enter the signature-mismatch wording instead of an error.

The salt never shows in the length either: the saltLength for PS256 is hard-coded to 32, and under the same RSA-PSS-2048 key a signature issued with salt=20 is still 256 bytes and 342 characters, still fails verification, and the panel never mentions salt.

One note on scope: apart from the entry marked in the table, everything above is measured in pure Node; DOM pixels, render-timer firing counts, and whether the asymmetric libraries throw on truncated signatures were not measured, because no browser was opened in this batch.

Three panel switches, each listening only to family

jwt-hs-box opens only for hmac and jwt-pk-box only for rsa, pss or ec, and the button takes the or of the two. Eight inputs were tried: with no alg, an unknown string XYZALG, and none, all three elements hide; HS256 opens only the HMAC box; RS256, PS384 and ES512 open only the public-key box; and a bad signature whose length divided by 4 leaves remainder 1 moves no switch - only the middle of that line changes from N bytes to undecodable.

Frequently asked questions

Q: I pasted one JWT - why does this page read its content yet fail the signature?

Two separate decision sets: decoding only requires the segments to split and the bytes to restore, while verification must clear the gates first - alg has to sit in the 13 cells, the family inside the four, and a real third segment must exist. The three-part structure and how to read exp are covered in the older post on this site.

Q: If the signature segment is 256 bytes, must it be RS256?

Not necessarily: under a 2048-bit key RS and PS both give 256 bytes and 342 characters; only the three HS levels follow the hash, and only the three ES levels follow the curve.

Q: I did enter a key - why does it still say none was entered?

Either it went into the wrong box - visibility follows alg, and anything outside the four families or with no alg hides the button along with the box - or that box holds nothing but a run of spaces.

📎 Related articles

Eight elements compute 40320 permutations but list only 5000 rows: two unrelated caps on the permutation page, duplicates counted by 3!, and the process break at n above 20 2026-11-12 Assembly Order and Case Folding for Ad Links: Six UTM States, Two Hard Caps and Five Panel Fields on Their Own Scales 2026-11-12 Drag to 4 and drag to 8, both preview 43890 blocks: two block grids for preview and export, three stops that crash and the 6px misclick threshold 2026-11-10 Putting a Bitmap Into SVG Only Swaps the Box: the 232-Byte Container Constant, a base64 Embed Checkbox That Never Fires, and the 142064 Characters That Do Not Move Across Four Zoom Levels 2026-11-10 271 Characters Quietly Missing After the Conversion: The Hole Protocol, the 64-Round Refill Cap and the 20-Entry Warning Limit of the Markdown to HTML Page 2026-11-10