Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .cspell/custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ Crossmint
cryptographical
CYGPATTERN
Dafiti
disclosable
Disclosable
davecgh
dcql
Dcql
DCQL
deviceauth
Dfile
disclosable
Disclosable
dmypy
Doku
Dorg
Expand All @@ -47,6 +47,7 @@ emvco
endlocal
envoyproxy
esac
fastmcp
felixge
Fiuu
fontawesome
Expand Down Expand Up @@ -115,6 +116,7 @@ Nuvei
objx
octicons
okhttp
omitempty
opentelemetry
otelgrpc
otelhttp
Expand Down Expand Up @@ -142,6 +144,7 @@ renamesourcefileattribute
representment
repudiable
Revolut
rfc8785
Riskified
ROOTDIRS
ROOTDIRSRAW
Expand Down
3 changes: 2 additions & 1 deletion code/samples/python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
100 changes: 100 additions & 0 deletions code/samples/python/src/common/validation.py
Original file line number Diff line number Diff line change
@@ -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,
)
15 changes: 15 additions & 0 deletions code/sdk/python/ap2/models/mandate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
103 changes: 63 additions & 40 deletions docs/ap2/specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -163,14 +163,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.
Expand Down Expand Up @@ -270,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.

Expand All @@ -306,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
Expand All @@ -324,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
Expand All @@ -347,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
Expand All @@ -374,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

Expand Down
Loading