Repository navigation
Conversation
|
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. |
69c3715 to
defbf69
Compare
Adds contrib/mandate-corpus/ with signed AP2 mandate fixtures and a
portable reference verifier. No SDK source file is modified.
The repository contains no fixture, corpus, vector or conformance
directory:
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 is a blocker rather than a nicety, 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. Python is therefore the only language that can
verify an AP2 mandate, and other implementations have nothing to test
against.
Adds:
- tools/generate_fixtures.py - reproducible signed fixture generator
- tools/verify_fixtures.py - reference verifier, portable to Go/TS/PHP
- tools/negative_vectors.py - negative vectors plus a positive control
- tools/poc_aud_not_enforced.py - reproducer for the defect below
- vectors/ - three signed vectors plus index.json
- .github/workflows/mandate-corpus.yml - runs vectors on SDK changes
Vectors: card-basic (100 USD), debit-large (50 000 minor units) and
non-USD (BRL), the last catching implementations that silently normalise
every currency to dollars.
One AP2 subtlety is recorded deliberately: the root must carry a cnf
naming the agent key before the human signs, otherwise the mandate is
signed but unenforceable. The same defect is described independently in
openmobilehub/credentagent PR google-agentic-commerce#189. The generator sets
DisclosureMetadata(sd_keys=[]) explicitly, because from_model otherwise
hides the whole payload under _sd.
Defect found, 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 therefore never runs and no warning is
emitted. Reproducer:
python contrib/mandate-corpus/tools/poc_aud_not_enforced.py
Fixing it turns the corresponding negative vector from xfail to passing.
cspell gains only the identifiers this corpus introduces; the signed
vector payloads are excluded from spelling as they are base64.
Apache-2.0, matching the repository.
defbf69 to
d22128c
Compare
pyproject.toml lives at the root; code/sdk/python is declared through [tool.setuptools.packages.find] where = ["code/sdk/python"]. Installing from the subdirectory fails because it contains neither setup.py nor pyproject.toml, which is what the workflow log showed. Verified locally: pip install -e . from the root imports ap2 cleanly.
|
Both actions in |
|
Thanks for the CI setup. The only failing check is All other checks are green: CLA, zizmor (actions pinned to full SHAs + least-privilege Happy to rebase once #382 lands. If repo-wide Biome is out of scope for this PR, merging #382 first would clear the last red check on our side. |
Summary
Adds signed AP2 mandate fixtures plus a portable reference verifier, under
contrib/mandate-corpus/. No existing file is modified.This repository currently has no
fixture,corpus,vectororconformancedirectory:The Python SDK can sign mandates (
sd_jwt.create,kb_sd_jwt.create), but no signed fixtures are published. Consequently the positive test inUniversal-Commerce-Protocol/conformanceis skipped — PR #115 there states:This matters for implementations in other languages — the Go sample (
code/samples/go), TypeScript, andagentic-commerce-alliance/ucp-php-sdk. They need a byte-level reference to conform against, not somebody else's test code.Contents
tools/generate_fixtures.pytools/verify_fixtures.pytools/negative_vectors.pytools/poc_aud_not_enforced.pyvectors/index.jsonVectors:
card-basic(100 USD),debit-large(50 000 minor units), andnonusd(BRL) — the last catches implementations that silently normalise every currency to dollars.2. Defect found:
expected_audis not enforcedImpact: audience check bypass. Severity: medium. Reported here 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 explicitly passedexpected_aud. There is no warning.Reproduction:
A merchant relying on the audience check gets silent protection that does not exist. Substituting
audpasses without refusal.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 reproducer is self-contained and runs against any revision, so it will report green once fixed. In
negative_vectors.pythat case is markedxfailfor exactly this reason — a fix turns it green instead of making it vanish.3. One 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 class of defect was described independently inopenmobilehub/credentagentPR #189:The generator sets
DisclosureMetadata(sd_keys=[])explicitly. Without itDisclosureMetadata.from_modelhides the whole payload under_sd,cnfbecomes unreadable, and the mandate is signed but unenforceable.4. Verification
Run from the repository root:
Actual output of
verify_fixtures.py:Scripts resolve
vectors/relative to their own location, with a parent-level fallback, so the directory can be lifted out of this repository and run against another implementation unchanged.Scope and limits
Licence
Apache-2.0, matching this repository. Independent work; no affiliation with Google LLC.