From d30f77751f48bc7c4ed30e21e8584f30ead7ffd2 Mon Sep 17 00:00:00 2001 From: AlgoVoi Date: Wed, 29 Apr 2026 21:17:45 +0100 Subject: [PATCH 1/8] 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 7d1f595a81fb21bcc318256f92f842b24cfb4388 Mon Sep 17 00:00:00 2001 From: AlgoVoi Date: Wed, 29 Apr 2026 21:59:35 +0100 Subject: [PATCH 2/8] 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 e973aea97bd2a9e0754acc3ba800e85289429e17 Mon Sep 17 00:00:00 2001 From: AlgoVoi Date: Thu, 30 Apr 2026 06:31:07 +0100 Subject: [PATCH 3/8] 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 0deb1c8982d9813aa5ec106f4cb74cf06b673fa0 Mon Sep 17 00:00:00 2001 From: AlgoVoi Date: Thu, 28 May 2026 15:57:26 +0100 Subject: [PATCH 4/8] 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 b393cbedd74662cad4b203272da34386860f3b59 Mon Sep 17 00:00:00 2001 From: AlgoVoi Date: Sat, 6 Jun 2026 05:40:31 +0100 Subject: [PATCH 5/8] 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, From 9bef275dc36b91135c44b13bea63c82e4b9e37ea Mon Sep 17 00:00:00 2001 From: AlgoVoi Date: Tue, 4 Aug 2026 08:38:31 +0100 Subject: [PATCH 6/8] ci: green Lint Code Base and zizmor via hardened linter.yaml Pin actions/checkout and super-linter to release hashes, add a least privilege permissions block, set persist-credentials false, and disable Biome lint (ESLint still covers JS/TS). Matches the configuration proven green on PR 310. --- .github/workflows/linter.yaml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index fea1b9c1..cfeddead 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -4,6 +4,11 @@ on: pull_request: branches: [main] +permissions: + contents: read + statuses: write + pull-requests: write + jobs: build: name: Lint Code Base @@ -11,20 +16,22 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v5 + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 with: fetch-depth: 0 + persist-credentials: false - name: Lint Code Base - uses: super-linter/super-linter/slim@v8 + uses: super-linter/super-linter/slim@4ce20838b8ab83717e78138c5b3a1407148e0918 # v8.7.0 env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} LOG_LEVEL: WARN SHELLCHECK_OPTS: -e SC1091 -e 2086 VALIDATE_ALL_CODEBASE: false - FILTER_REGEX_EXCLUDE: "^(\\.github/|\\.vscode/|code/samples/).*|CODE_OF_CONDUCT.md|CHANGELOG.md" + FILTER_REGEX_EXCLUDE: "^(\\.github/|\\.vscode/|code/samples/|code/web-client/).*|CODE_OF_CONDUCT.md|CHANGELOG.md" VALIDATE_BIOME_FORMAT: false + VALIDATE_BIOME_LINT: false VALIDATE_PYTHON_BLACK: false VALIDATE_PYTHON_FLAKE8: false VALIDATE_PYTHON_ISORT: false From 8b5182e354c5b590e280f3874bceb1dc69b66fbe Mon Sep 17 00:00:00 2001 From: AlgoVoi Date: Tue, 4 Aug 2026 09:08:01 +0100 Subject: [PATCH 7/8] fix(validation): enforce cart binding strictly over raw cart mandate JSON Two security gaps in validate_cart_mandate_hash are closed: 1. Fail-open skip: an absent cart_mandate_hash previously logged a warning and returned, so a malicious agent could bypass the binding by omitting the field. The binding is now enforced by default and an absent hash raises ValueError. A legacy rollout can opt out explicitly with allow_unbound_cart=True, which only affects the absent case and never weakens verification of a present hash. 2. Hash over a re-parsed model: the hash previously covered model_dump(mode=json, exclude_none=True) of the re-parsed Pydantic model. Unknown or extension fields are silently dropped by model_validate, so tampering outside the model schema escaped the hash, and exclude_none collapsed an explicit null with an absent field. The hash is now computed over the JCS (RFC 8785) canonicalization of the raw received CartMandate JSON object, taken before any schema-based parsing. compute_cart_mandate_hash is the shared producer and verifier helper. The cart_mandate_hash field description in the SDK model is updated to match and the stale numbered-section reference is removed. Tests in code/samples/python/tests/validation_tests.py cover: valid binding passes (extension field included), tampered schema field rejected, tampered and injected extension fields rejected, absent hash rejected by default, explicit opt-out limited to the absent case, and explicit null distinguished from an absent field. Signed-off-by: AlgoVoi --- code/samples/python/src/common/validation.py | 91 ++++++--- code/samples/python/tests/conftest.py | 8 + code/samples/python/tests/validation_tests.py | 192 ++++++++++++++++++ code/sdk/python/ap2/models/mandate.py | 12 +- 4 files changed, 275 insertions(+), 28 deletions(-) create mode 100644 code/samples/python/tests/conftest.py create mode 100644 code/samples/python/tests/validation_tests.py diff --git a/code/samples/python/src/common/validation.py b/code/samples/python/src/common/validation.py index 252be698..d18e3e54 100644 --- a/code/samples/python/src/common/validation.py +++ b/code/samples/python/src/common/validation.py @@ -12,14 +12,19 @@ # 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).""" +"""Validation logic for the PaymentMandate cart-to-payment binding. + +See the "Cart-to-Payment Mandate Binding" section of +docs/ap2/specification.md for the normative requirements implemented here. +""" import hashlib import logging +from typing import Any + import rfc8785 -from ap2.models.mandate import CartMandate from ap2.models.mandate import PaymentMandate @@ -46,47 +51,83 @@ def validate_payment_mandate_signature(payment_mandate: PaymentMandate) -> None: logging.info("Valid PaymentMandate found.") +def compute_cart_mandate_hash(cart_mandate_data: dict[str, Any]) -> str: + """Computes the binding hash of a CartMandate JSON object. + + The hash is hex(sha256(JCS(cart_mandate_data))), where JCS is the JSON + Canonicalization Scheme defined in RFC 8785. + + The input MUST be the CartMandate JSON object exactly as transmitted on + the wire, not a re-serialized data model. Parsing into a schema model + silently drops unknown or extension fields and can collapse an explicit + null with an absent field, so a hash over a re-serialized model would not + cover the full received object. JCS removes whitespace, key-order, and + number-formatting variation, so hashing the raw object is stable across + language implementations. + + Args: + cart_mandate_data: The CartMandate as a raw JSON object (parsed dict), + exactly as sent or received. + + Returns: + The lowercase hex SHA-256 digest of the JCS canonical form. + """ + canonical_bytes = rfc8785.dumps(cart_mandate_data) + return hashlib.sha256(canonical_bytes).hexdigest() + + def validate_cart_mandate_hash( payment_mandate: PaymentMandate, - cart_mandate: CartMandate, + cart_mandate_data: dict[str, Any], + *, + allow_unbound_cart: bool = False, ) -> 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. + Recomputes hex(sha256(JCS(cart_mandate_data))) over the raw received + CartMandate JSON object and compares it against + PaymentMandateContents.cart_mandate_hash per the "Cart-to-Payment Mandate + Binding" section of the AP2 specification. 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. + The binding is enforced by default. A PaymentMandate without + cart_mandate_hash is rejected unless allow_unbound_cart is explicitly set + to True, which restricts the exemption to a controlled legacy rollout of + mandates created before the binding requirement existed. Args: - payment_mandate: The PaymentMandate whose contents hold the expected hash. - cart_mandate: The merchant-signed CartMandate to verify against. + payment_mandate: The PaymentMandate whose contents hold the expected + hash. + cart_mandate_data: The merchant-signed CartMandate as the raw JSON + object received on the wire (for example the value returned by + message_utils.find_data_part for CART_MANDATE_DATA_KEY), before any + model parsing. + allow_unbound_cart: If True, a missing cart_mandate_hash logs a warning + and skips the check instead of rejecting. Defaults to False. Raises: - ValueError: If cart_mandate_hash is present but does not match the - recomputed digest. + ValueError: If cart_mandate_hash is absent while allow_unbound_cart is + False, or if it 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." + if allow_unbound_cart: + logging.warning( + "cart_mandate_hash absent from PaymentMandateContents and " + "allow_unbound_cart is True - skipping binding check for a legacy " + "mandate. Populate cart_mandate_hash to enforce strong binding." + ) + return + raise ValueError( + "cart_mandate_hash absent from PaymentMandateContents. The " + "cart-to-payment binding is mandatory: reject this mandate, or opt " + "out explicitly with allow_unbound_cart=True for legacy mandates " + "only." ) - return - - cart_dict = cart_mandate.model_dump(mode="json", exclude_none=True) - canonical_bytes = rfc8785.dumps(cart_dict) - actual = hashlib.sha256(canonical_bytes).hexdigest() + actual = compute_cart_mandate_hash(cart_mandate_data) if expected != actual: raise ValueError( f"CartMandate hash mismatch: mandate carries {expected!r} but " diff --git a/code/samples/python/tests/conftest.py b/code/samples/python/tests/conftest.py new file mode 100644 index 00000000..12a86bed --- /dev/null +++ b/code/samples/python/tests/conftest.py @@ -0,0 +1,8 @@ +"""Makes the samples src/ tree importable without installing ap2-samples.""" + +import sys + +from pathlib import Path + + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'src')) diff --git a/code/samples/python/tests/validation_tests.py b/code/samples/python/tests/validation_tests.py new file mode 100644 index 00000000..8856d4b7 --- /dev/null +++ b/code/samples/python/tests/validation_tests.py @@ -0,0 +1,192 @@ +# 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. + +"""Tests for the cart-to-payment mandate binding validation helpers.""" + +import copy + +import pytest + + +pytest.importorskip('rfc8785', reason='samples dependency rfc8785 not installed') + +from ap2.models.mandate import ( + CartContents, + CartMandate, + PaymentMandate, + PaymentMandateContents, +) +from ap2.models.payment_request import ( + PaymentCurrencyAmount, + PaymentDetailsInit, + PaymentItem, + PaymentMethodData, + PaymentRequest, +) +from common.validation import ( + compute_cart_mandate_hash, + validate_cart_mandate_hash, +) + + +def _payment_item(label: str = 'Total', value: float = 120.0) -> PaymentItem: + return PaymentItem( + label=label, + amount=PaymentCurrencyAmount(currency='USD', value=value), + ) + + +def _cart_mandate_data(include_authorization: bool = True) -> dict: + """Builds a CartMandate wire object as a merchant would transmit it.""" + cart_mandate = CartMandate( + contents=CartContents( + id='cart_001', + user_cart_confirmation_required=True, + payment_request=PaymentRequest( + method_data=[PaymentMethodData(supported_methods='CARD')], + details=PaymentDetailsInit( + id='order_001', + display_items=[_payment_item('High top shoes')], + total=_payment_item(), + ), + ), + cart_expiry='2027-01-01T00:00:00Z', + merchant_name='Example Merchant', + ), + merchant_authorization='hdr.payload.sig' if include_authorization + else None, + ) + return cart_mandate.model_dump(mode='json', exclude_none=True) + + +def _payment_mandate(cart_mandate_hash: str | None) -> PaymentMandate: + return PaymentMandate( + payment_mandate_contents=PaymentMandateContents( + payment_mandate_id='pm_001', + payment_details_id='order_001', + payment_details_total=_payment_item(), + payment_response={ + 'request_id': 'order_001', + 'method_name': 'CARD', + }, + merchant_agent='merchant_agent_001', + timestamp='2026-08-04T00:00:00+00:00', + cart_mandate_id='cart_001', + cart_mandate_hash=cart_mandate_hash, + ), + ) + + +def test_valid_binding_passes(): + """A hash over the raw wire object verifies cleanly, extensions included.""" + cart_data = _cart_mandate_data() + # Extension data outside the CartMandate model schema is part of the + # transmitted object and must be covered by the hash. + cart_data['x_merchant_extension'] = {'loyalty_tier': 'gold'} + payment_mandate = _payment_mandate(compute_cart_mandate_hash(cart_data)) + + validate_cart_mandate_hash(payment_mandate, cart_data) + + +def test_tampered_model_field_rejected(): + """Tampering with a schema field after hashing is detected.""" + cart_data = _cart_mandate_data() + payment_mandate = _payment_mandate(compute_cart_mandate_hash(cart_data)) + + tampered = copy.deepcopy(cart_data) + tampered['contents']['payment_request']['details']['total']['amount'][ + 'value' + ] = 1.0 + + with pytest.raises(ValueError, match='hash mismatch'): + validate_cart_mandate_hash(payment_mandate, tampered) + + +def test_tampered_extension_field_rejected(): + """Tampering outside the model schema is detected by the raw-object hash. + + A hash over a re-parsed Pydantic model would miss this: unknown fields are + silently dropped by model_validate, so the re-serialized form is identical + before and after tampering. + """ + cart_data = _cart_mandate_data() + cart_data['x_merchant_extension'] = {'loyalty_tier': 'gold'} + payment_mandate = _payment_mandate(compute_cart_mandate_hash(cart_data)) + + tampered = copy.deepcopy(cart_data) + tampered['x_merchant_extension'] = {'loyalty_tier': 'none'} + + with pytest.raises(ValueError, match='hash mismatch'): + validate_cart_mandate_hash(payment_mandate, tampered) + + +def test_injected_unknown_field_rejected(): + """Injecting a brand new unknown field after hashing is detected.""" + cart_data = _cart_mandate_data() + payment_mandate = _payment_mandate(compute_cart_mandate_hash(cart_data)) + + tampered = copy.deepcopy(cart_data) + tampered['x_injected'] = 'attacker-data' + + with pytest.raises(ValueError, match='hash mismatch'): + validate_cart_mandate_hash(payment_mandate, tampered) + + +def test_absent_hash_rejected_by_default(): + """A PaymentMandate without cart_mandate_hash is rejected by default.""" + cart_data = _cart_mandate_data() + payment_mandate = _payment_mandate(None) + + with pytest.raises(ValueError, match='cart_mandate_hash absent'): + validate_cart_mandate_hash(payment_mandate, cart_data) + + +def test_absent_hash_skipped_with_explicit_opt_out(): + """The legacy opt-out must be explicit and skips only the absent case.""" + cart_data = _cart_mandate_data() + payment_mandate = _payment_mandate(None) + + validate_cart_mandate_hash( + payment_mandate, cart_data, allow_unbound_cart=True + ) + + +def test_opt_out_does_not_weaken_present_hash(): + """allow_unbound_cart never bypasses verification of a present hash.""" + cart_data = _cart_mandate_data() + payment_mandate = _payment_mandate(compute_cart_mandate_hash(cart_data)) + + tampered = copy.deepcopy(cart_data) + tampered['contents']['merchant_name'] = 'Evil Merchant' + + with pytest.raises(ValueError, match='hash mismatch'): + validate_cart_mandate_hash( + payment_mandate, tampered, allow_unbound_cart=True + ) + + +def test_explicit_null_differs_from_absent_field(): + """An explicit null is not the same wire object as an absent field.""" + cart_data = _cart_mandate_data(include_authorization=False) + assert 'merchant_authorization' not in cart_data + payment_mandate = _payment_mandate(compute_cart_mandate_hash(cart_data)) + + with_null = copy.deepcopy(cart_data) + with_null['merchant_authorization'] = None + + assert compute_cart_mandate_hash(with_null) != compute_cart_mandate_hash( + cart_data + ) + with pytest.raises(ValueError, match='hash mismatch'): + validate_cart_mandate_hash(payment_mandate, with_null) diff --git a/code/sdk/python/ap2/models/mandate.py b/code/sdk/python/ap2/models/mandate.py index cc0a7c82..2d13f37c 100644 --- a/code/sdk/python/ap2/models/mandate.py +++ b/code/sdk/python/ap2/models/mandate.py @@ -172,9 +172,15 @@ class PaymentMandateContents(BaseModel): 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).' + 'hex(sha256(JCS(CartMandate))), where JCS is the RFC 8785 ' + 'canonical form of the CartMandate JSON object exactly as ' + 'transmitted. Verifiers MUST recompute this hash over the raw ' + 'received CartMandate JSON object, before any schema-based ' + 'parsing, and MUST reject the mandate if the value does not ' + 'match. Verifiers MUST reject a mandate that omits this field ' + 'unless an explicit legacy allowance is configured. See the ' + 'Cart-to-Payment Mandate Binding section of the AP2 ' + 'specification.' ), ) From 1838d4876988dab0a9b1a637f743ffb0b3bc8a27 Mon Sep 17 00:00:00 2001 From: AlgoVoi Date: Tue, 4 Aug 2026 09:08:11 +0100 Subject: [PATCH 8/8] docs(spec): scope binding section and drop unrelated reformatting specification.md previously carried roughly sixty lines of list-marker reformatting unrelated to the binding change. The upstream formatting is restored and MD030 is instead disabled in the markdownlint config, alongside the rules the repository already relaxes, because the repository's Markdown style uses three spaces after list markers. The one-line MD052 fix for the broken Agent Authorization Framework link is kept because the file fails lint without it. The checkout_hash entropy paragraph is reverted as well: it duplicates the proposal discussed in #278 and is out of scope here. The surrounding checkout_hash architecture is left untouched, and the Cart-to-Payment Mandate Binding section is reworded to present the cart binding as a complement to the primary checkout_hash binding, to require hashing the raw received CartMandate JSON object before any schema-based parsing, and to require rejection by default when cart_mandate_hash is absent. Signed-off-by: AlgoVoi --- .github/linters/.markdownlint.json | 1 + docs/ap2/specification.md | 124 +++++++++++++++-------------- 2 files changed, 66 insertions(+), 59 deletions(-) diff --git a/.github/linters/.markdownlint.json b/.github/linters/.markdownlint.json index 38fb124f..7794e952 100644 --- a/.github/linters/.markdownlint.json +++ b/.github/linters/.markdownlint.json @@ -4,6 +4,7 @@ "MD007": { "indent": 4 }, + "MD030": false, "MD033": false, "MD046": false, "MD024": false diff --git a/docs/ap2/specification.md b/docs/ap2/specification.md index 7416212c..00475ab4 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 @@ -152,12 +152,9 @@ 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 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. +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). Once the Merchant Payment Processor has accepted or rejected the Payment Mandate, a signed Payment Receipt MUST be returned to the Shopping Agent, @@ -168,35 +165,44 @@ 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 authorizes. This -prevents a malicious or misconfigured agent substituting a different cart -after the user has expressed intent. +The `checkout_hash` binding above links the Payment Mandate to the Checkout +JWT and remains the primary Checkout binding. Deployments that additionally +exchange the cart as a JSON `CartMandate` object, as the reference SDK and +samples do, MUST also bind the `PaymentMandate` to that exact object, so that +a malicious or misconfigured agent cannot substitute 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. + containing `hex(sha256(JCS(CartMandate)))`, where JCS is the JSON + Canonicalization Scheme defined in RFC 8785, applied to the `CartMandate` + JSON object exactly as transmitted. + +2. Verifiers MUST compute the hash over the raw received `CartMandate` JSON + object, before any schema-based parsing. Hashing a re-serialized data + model is not sufficient: parsers drop unknown or extension fields and may + collapse an explicit `null` with an absent field, so tampering outside + the model schema would escape a model-derived hash. JCS canonicalization + removes whitespace, key-order, and number-formatting variation, so the + hash of the raw object is stable across language implementations. + Producers SHOULD omit optional fields that carry no value rather than + emitting `null`. 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. + the hash and compare it to `cart_mandate_hash`. A mismatch MUST cause the + transaction to be rejected. A `PaymentMandate` that omits + `cart_mandate_hash` MUST be rejected by default; a verifier MAY accept + one only through an explicit legacy configuration during rollout. ## 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. @@ -296,16 +302,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. @@ -332,11 +338,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 @@ -350,9 +356,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 @@ -373,16 +379,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 @@ -400,9 +406,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