Skip to content

fix(sdjwt): enforce aud/nonce binding on every KB hop, fail closed on terminal - #313

Open
SashaMIT wants to merge 3 commits into
google-agentic-commerce:mainfrom
SashaMIT:fix/kb-sdjwt-aud-nonce-binding
Open

fix(sdjwt): enforce aud/nonce binding on every KB hop, fail closed on terminal#313
SashaMIT wants to merge 3 commits into
google-agentic-commerce:mainfrom
SashaMIT:fix/kb-sdjwt-aud-nonce-binding

Conversation

@SashaMIT

@SashaMIT SashaMIT commented Aug 5, 2026

Copy link
Copy Markdown

Summary

In plain terms: a presentation token's aud/nonce are what bind it to a specific recipient and session. Two gaps in KB-SD-JWT verification weakened that binding:

  1. Intermediate hops never checked aud/nonce even when the caller passed expected values — the check only ran on the terminal hop. (The repo's own test_verify_rejects_aud_mismatch / test_verify_rejects_nonce_mismatch were failing on main because of exactly this.)
  2. A terminal hop carrying aud/nonce verified fine with no expected values passed at all. Per RFC 9901 §7.3 a verifier MUST confirm a key-binding token's aud identifies itself; the AP2 profile always issues terminal hops with aud/nonce, and exp is optional — so a verifier that forgot to bind turned every captured presentation into a replayable bearer credential, potentially indefinitely.

Fix (kb_sd_jwt.verify)

  • expected_aud/expected_nonce are now honored on every hop when provided (fixes the two failing tests).
  • A terminal hop that carries aud/nonce but is verified without binding them raises (fail-closed on the spec MUST). Root-only credential verification is unaffected.

Test plan

  • Full suite: 190 passed (baseline 186 passed + 2 pre-existing failures; the 2 failures are now fixed)
  • 7 existing tests that verified terminal hops without binding were updated to bind expected_aud/expected_nonce — the previously-unsafe pattern
  • 2 new regression tests: terminal hop carrying aud/nonce rejects an unbound verifier

Compatibility note

Callers who verified terminal presentations without passing expected_aud/expected_nonce will now get an error instead of a silent pass — that is the intended behavior change; those verifiers were accepting replays.

Made with Cursor

Made with Cursor

@SashaMIT
SashaMIT requested a review from a team as a code owner August 5, 2026 20:55
@google-cla

google-cla Bot commented Aug 5, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@SashaMIT
SashaMIT force-pushed the fix/kb-sdjwt-aud-nonce-binding branch 2 times, most recently from d50e31d to b0ffebe Compare August 6, 2026 10:41
@SashaMIT

SashaMIT commented Aug 6, 2026

Copy link
Copy Markdown
Author

@googlebot I signed it! (commits re-authored to the signing email)

@SashaMIT

SashaMIT commented Aug 6, 2026

Copy link
Copy Markdown
Author

@googlebot rescan

@SashaMIT
SashaMIT force-pushed the fix/kb-sdjwt-aud-nonce-binding branch from 6244749 to 44ad0bb Compare August 6, 2026 11:30
@SashaMIT

SashaMIT commented Aug 6, 2026

Copy link
Copy Markdown
Author

@googlebot rescan

@SashaMIT
SashaMIT force-pushed the fix/kb-sdjwt-aud-nonce-binding branch 2 times, most recently from 0679ba5 to fef421f Compare August 6, 2026 12:31
@SashaMIT

SashaMIT commented Aug 6, 2026

Copy link
Copy Markdown
Author

@googlebot rescan

@SashaMIT

SashaMIT commented Aug 6, 2026

Copy link
Copy Markdown
Author

CI note: the Lint Code Base failure is the repo-wide Biome run (75 errors in code/web-client TS files, also failing on main at run 25120930358). This PR touches Python SDK + tests only, so none of the reported diagnostics come from this change. The remaining blocker here is cla/google.

@SashaMIT
SashaMIT force-pushed the fix/kb-sdjwt-aud-nonce-binding branch from fef421f to 62e9a81 Compare August 7, 2026 05:41
SashaMIT and others added 3 commits August 7, 2026 12:42
… terminal

Two gaps in KB-SD-JWT verification:

1. aud/nonce were only checked on terminal hops; intermediate hops skipped
   the check even when the caller passed expected values (the repo's own
   test_verify_rejects_aud_mismatch / _nonce_mismatch tests were failing
   on main because of this).

2. A terminal hop carrying aud/nonce verified fine when the caller passed
   no expected_aud/expected_nonce. Per RFC 9901 section 7.3 a verifier
   MUST confirm a key-binding token's aud identifies itself; the AP2
   profile always issues terminal hops with aud/nonce, so accepting one
   unbound turned every captured presentation into a replayable bearer
   credential (exp is optional, so replay could be indefinite).

verify() now checks expected claims on all hops when provided, and a
terminal hop that carries aud/nonce but is verified without binding them
raises. Tests updated to bind at verify time (the previously-unsafe
pattern), plus two regression tests for the fail-closed behavior.

Signed-off-by: SashaMIT <sash@ela.city>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…om words

Signed-off-by: SashaMIT <sash.t.mitchell@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@SashaMIT
SashaMIT force-pushed the fix/kb-sdjwt-aud-nonce-binding branch from 62e9a81 to f2074f5 Compare August 7, 2026 05:42
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.

1 participant