Skip to content

fix: hold string claims and exp to REQ-COMMON-19 / REQ-COMMON-19D - #4

Merged
xgreenx merged 2 commits into
feat/ceremony-circuitsfrom
fix/jwt-email-req-common-19-19d
Aug 24, 2026
Merged

xgreenx merged 2 commits into
feat/ceremony-circuitsfrom
fix/jwt-email-req-common-19-19d

Conversation

@xgreenx

@xgreenx xgreenx commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Closes out the two findings from the review of #1 — both pre-existing on
main, both defense-in-depth misses in circuits/jwt_email/src/main.nr.
Targets the feat/ceremony-circuits branch so it lands as part of #1's
history; the branch is #1's head plus this one commit.

REQ-COMMON-19 — trailing structural byte on string claims

The spec requires the circuit to "assert the full "field":" delimiter at
that offset, the value bytes, the closing quote, and the following
structural byte
fixed by the profile". Only email_verified and exp
asserted their trailing byte; the five string claims — email, nonce,
sub, iss, aud — stopped at the closing quote, so a claim-shaped
substring inside a longer signed string value would have satisfied the
match, with JSON escaping of attacker-typed content the only thing in the
way.

Every claim now asserts the byte after its match is , or } through one
shared assert_structural() (the two existing inline checks fold into it),
and the section-9 offset bounds each grow by one byte so the trailing byte
itself must sit inside the authenticated payload length per REQ-COMMON-19B —
without that, the new assertion could be satisfied from prover-controlled
zero padding.

REQ-COMMON-19D — canonical exp

The spec pins integers to canonical 0|[1-9][0-9]* and explicitly lists
leading-zero among the rejects. The digit-run check accepted any digits, so
"exp":0123… was a second signed encoding of the same u64. A multi-digit
exp may no longer begin with '0'; the lone "0" remains legal.

Tests

Four new tests (jwt_email 3 → 7, repo 24 → 28): a claim-shaped substring
whose apparent closing quote is followed by ordinary string content fails
with the new assertion after passing every pre-existing check, and a
leading-zero exp fails; companion positive tests pin , / }
continuations and the two canonical exp shapes (ten-digit timestamp,
lone "0").

Artifact impact (pinned toolchain: nargo 1.0.0-beta.20, bb 5.0.0-nightly.20260324)

before (#1 head) after
jwt_email gates 179,367 179,413 (+46, 0.03 %)
jwt_email vk_hash 0x24db903f…88a5a69f 0x1596b429…2af7a27c
bearer-link / dyaka-noir-token unchanged byte-identical

The README's recorded jwt_email vk_hash is updated accordingly (the
2026-08-12 reproducibility note now dates its hash and records the current
one), and the stale "jwt_email has no tests" comment in ci.yml is corrected.
nargo fmt --check, nargo test (28/28) and scripts/build.sh all pass
locally under the pinned toolchain.

🤖 Generated with Claude Code


Reopened from #3, which was squash-merged into feat/ceremony-circuits rather than left standing as a reviewable diff on top of #1. That merge (effeb69) has been rolled back; feat/ceremony-circuits is restored to 5b1a0bd and this PR carries the identical change for review.

Two spec-compliance gaps found in review of #1, both pre-existing on main.

REQ-COMMON-19 requires a string claim's match to cover the closing quote
AND the following structural byte. Only email_verified and exp asserted
their trailing byte; email, nonce, sub, iss and aud stopped at the closing
quote, so a claim-shaped substring inside a longer signed string value
would have matched -- JSON escaping of attacker-typed content was the only
thing in the way. Every claim now ends at `,` or `}` via one shared
assert_structural(), and the section-9 bounds grow by one byte so the
trailing byte itself must lie inside the authenticated payload length
(REQ-COMMON-19B), not in prover-controlled zero padding.

REQ-COMMON-19D pins integers to canonical 0|[1-9][0-9]* and lists leading
zeros among the rejects; the exp digit-run check accepted them, so a
leading-zero rendering was a second signed encoding of the same u64. A
multi-digit exp may no longer start with '0'; the lone "0" stays legal.

Negative tests cover both: a claim-shaped substring whose apparent closing
quote is followed by ordinary string content now fails, as does a
leading-zero exp; companion positive tests pin the accepted shapes.

The added constraints move jwt_email from 179,367 to 179,413 gates (+46,
0.03 %) and change its vk_hash from 0x24db903f...88a5a69f to
0x1596b429...2af7a27c; the on-chain verifier regenerates with the next
release. bearer-link and dyaka-noir-token artifacts are byte-identical.

Assisted-by: Claude Fable 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
Assisted-by: GPT-5
Signed-off-by: Wondertan <hlibwondertan@gmail.com>
@xgreenx
xgreenx merged commit 27f7f84 into feat/ceremony-circuits Aug 24, 2026
3 checks passed
SupremaLex added a commit that referenced this pull request Aug 24, 2026
)

* fix: restore the REQ-COMMON-19 / REQ-COMMON-19D constraints

These were written by xgreenx in review of #1, merged into that branch as #4,
and then lost: I rebased the branch onto main and force-pushed, so the merge
commit carrying them was discarded and #1 landed without them. Restoring the
patch verbatim onto the renamed path.

REQ-COMMON-19 requires a string claim's match to cover the closing quote AND
the following structural byte. Only `email_verified` and `exp` asserted their
trailing byte; `email`, `nonce`, `sub`, `iss` and `aud` stopped at the closing
quote, so a claim-shaped substring inside a longer signed string value would
have matched -- JSON escaping of attacker-typed content was the only thing in
the way. Every claim now ends at `,` or `}` through one shared
`assert_structural()`, and the section-9 bounds grow by one byte so the
trailing byte itself must lie inside the authenticated payload length
(REQ-COMMON-19B) rather than in prover-controlled zero padding.

REQ-COMMON-19D pins integers to canonical `0|[1-9][0-9]*` and lists leading
zeros among the rejects; the `exp` digit-run check accepted them, so a
leading-zero rendering was a second signed encoding of the same u64. A
multi-digit `exp` may no longer start with `0`; the lone `0` stays legal.

The rebuilt key is `0x1596b4295a9471df3d620bb3a94c970dd695a0617117ed4d0c9190ad2af7a27c`,
which is the value xgreenx's original commit predicted -- an independent check
that the restore is byte-exact. Tests go 3 to 7; `bearer-link` is unchanged.

Still missing from main, and not in this commit: the bb 5.2.0 / nargo
1.0.0-beta.25 toolchain bump, lost the same way. It moves every key again and
needs that toolchain installed to verify, so it goes on its own.

Co-authored-by: xgreenx <xgreenx9999@gmail.com>
Signed-off-by: SupremaLex <georglutsenko@gmail.com>

* chore: restore the bb 5.2.0 / nargo 1.0.0-beta.25 toolchain bump

Wondertan's, lost in the same force-push as the constraints in the commit
before this one. Restoring it here rather than after the release, because it
moves every verification key and releasing without it means re-cutting.

Rebuilt and verified under the restored pins, with x-token already gone:

  bearer-link  0x1d161afb536683d31a3e426db0feaa30de8be89cc45510579f361266c20e078f
  oidc-google  0x1b50bbf6d8ea6efc7ecc2547c25b285511704a10d9247466e84045095d9c3f77

Both keys move: the toolchain is what derives them, which is the whole reason
the pins are a single source of truth. Downstream verifiers regenerate from
these, so libid-contracts rolls its committed Solidity together with the tag
that carries them.

bearer_link 12 tests, oidc_google 7, `nargo fmt --check` clean on both under
beta.25.

Co-authored-by: Wondertan <hlibwondertan@gmail.com>
Signed-off-by: SupremaLex <georglutsenko@gmail.com>

---------

Signed-off-by: SupremaLex <georglutsenko@gmail.com>
Co-authored-by: xgreenx <xgreenx9999@gmail.com>
Co-authored-by: Wondertan <hlibwondertan@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants