Skip to content

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

Open
chopmob-cloud wants to merge 3 commits intogoogle-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 3 commits intogoogle-agentic-commerce:mainfrom
chopmob-cloud:fix/jcs-cart-payment-binding-v3

Conversation

@chopmob-cloud
Copy link
Copy Markdown

Summary

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

The core problem: without a cryptographic link from CartMandate to PaymentMandate, a malicious or misconfigured agent can substitute a different cart after the user has expressed intent. The fix uses RFC 8785 (JSON Canonicalization Scheme) to produce a deterministic hash that is consistent across Python, Go, and TypeScript implementations.

Note: This replaces the previously closed PR #251. The branch on #251 was inadvertently broken during a local history-cleanup pass (the force-push lost the shared ancestor with upstream/main, so GitHub auto-closed the PR). The code below is identical to what #251 carried — three commits cherry-picked clean onto the current upstream/main tip. All Gemini high-priority feedback from the earlier closed PR #241 has been incorporated upfront: model_dump(exclude_none=True) for cross-language hash consistency, f-strings throughout, and import path updated from ap2.types.mandateap2.models.mandate.

Spec (docs/ap2/specification.md)

New normative section Cart-to-Payment Mandate Binding under §Payment Mandate 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 (CP, Merchant, MPP) MUST recompute the hash and MUST reject on mismatch before releasing credentials or initiating payment.

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

Added two Optional fields to PaymentMandateContents:

  • cart_mandate_id — reference to the bound CartMandate.
  • cart_mandate_hashhex(sha256(JCS(CartMandate))).

Both are Optional for backward compatibility; new mandates SHOULD populate both.

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

New helper module containing:

  • validate_payment_mandate_signature() — placeholder for sd-jwt-vc key-binding.
  • validate_cart_mandate_hash() — recomputes and compares the JCS hash. Uses model_dump(exclude_none=True) for cross-language consistency (matches Go omitempty); raises ValueError on mismatch; skips with a warning if cart_mandate_hash is absent (backward-compatible rollout).

Dependency (code/samples/python/pyproject.toml)

Added rfc8785>=0.1.2.

Test plan

  • validate_cart_mandate_hash() returns without error when hash matches.
  • validate_cart_mandate_hash() raises ValueError when the hash does not match.
  • validate_cart_mandate_hash() logs a warning and returns cleanly when cart_mandate_hash is None (backward compat).
  • PaymentMandateContents with both new fields round-trips through Pydantic serialisation without error.

Addresses the CartMandate <> PaymentMandate binding gap raised in google-agentic-commerce#211.

## Spec (docs/ap2/specification.md)

Added section "Cart-to-Payment Mandate Binding" under Payment Mandate
with three normative requirements:

1. PaymentMandateContents MUST include cart_mandate_id and
   cart_mandate_hash = hex(sha256(JCS(CartMandate))) per RFC 8785.
   JCS eliminates cross-language float-serialisation ambiguity
   (Python: 120.0, Go: 120 — different bytes without canonicalisation).
2. cart_mandate_hash MUST be computed with null/None optional fields
   excluded so Python and Go (omitempty) produce the same canonical form.
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)

Added two Optional fields to PaymentMandateContents:
- cart_mandate_id — reference to the bound CartMandate.
- cart_mandate_hash — sha256(RFC 8785 canonical form of CartMandate).

Both Optional for backward compatibility; new mandates SHOULD populate both.

## Sample validation (code/samples/python/src/common/validation.py)

New helper module with:
- validate_payment_mandate_signature() — placeholder for sd-jwt-vc
  key-binding verification (unchanged from prior design).
- validate_cart_mandate_hash() — recomputes and compares the JCS hash.
  Uses model_dump(exclude_none=True) so None-valued optional fields are
  omitted, matching Go omitempty and ensuring cross-language consistency
  (high-priority Gemini feedback on the earlier closed PR google-agentic-commerce#241).
  Uses f-strings throughout (low-priority Gemini feedback).

## Dependency (code/samples/python/pyproject.toml)

Added rfc8785>=0.1.2 for RFC 8785 JSON canonicalisation.
Addresses cspell and markdownlint failures in CI:

- validation.py: serialised→serialized, behaviour→behavior,
  authorised→authorized (cspell uses American English dictionary)
- specification.md: same spelling fixes + serialisation→serialization,
  canonicalisation→canonicalization, authorises→authorizes
- specification.md: fix pre-existing MD030 violations (3 spaces after
  list markers → 1 space) exposed by touching the file
- .cspell/custom-words.txt: add fastmcp, omitempty, rfc8785
  (pre-existing package name and Go struct tag used in pyproject.toml
  and the new validation comments)
[Agent Authorization Framework][agent_authorization.md] used a
reference-style label with no corresponding link definition, triggering
markdownlint MD052. Convert to an equivalent inline link.

Pre-existing issue exposed by touching the file.
@chopmob-cloud chopmob-cloud requested a review from a team as a code owner May 1, 2026 13:47
Copy link
Copy Markdown
Contributor

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

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.

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