Skip to content

fix: implement JCS cart-to-payment mandate binding per RFC 8785 - #253

Open
chopmob-cloud wants to merge 1 commit into
google-agentic-commerce:mainfrom
chopmob-cloud:fix/jcs-cart-payment-binding-v3
Open

fix: implement JCS cart-to-payment mandate binding per RFC 8785#253
chopmob-cloud wants to merge 1 commit into
google-agentic-commerce:mainfrom
chopmob-cloud:fix/jcs-cart-payment-binding-v3

Conversation

@chopmob-cloud

@chopmob-cloud chopmob-cloud commented May 1, 2026

Copy link
Copy Markdown

Summary

Closes the CartMandate / PaymentMandate binding gap raised in #211.

Without a cryptographic link from CartMandate to PaymentMandate, a malicious or misconfigured agent can substitute a different cart after the user has expressed intent. This binds them with a deterministic RFC 8785 (JCS) hash that is stable across implementations.

Spec (docs/ap2/specification.md)

New normative section, Cart-to-Payment Mandate Binding, under the Payment Mandate section, with three requirements:

  1. PaymentMandateContents MUST include cart_mandate_id and cart_mandate_hash = hex(sha256(JCS(CartMandate))).
  2. JCS serialisation MUST exclude null / None optional fields, so Python and Go (omitempty) produce identical canonical bytes.
  3. Verifiers MUST recompute the hash and MUST reject on mismatch before releasing credentials or initiating payment.

Types (code/sdk/python/ap2/models/mandate.py)

Adds two optional fields to PaymentMandateContents: cart_mandate_id and cart_mandate_hash. Both are optional for backward compatibility; new mandates SHOULD populate them.

Verifier (code/samples/python/src/common/validation.py)

validate_cart_mandate_hash() recomputes and compares the hash over the raw received CartMandate JSON, taken before schema parsing. Hashing a re-serialised model is not sufficient: parsers silently drop unknown or extension fields and can collapse an explicit null with an absent field, so tampering outside the model schema would escape a model-derived hash. The verifier is strict by default: a PaymentMandate that omits cart_mandate_hash is rejected. An explicit keyword-only allow_unbound_cart=True opt-out is reserved for legacy mandates during rollout, and it never weakens verification of a present hash.

Dependency

Adds rfc8785>=0.1.2 to the samples.

Test plan

  • Valid binding passes, with an extension field included in the hash.
  • A tampered model field is rejected (hash mismatch).
  • A tampered extension field is rejected (escapes a model-derived hash, caught by hashing raw bytes).
  • An injected unknown field is rejected.
  • An absent cart_mandate_hash is rejected by default.
  • An absent hash is skipped only with explicit allow_unbound_cart=True, and that opt-out does not weaken a present hash.
  • An explicit null differs from an absent field.

All eight tests pass (code/samples/python/tests/validation_tests.py).

@chopmob-cloud
chopmob-cloud requested a review from a team as a code owner May 1, 2026 13:47

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a cart-to-payment mandate binding mechanism to ensure the integrity of the payment process by linking a PaymentMandate to a specific CartMandate. Key changes include the addition of cart_mandate_id and cart_mandate_hash fields to the PaymentMandateContents model, the introduction of a validation module using RFC 8785 (JCS) for canonicalization and SHA-256 for hashing, and updates to the AP2 specification documentation. I have no feedback to provide.

@chopmob-cloud

Copy link
Copy Markdown
Author

Thanks @gemini-code-assist. Noted that the automated review found no issues. Flagging for @GarethCOliver or any AP2 maintainer when you have a cycle -- this is the cart-integrity binding gap from #211 and is independently useful alongside the Biome lint fix in #249.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Thanks for the update, @chopmob-cloud. I've reviewed the implementation of the cart-to-payment mandate binding. The use of RFC 8785 for canonicalization is the correct approach to ensure cross-language consistency, and the model_dump(exclude_none=True) approach effectively aligns with Go's omitempty behavior. The documentation updates and the validation helper in code/samples/python/src/common/validation.py are clear and correctly implement the requirements specified in the PR description. This looks solid.

@chopmob-cloud

Copy link
Copy Markdown
Author

The Biome lint failures are pre-existing in code/web-client/ -- none are in files this PR touches (code/sdk/python/, code/samples/python/, docs/). PR #249 resolves all 22 errors. Flagging for @GarethCOliver -- Gemini found no issues with the JCS approach, and the cart_mandate_hash convention has already been adopted normatively in AP2-COMPOSITION.md by PEAC Protocol. Happy to rebase once #249 is merged, or both can land together as they are non-conflicting.

@chopmob-cloud
chopmob-cloud force-pushed the fix/jcs-cart-payment-binding-v3 branch 4 times, most recently from a1b73d4 to d1cb27c Compare August 10, 2026 22:16
…oogle-agentic-commerce#211)

Adds cart_mandate_id and cart_mandate_hash = hex(sha256(JCS(CartMandate)))
to PaymentMandateContents, and a strict verifier that recomputes the hash
over the raw received CartMandate bytes and rejects on mismatch, absent
hash, or tampered/injected fields. Fail-closed by default; an explicit
allow_unbound_cart opt-out is reserved for legacy mandates and never
weakens a present hash.

Signed-off-by: AlgoVoi <chopmob@gmail.com>
@chopmob-cloud
chopmob-cloud force-pushed the fix/jcs-cart-payment-binding-v3 branch from d1cb27c to 23a8847 Compare August 10, 2026 22:19
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