Repository navigation
Conversation
Signed AP2 mandate fixtures under contrib/mandate-corpus/, plus a second
verifier in a different language. No SDK source file is modified.
There is no fixture, corpus, vector or conformance directory in this
repository:
find . -type d \( -name "*fixture*" -o -name "*corpus*" -o -name "*vector*" -o -name "*conformance*" \)
-> (empty)
The Python SDK can sign mandates (sd_jwt.create, kb_sd_jwt.create) but
nobody publishes signed fixtures. Consequently the positive test in
Universal-Commerce-Protocol/conformance is skipped; PR google-agentic-commerce#115 there states:
"A real positive needs a signed SD-JWT+kb mandate fixture and a signing
model, which this PR does not attempt."
Why this blocks the ecosystem, measured 2026-10-04:
language lines signing crypto mandate support
Python 8863 20 modules works
Go 2258 none (FakeJWT) cannot verify
PHP 39194 present PR google-agentic-commerce#90 unmerged since 2026-07-15
The Go sample hardcodes a token and contains zero files performing real
signing or verification:
code/samples/go/pkg/roles/merchant_agent/tools.go:31
FakeJWT = "eyJhbGciOiJSUz1NiIsImtpZIwMjQwOTA..."
Python was therefore the only language able to verify an AP2 mandate, so
no other implementation could be tested. That is why the corpus alone was
not enough and this adds a Go verifier, standard library only, so the
point cannot be dismissed as wiring in a dependency.
Contents:
- tools/generate_fixtures.py - reproducible signed fixture generator
- tools/verify_fixtures.py - reference verifier, python
- tools/negative_vectors.py - negative vectors plus a positive control
- tools/poc_aud_not_enforced.py - reproducer for the defect below
- go/sdjwt/, go/cmd/verify/ - second verifier, go standard library only
- crosscheck.py - python versus go, must agree
- vectors/ - three signed vectors plus index.json
Vectors: card-basic (100 USD), debit-large (50000 minor units) and
nonusd (BRL), the last catching implementations that silently normalise
every currency to dollars.
Cross-language agreement, enforced in CI:
agreements: 3/3, divergences: 0
The go verifier deliberately omits the same checks the reference SDK omits,
notably expected_aud, so that the two agree. When the SDK gains that check
the identical check belongs here.
Defect found and reported rather than patched, because changing verification
semantics is a maintainer decision: verify_chain documents that it enforces
expected_aud and expected_nonce on the terminal hop, but kb_sd_jwt.verify
calls verify_expected_claims only when typ in TYP_TERMINAL, and a
kb+sd-jwt+kb token - which the SDK itself produces whenever the payload
carries a cnf - falls into TYP_INTERMEDIATE. The check never runs and no
warning is emitted:
hop type : kb+sd-jwt+kb (per TYP_INTERMEDIATE)
aud in token : https://attacker.example/ap2
expected_aud : https://honest-merchant.example/ap2
ACCEPTED a token with the wrong audience.
VERDICT: DEFECT CONFIRMED
Possible fixes: evaluate expected_aud/expected_nonce against the last token
regardless of typ; or widen TYP_TERMINAL; or raise when expected_aud is
supplied but the hop type is not terminal.
The go verifier is given least-privilege permissions: the workflow only reads
a checkout and runs local code.
Scope and limits:
- Vectors are validated by two implementations, but both by the same author.
Independent authorship would be stronger.
- The go verifier covers the happy path plus cnf and sd_hash. It does not cover
the negative vectors yet.
- No iat/exp checks.
- The defect reflects AP2 HEAD as of 2026-10-04.
Apache-2.0, matching this repository. Independent work; no affiliation with
Google LLC.
zizmor flagged five unpinned action references: mandate-corpus.yml:37,38,60,61,64: unpinned action reference: action is not pinned to a hash (required by blanket policy) Found 5 findings for mandatory checks that must always succeed. Tag references are mutable, so a compromised or repointed tag would run attacker code with this repository's credentials. Pinning to a commit digest removes that surface, and the version is kept in a trailing comment so the pin stays readable and auditable. actions/checkout v4 -> 11d5960a326750d5838078e36cf38b85af677262 actions/setup-python v5 -> a26af69be951a213d495a4c3e4e4022e16d87065 actions/setup-go v5 -> 40f1582b2485089dde7abd97c1529aa768e1baff Cross-check still passes locally: 3/3 agreements, 0 divergences.
PR_BODY.md and PR_URL*.txt are working drafts used to open pull requests. A previous 'git add -A' staged them, so spellcheck started reading PR_BODY.md and failed on a British spelling that no other file in the tree uses. Moved to sales/03.10.2026/pr-drafts/ and ignored here. The PR body is pasted into the GitHub form, it has no business being in the source tree.
Two problems, both self-inflicted: - 'pip install -e .' generated code/sdk/python/ap2.egg-info/, and a 'git add -A' staged it. Six build-artifact files do not belong in the source tree; now ignored. - The .gitignore comment was in Russian, which spellcheck flagged. Every other file added by this contribution is English-only, so the comment now is too.
This branch has not been deployed
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.
Summary
Signed AP2 mandate fixtures under
contrib/mandate-corpus/, plus a second verifier in a different language. No SDK source file is modified.A corpus that claims other languages can use it is only a claim until an independent implementation reads it. This supplies that implementation.
The gap
There is no
fixture,corpus,vectororconformancedirectory in this repository:The Python SDK can sign mandates (
sd_jwt.create,kb_sd_jwt.create), but nobody publishes signed fixtures. Consequently the positive test inUniversal-Commerce-Protocol/conformanceis skipped; PR #115 there states:Why this blocks the ecosystem, measured 2026-10-04
code/samples/go)FakeJWTucp-php-sdk)The Go sample hardcodes a token and contains zero files performing real signing or verification:
Python was the only language able to verify an AP2 mandate, so no other implementation could be tested. That is why the corpus alone was not enough, and why this adds a Go verifier.
Contents
tools/generate_fixtures.pytools/verify_fixtures.pytools/negative_vectors.pytools/poc_aud_not_enforced.pygo/sdjwt/,go/cmd/verify/crosscheck.pyvectors/index.jsonVectors:
card-basic(100 USD),debit-large(50 000 minor units),nonusd(BRL) — the last catches implementations that silently normalise every currency to dollars.Cross-language agreement
crosscheck.pyruns in CI, so agreement is enforced rather than observed. A change that breaks the chain in one language but not the other fails the build — the failure mode a conformance corpus exists to prevent.The Go verifier uses the standard library only, deliberately: the claim "Go cannot do this" should not be answerable by wiring in a dependency.
Wire format
Taken from the reference SDK rather than assumed, which mattered in three places:
2. Defect found:
expected_audis not enforcedImpact: audience check bypass. Severity: medium. Reported rather than patched, because changing verification semantics is a maintainer decision.
verify_chaindocuments:But
kb_sd_jwt.verifyonly callscommon.verify_expected_claimswhentyp in TYP_TERMINAL:A token with
typ='kb+sd-jwt+kb'— which the SDK itself produces whenever the payload carries acnf— falls intoTYP_INTERMEDIATE. Theaud/noncecheck therefore never runs, even when the caller passedexpected_aud. No warning is emitted.Possible fixes: evaluate
expected_aud/expected_nonceagainst the last token regardless oftyp; or widenTYP_TERMINAL; or raise whenexpected_audis supplied but the hop type is not terminal.The Go verifier deliberately omits the same check, mirroring upstream behaviour, so that the two agree. When the SDK is fixed the identical check belongs there.
3. A subtlety worth recording
The root SD-JWT must carry a
cnfnaming the agent key before the human signs. Otherwise the human authorises a spending authority nobody can use. The same defect is described independently inopenmobilehub/credentagentPR #189:Both verifiers check this and reject a root without
cnf.jwk.4. Verification
Both verifiers report
3/3 passed verification; cross-check reports3/3 agreements, 0 divergences.The workflow declares least-privilege permissions: it only reads a checkout and runs local code.
Scope and limits
cnfandsd_hash. It does not cover the negative vectors yet.iat/expchecks.Licence
Apache-2.0, matching this repository. Independent work; no affiliation with Google LLC.