perf: parse and verify each token once - #30
Merged
Merged
Conversation
The decode path decoded a token's header and claims three times on a --key run: jwt.ParseUnverified decoded both segments, parseUnverifiedJWT threw those away and redid them strictly, and verifyJWTSignature parsed the whole compact string again just to reach the cryptography. parseUnverifiedJWT now does the segment work itself and carries the signing method and decoded signature bytes forward, so verification calls SigningMethod.Verify over the segments it already has. The algorithm allowlist jwt.WithValidMethods used to apply is now spelled out explicitly and runs before Verify -- without it an HS256 token signed with a published public key would verify against that key. Both text escapers open with a byte-level plain-ASCII shortcut, the shape predicates gained byte forms so a decrypted payload is not copied just to be measured, and whole-second timestamps skip the big.Rat path. On a 36 KB token: parse 346us -> 170us, verify 196us -> 19us, full --key decode 1.15ms -> 0.74ms, and the escapers go from 287/664 MB/s to about 4.1 GB/s on clean text. Behaviour is unchanged: stdout and stderr are byte-identical across signed JWT, JWE, and malformed-token cases, and only the wording of four malformed-JSON errors differs, now naming the failing segment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q26LwBBSBPUpbf1kd4Ep3z
Verification used to re-parse the compact string through jwt.Parse, and that was the only thing reading the field. Nothing reads it now, and an unread struct field draws no compiler or vet diagnostic, so it would quietly rot out of step with the doc comment. The segments still reassemble into the original token where a caller needs it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q26LwBBSBPUpbf1kd4Ep3z
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Removes redundant work from the decode path. No user-visible behavior change: a
jwtd <token>invocation is still ~0.9 ms, of which ~0.46 ms is bare Go process startup. These wins only show on unusually large tokens and payloads.What was redundant
A
--keyrun decoded the token's header and claims three times:jwt.ParseUnverifieddecoded both segments — with plainjson.Unmarshal, which loses number precision and accepts trailing data, so every result was discardedparseUnverifiedJWTre-decoded both strictlyverifyJWTSignaturecalledjwt.Parseon the whole compact string again, purely to reach the cryptographyparseUnverifiedJWTnow does the segment work itself and carries the signing method and decoded signature bytes forward, so verification callsSigningMethod.Verifyover the segments it already has.Security-critical detail
Verification used to get its algorithm allowlist from
jwt.WithValidMethodsinsidejwt.Parse. Verifying from an already-parsed token makes that jwtd's own check, so it is now explicit and runs beforeVerify:Without it, an HS256 token signed with the bytes of a published public key would verify against that key.
TestVerifyJWTSignature_RejectsAlgOutsideKeyTypeAllowlistpins this across RSA, EC, Ed25519, and HMAC keys.Other changes
bytes.ContainsFunc: rune decoding maps every invalid UTF-8 byte toRuneError, which no escape predicate matches, so a rune-level fast path would wave malformed bytes through unescaped.isJWEBytes/isJWTBytesshare their delimiter counts with the string forms, soprintDecryptedPayloadno longer copies an unbounded decrypted payload just to measure it.claimTimeconverts whole seconds withstrconv.ParseInt, keeping the exactbig.Ratpath for the fractional and exponent forms.parsedJWT.rawdropped — removingjwt.Parseleft it with no reader, and an unread struct field draws no compiler orvetdiagnostic.Numbers (36 KB / 500-claim token)
parseUnverifiedJWTverifyJWTSignature--keydecodeescapeTerminalText(clean)escapeFormattedJSONControls(clean)formatTimestampsVerification
Beyond the suite,
vet, andgofmt, the old and new binaries were diffed directly: 555 malformed/random tokens, 108 signed-token × key × flag combinations, and 42 JWE cases across JSON/text/binary/nested/array payloads — stdout and stderr byte-identical, zero differences.The only behavior change anywhere is the wording of four malformed-JSON errors, which now name the failing segment (
parsing JWT claims: EOFrather thanparsing JWT: token is malformed: could not JSON decode claim: EOF). Same rejections, same exit codes.New tests pin the properties that removing
jwt.Parse/jwt.ParseUnverifiedput at risk: the algorithm allowlist across every key type, the eight malformed-token rejections that used to come from the library (extra segments, missing/unknownalg, non-base64url segments), and fast-path equivalence for both escapers and the timestamp shortcut.🤖 Generated with Claude Code
https://claude.ai/code/session_01Q26LwBBSBPUpbf1kd4Ep3z