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/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"] + } +} 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..252be698 --- /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 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 + 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-authorized 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..7416212c 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 @@ -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 @@ -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, @@ -163,14 +166,37 @@ 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 authorizes. 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-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 serialization behavior (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. -- 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. @@ -270,16 +296,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. @@ -306,11 +332,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 @@ -324,9 +350,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 @@ -347,16 +373,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 @@ -374,9 +400,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