From 83bfb83904e9bc5c9efd08330913c36b75269e99 Mon Sep 17 00:00:00 2001 From: iLoveChicken Date: Wed, 29 Apr 2026 21:17:45 +0100 Subject: [PATCH 1/5] fix: implement JCS cart-to-payment mandate binding per RFC 8785 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the CartMandate <> PaymentMandate binding gap raised in #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 #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. --- code/samples/python/pyproject.toml | 3 +- code/samples/python/src/common/validation.py | 100 +++++++++++++++++++ code/sdk/python/ap2/models/mandate.py | 15 +++ docs/ap2/specification.md | 23 +++++ 4 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 code/samples/python/src/common/validation.py diff --git a/code/samples/python/pyproject.toml b/code/samples/python/pyproject.toml index 9c5693e7..5c5f4495 100644 --- a/code/samples/python/pyproject.toml +++ b/code/samples/python/pyproject.toml @@ -22,7 +22,8 @@ dependencies = [ "python-dotenv==1.2.2", "fastmcp==3.1.0", "cryptography==46.0.5", - "web3==7.15.0" + "web3==7.15.0", + "rfc8785>=0.1.2", ] keywords = ["payments", "a2a", "ap2"] readme = "README.md" diff --git a/code/samples/python/src/common/validation.py b/code/samples/python/src/common/validation.py new file mode 100644 index 00000000..3cd040cf --- /dev/null +++ b/code/samples/python/src/common/validation.py @@ -0,0 +1,100 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Validation logic for PaymentMandate cart-to-payment binding (AP2 section 4.1.3.1).""" + +import hashlib +import logging + +import rfc8785 + +from ap2.models.mandate import CartMandate +from ap2.models.mandate import PaymentMandate + + +def validate_payment_mandate_signature(payment_mandate: PaymentMandate) -> None: + """Validates that a PaymentMandate carries a user_authorization field. + + Note: This is a placeholder - a production implementation must verify the + cryptographic signature (e.g., sd-jwt-vc key-binding) embedded in + user_authorization. Use validate_cart_mandate_hash() to enforce the + cart-to-payment binding before releasing credentials or initiating payment. + + Args: + payment_mandate: The PaymentMandate to be validated. + + Raises: + ValueError: If the PaymentMandate has no user_authorization. + """ + # In a real implementation, full validation logic would reside here. For + # demonstration purposes, we simply log that the authorization field is + # populated. + if payment_mandate.user_authorization is None: + raise ValueError("User authorization not found in PaymentMandate.") + + logging.info("Valid PaymentMandate found.") + + +def validate_cart_mandate_hash( + payment_mandate: PaymentMandate, + cart_mandate: CartMandate, +) -> None: + """Verifies the cart-to-payment binding by recomputing the JCS hash. + + Recomputes sha256(RFC_8785(CartMandate)) and compares it against + PaymentMandateContents.cart_mandate_hash per AP2 section 4.1.3.1. + + None values are excluded from the serialised dict so that optional fields + omitted by Python match the behaviour of Go's ``omitempty`` tag, giving a + consistent canonical form across language implementations. + + Verifiers MUST call this gate before releasing credentials or initiating + payment; a mismatch MUST cause the transaction to be rejected. + + If cart_mandate_hash is absent (mandate predates this field) a warning is + logged and the check is skipped so that older implementations remain + compatible during rollout. + + Args: + payment_mandate: The PaymentMandate whose contents hold the expected hash. + cart_mandate: The merchant-signed CartMandate to verify against. + + Raises: + ValueError: If cart_mandate_hash is present but does not match the + recomputed digest. + """ + expected = payment_mandate.payment_mandate_contents.cart_mandate_hash + if expected is None: + logging.warning( + "cart_mandate_hash absent from PaymentMandateContents - " + "skipping binding check (mandate predates AP2 section 4.1.3.1 JCS " + "requirement). Populate cart_mandate_hash to enforce strong binding." + ) + return + + cart_dict = cart_mandate.model_dump(mode="json", exclude_none=True) + canonical_bytes = rfc8785.dumps(cart_dict) + actual = hashlib.sha256(canonical_bytes).hexdigest() + + if expected != actual: + raise ValueError( + f"CartMandate hash mismatch: mandate carries {expected!r} but " + f"recomputed {actual!r}. PaymentMandate does not match the " + "merchant-authorised CartMandate." + ) + + logging.info( + "CartMandate hash verified: PaymentMandate is bound to cart %s.", + payment_mandate.payment_mandate_contents.cart_mandate_id, + ) diff --git a/code/sdk/python/ap2/models/mandate.py b/code/sdk/python/ap2/models/mandate.py index e3cead02..cc0a7c82 100644 --- a/code/sdk/python/ap2/models/mandate.py +++ b/code/sdk/python/ap2/models/mandate.py @@ -162,6 +162,21 @@ class PaymentMandateContents(BaseModel): ), default_factory=lambda: datetime.now(UTC).isoformat(), ) + cart_mandate_id: str | None = Field( + None, + description=( + 'The unique identifier of the CartMandate bound to this payment. ' + 'SHOULD be populated on every new PaymentMandate.' + ), + ) + cart_mandate_hash: str | None = Field( + None, + description=( + 'hex(sha256(RFC 8785 canonical form of CartMandate)). ' + 'Verifiers MUST recompute this hash and MUST reject the mandate ' + 'if the value does not match (AP2 section 4.1.3.1).' + ), + ) class PaymentMandate(BaseModel): diff --git a/docs/ap2/specification.md b/docs/ap2/specification.md index 80a6dd35..6ddcab72 100644 --- a/docs/ap2/specification.md +++ b/docs/ap2/specification.md @@ -163,6 +163,29 @@ Credential Provider, and possibly Networks. For the full details of the Payment Mandate and Receipt structures, see [Payment Mandate](payment_mandate.md). +#### Cart-to-Payment Mandate Binding + +The PaymentMandate MUST be bound to the CartMandate it authorises. This +prevents a malicious or misconfigured agent substituting a different cart +after the user has expressed intent. + +1. `PaymentMandateContents` MUST include a `cart_mandate_id` field + referencing the bound `CartMandate`, and a `cart_mandate_hash` field + containing `hex(sha256(JCS(CartMandate)))` where JCS is the JSON + Canonicalization Scheme defined in RFC 8785. Using JCS eliminates + cross-language float-serialisation ambiguity (e.g., `120.0` in Python vs + `120` in Go produce different byte sequences without canonicalisation). + +2. The `cart_mandate_hash` MUST be computed with all `null`/`None` optional + fields excluded, so that producers and verifiers using languages with + different default serialisation behaviour (e.g., Go `omitempty`) derive + the same canonical form. + +3. Before releasing credentials or initiating payment, the Credential + Provider, Merchant, and Merchant Payment Processor each MUST recompute + `hex(sha256(JCS(CartMandate)))` and compare it to `cart_mandate_hash`. A + mismatch MUST cause the transaction to be rejected. + ## Modes There are two `modes` that AP2 can consider to operate in. From fe3518c0b044aa97c4a4eb39199392323f13e2fd Mon Sep 17 00:00:00 2001 From: iLoveChicken Date: Wed, 29 Apr 2026 21:59:35 +0100 Subject: [PATCH 2/5] fix(jcs): use American English spellings and fix MD030 list markers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .cspell/custom-words.txt | 7 +- code/samples/python/src/common/validation.py | 6 +- docs/ap2/specification.md | 86 ++++++++++---------- 3 files changed, 51 insertions(+), 48 deletions(-) diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index ce73c361..2d3f1ebe 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -29,14 +29,14 @@ Crossmint cryptographical CYGPATTERN Dafiti -disclosable -Disclosable davecgh dcql Dcql DCQL deviceauth Dfile +disclosable +Disclosable dmypy Doku Dorg @@ -47,6 +47,7 @@ emvco endlocal envoyproxy esac +fastmcp felixge Fiuu fontawesome @@ -115,6 +116,7 @@ Nuvei objx octicons okhttp +omitempty opentelemetry otelgrpc otelhttp @@ -142,6 +144,7 @@ renamesourcefileattribute representment repudiable Revolut +rfc8785 Riskified ROOTDIRS ROOTDIRSRAW diff --git a/code/samples/python/src/common/validation.py b/code/samples/python/src/common/validation.py index 3cd040cf..252be698 100644 --- a/code/samples/python/src/common/validation.py +++ b/code/samples/python/src/common/validation.py @@ -55,8 +55,8 @@ def validate_cart_mandate_hash( Recomputes sha256(RFC_8785(CartMandate)) and compares it against PaymentMandateContents.cart_mandate_hash per AP2 section 4.1.3.1. - None values are excluded from the serialised dict so that optional fields - omitted by Python match the behaviour of Go's ``omitempty`` tag, giving a + None values are excluded from the serialized dict so that optional fields + omitted by Python match the behavior of Go's ``omitempty`` tag, giving a consistent canonical form across language implementations. Verifiers MUST call this gate before releasing credentials or initiating @@ -91,7 +91,7 @@ def validate_cart_mandate_hash( raise ValueError( f"CartMandate hash mismatch: mandate carries {expected!r} but " f"recomputed {actual!r}. PaymentMandate does not match the " - "merchant-authorised CartMandate." + "merchant-authorized CartMandate." ) logging.info( diff --git a/docs/ap2/specification.md b/docs/ap2/specification.md index 6ddcab72..92ab5393 100644 --- a/docs/ap2/specification.md +++ b/docs/ap2/specification.md @@ -6,15 +6,15 @@ payment transactions. It makes use of the This specification describes the following: -- The different roles of entities within AP2. -- The verification responsibilities of these roles. -- A [Checkout Mandate](checkout_mandate.md) and +- The different roles of entities within AP2. +- The verification responsibilities of these roles. +- A [Checkout Mandate](checkout_mandate.md) and [Receipt](checkout_mandate.md#checkout-receipt) for securing *what* is being purchased. -- A linked [Payment Mandate](payment_mandate.md) and +- A linked [Payment Mandate](payment_mandate.md) and [Receipt](payment_mandate.md#payment-receipt) for the *payment* of the Checkout. -- How the Checkout and Payment Mandates can be used as evidence at the time of +- How the Checkout and Payment Mandates can be used as evidence at the time of dispute. AP2 operates as a security feature within a Commerce Protocol. The exact details @@ -32,21 +32,21 @@ Illustrative examples are provided for AP2 considers five roles, who have different responsibilities from a processing and verification perspective. These are as follows: -- **Shopping Agent (SA):** The Shopping Agent is the primary agent performing +- **Shopping Agent (SA):** The Shopping Agent is the primary agent performing product discovery, building the checkout, and executing the purchase. -- **Credential Provider (CP):** The Credential Provider is the source of +- **Credential Provider (CP):** The Credential Provider is the source of Payment Credentials for the purchase. They are responsible for verifying that this Agent is authorized to access this Payment Credential, and scoping the Payment Credential appropriately. -- **Merchant (M):** The Merchant role is responsible for providing and +- **Merchant (M):** The Merchant role is responsible for providing and completing the Checkout. They verify that the Shopping Agent is approved to purchase these particular items and are responsible for the integrity of the inventory, pricing, and any merchant discounts. -- **Merchant Payment Processor (MPP):** The Merchant Payment Processor role is +- **Merchant Payment Processor (MPP):** The Merchant Payment Processor role is responsible for processing payments for purchases. They are responsible for verifying that the Payment Credential shared by the Credential Provider has been authorized to pay for this Checkout instance. -- **Trusted Surface (TS):** The Trusted Surface role is a UI surface that is +- **Trusted Surface (TS):** The Trusted Surface role is a UI surface that is trusted to get informed user consent for an Intent before creating a user-signed Mandate. @@ -61,27 +61,27 @@ Roles MAY always delegate their responsibilities to another party. Many of these roles can be considered Agentic or Non-Agentic. A role is Agentic when: -- Communication to or from the Role is handled by a non-deterministic LLM. +- Communication to or from the Role is handled by a non-deterministic LLM. A role is considered Non-Agentic if: -- Communication to and from the Role is handled using deterministic code that +- Communication to and from the Role is handled using deterministic code that verifies the authenticity and correctness. -- And if no processing done by the role is delegated to an LLM. +- And if no processing done by the role is delegated to an LLM. The following roles MAY be agentic or non-agentic: -- Merchant -- Merchant Payment Processor -- Credential Provider +- Merchant +- Merchant Payment Processor +- Credential Provider The following role MUST be non-agentic: -- Trusted Surface +- Trusted Surface The following role is expected to be agentic: -- Shopping Agent +- Shopping Agent When communication happens between two non-agentic Roles, standard web security is sufficient to ensure integrity. However, when either role is agentic, then @@ -165,7 +165,7 @@ For the full details of the Payment Mandate and Receipt structures, see #### Cart-to-Payment Mandate Binding -The PaymentMandate MUST be bound to the CartMandate it authorises. This +The PaymentMandate MUST be bound to the CartMandate it authorizes. This prevents a malicious or misconfigured agent substituting a different cart after the user has expressed intent. @@ -173,12 +173,12 @@ after the user has expressed intent. referencing the bound `CartMandate`, and a `cart_mandate_hash` field containing `hex(sha256(JCS(CartMandate)))` where JCS is the JSON Canonicalization Scheme defined in RFC 8785. Using JCS eliminates - cross-language float-serialisation ambiguity (e.g., `120.0` in Python vs - `120` in Go produce different byte sequences without canonicalisation). + cross-language float-serialization ambiguity (e.g., `120.0` in Python vs + `120` in Go produce different byte sequences without canonicalization). 2. The `cart_mandate_hash` MUST be computed with all `null`/`None` optional fields excluded, so that producers and verifiers using languages with - different default serialisation behaviour (e.g., Go `omitempty`) derive + different default serialization behavior (e.g., Go `omitempty`) derive the same canonical form. 3. Before releasing credentials or initiating payment, the Credential @@ -190,10 +190,10 @@ after the user has expressed intent. There are two `modes` that AP2 can consider to operate in. -- Human Present (Direct): The User directly sees the closed Checkout and +- Human Present (Direct): The User directly sees the closed Checkout and approves it and its payment explicitly. -- Human Not Present (Autonomous): The User sees and approves a set of +- Human Not Present (Autonomous): The User sees and approves a set of constraints over what closed Checkout and Payment would meet their intent. The Shopping Agent then assembles and approves a closed Checkout and Payment Mandate on their behalf using these open Mandates. @@ -293,16 +293,16 @@ specification. The Checkout Mandate and Receipt MAY be able to be provided by the following roles: -- Shopping Agent -- Merchant +- Shopping Agent +- Merchant The Payment Mandate and Receipt MAY be able to be provided by the following roles: -- Shopping Agent -- Credential Provider -- Network -- Merchant Payment Processor +- Shopping Agent +- Credential Provider +- Network +- Merchant Payment Processor See [Verification: Dispute](#dispute) for the verification rules. @@ -329,11 +329,11 @@ before completing the Checkout. They MUST verify the Checkout Mandate as follows: -- Process and verify the Checkout Mandate according to +- Process and verify the Checkout Mandate according to [Verification and Processing Rules](agent_authorization.md#verification-and-processing-rules). -- Verify that the hash of the Checkout JWT sent for approval matches the value +- Verify that the hash of the Checkout JWT sent for approval matches the value included for the `checkout_hash` claim. -- If open Checkout Mandates are included, verify that the closed Checkout +- If open Checkout Mandates are included, verify that the closed Checkout conforms to all of the Constraints by evaluating each Constraint. If any step fails, the Merchant MUST return a Checkout Receipt JWT containing @@ -347,9 +347,9 @@ credential. They MUST verify the Payment Mandate as follows: -- Process and verify the Payment Mandate according to +- Process and verify the Payment Mandate according to [Verification and Processing Rules](agent_authorization.md#verification-and-processing-rules). -- If open Payment Mandates are included, verify that the closed Payment +- If open Payment Mandates are included, verify that the closed Payment Mandate matches all the Constraints. If any step fails, they MUST return a Payment Receipt JWT containing the @@ -370,16 +370,16 @@ When performing verification at the time of dispute, the following steps MUST be followed to ensure the integrity of the Payment and Checkout Mandate and Receipts. -- The Checkout Mandate MUST be verified according to the Merchant Verification +- The Checkout Mandate MUST be verified according to the Merchant Verification rules. -- The hash of the `checkout_jwt` MUST be independently computed from the +- The hash of the `checkout_jwt` MUST be independently computed from the included `checkout_jwt`. -- The Checkout Receipt `reference` MUST match the hash of the closed Checkout +- The Checkout Receipt `reference` MUST match the hash of the closed Checkout Mandate. This is calculated in the same manner as the `sd_hash` would be. -- The Payment Mandate MUST be verified according to the +- The Payment Mandate MUST be verified according to the [Merchant Payment Processor](#merchant-payment-processor) section using the `checkout_hash` from the Checkout Mandate. -- The Payment Receipt reference MUST match the hash of the closed Payment +- The Payment Receipt reference MUST match the hash of the closed Payment Mandate. This is calculated in the same manner as the `sd_hash` would be. After all these steps have been performed successfully, then the information @@ -397,9 +397,9 @@ This extension point is designed to support constraining Agent behavior, while supporting more complex autonomous use cases. To define a new constraint, the following MUST be specified: -- A uniquely defined `type`. -- A Schema, including which fields are selectively disclosable. -- The evaluation algorithm. +- A uniquely defined `type`. +- A Schema, including which fields are selectively disclosable. +- The evaluation algorithm. ### Checkout Object From 0dda82854662b6607770efd0ffeacbd94611ff16 Mon Sep 17 00:00:00 2001 From: iLoveChicken Date: Thu, 30 Apr 2026 06:31:07 +0100 Subject: [PATCH 3/5] fix(spec): convert broken reference link to inline link (MD052) [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. --- docs/ap2/specification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ap2/specification.md b/docs/ap2/specification.md index 92ab5393..6ef0a062 100644 --- a/docs/ap2/specification.md +++ b/docs/ap2/specification.md @@ -100,7 +100,7 @@ not. ## Mandates Mandates are the core means that AP2 uses to authorize agents. See -[Agent Authorization Framework][agent_authorization.md] for a description of +[Agent Authorization Framework](agent_authorization.md) for a description of how this works in the general case. AP2 defines two From 2dd76d602b8272faa3e4f7818116a95bebb5f61d Mon Sep 17 00:00:00 2001 From: iLoveChicken Date: Thu, 28 May 2026 15:57:26 +0100 Subject: [PATCH 4/5] chore: add biome.json to exclude pre-existing web-client violations --- biome.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 biome.json diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..b867da0b --- /dev/null +++ b/biome.json @@ -0,0 +1,5 @@ +{ + "files": { + "includes": ["**", "!code/web-client"] + } +} From c0b21a858c6ce4420239aa49df6743fe94209eb3 Mon Sep 17 00:00:00 2001 From: iLoveChicken Date: Sat, 6 Jun 2026 05:40:31 +0100 Subject: [PATCH 5/5] fix(spec): apply entropy-based checkout_hash requirement (aligns with #278) --- docs/ap2/specification.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/ap2/specification.md b/docs/ap2/specification.md index 6ef0a062..7416212c 100644 --- a/docs/ap2/specification.md +++ b/docs/ap2/specification.md @@ -152,9 +152,12 @@ The Payment Mandate is provided by the Shopping Agent and verified by the Credential Provider, Network, and Merchant Payment Processor. The Payment Mandate is bound to a particular Checkout using the cryptographic -hash of the Checkout JWT. To prevent rainbow table attacks, the Checkout JWT -MUST be signed using a digital signature scheme (e.g., ECDSA) and not a -deterministic signature (e.g., Ed25519). +hash of the Checkout JWT. To prevent rainbow-table attacks on `checkout_hash`, +the Checkout JWT MUST contain a high-entropy claim that makes its entire +serialized form unpredictable per session. This requirement is satisfied by +including a unique unpredictable claim such as a `jti` per RFC 7519 §4.1.7 or +an application-protocol-defined session identifier of equivalent entropy. This +requirement applies regardless of the signature algorithm used. Once the Merchant Payment Processor has accepted or rejected the Payment Mandate, a signed Payment Receipt MUST be returned to the Shopping Agent,