Skip to content

[Docs]Human-present approval chain remains underdefined in AP2 #215

Open

Description

@zesttec

Description

Thanks for publishing AP2 and for making the early design and samples available. I realize v0.1 is still at an early stage and that the current repository is positioned mainly as samples and demos. With that in mind, I wanted to raise what seems to be a protocol-completeness gap in the human-present flow: AP2 spec does not yet make fully clear which object is the final user-approved proof artifact, what must bind that approval to the cart and payment details, and what a verifier is expected to check. Because of that, two independent implementations could produce different approval artifacts and still believe they follow AP2. That would make issuer interpretation, merchant evidence, and dispute handling less consistent.

A concrete example is the relationship between the Cart Mandate and the PaymentMandate. In Section 4.1.1 of docs/specification.md, the Cart Mandate is described as the “foundational credential” for the human-present case and as being “cryptographically signed by the user.” Section 5.1 also says the user’s signed approval creates a cryptographically signed Cart Mandate. But the same Section 5.1 says the merchant signs the cart, and Section 7.1 Step 10 says the cart mandate is “first signed by the merchant entity.” Separately, Section 4.1.3 describes the PaymentMandate as a separate credential “bound to Cart/Intent mandate but containing separate information.” So the main question is still unclear: in the human-present flow, does final user approval live on CartMandate, on PaymentMandate, or on both together?

The current implementation does not make that answer much clearer. In src/ap2/types/mandate.py, the split between CartMandate and PaymentMandate still leaves it unclear where final user approval is meant to live. In the Python human-present flow, samples/python/src/roles/shopping_agent/tools.py uses a placeholder user_authorization, and samples/python/src/common/validation.py only checks that the field is present. That is understandable for an early v0.1 demo, but it also means the implementation does not yet show a complete and reproducible approval chain that external AP2 implementers can follow consistently.

I think that is the core gap. Section 4.1.3 also says AI-agent presence and transaction modality signals must always be shared, but the current model does not make the full human-present approval chain easy to read and verify from the canonical objects alone. Right now, too much of that chain still lives in prose.

Potential hardening improvements

  • Spec can state explicitly where final user approval lives in the human-present flow: CartMandate, PaymentMandate, or both.
  • Define the required bindings between the approved cart, the payment details, and the modality / agent-presence signals.
  • Document one canonical verifier procedure that merchants, processors, issuers, and dispute systems can all follow.

I think this is mainly a protocol-completeness issue, and clarifying it would likely make independent AP2 implementations easier to align. I hope this is useful, and thanks again for publishing AP2 and sharing the work.

Activity

  1. jithinraj commented on Apr 22, 2026

    @jithinraj

    Great work, @zesttec. It looks like the right gap to close.

    The smallest useful step may be to make the verifier-facing approval artifact explicit, or at least require stable refs for the approved cart, the payment details, and the final approved object.

    That would let different implementations answer the same question, “what exactly did the user approve?” without inventing their own approval model.

  2. chopmob-cloud commented on Jun 6, 2026

    @chopmob-cloud

    The approval chain question you've raised has a concrete answer from production: final user approval belongs on the PaymentMandate, and the binding to the CartMandate is via cart_mandate_hash.

    The cleanest reading of the spec: the CartMandate is what the user wants to buy (merchant-signed, describing the cart). The PaymentMandate is the user's commitment to pay for that specific cart. By binding cart_mandate_hash = SHA-256(JCS(CartMandate)) into the PaymentMandate, the user's signature on the PaymentMandate IS their approval of the CartMandate contents — any modification to the cart invalidates the hash and therefore invalidates the approval.

    This makes the verifier procedure concrete:

    1. Re-derive SHA-256(JCS(CartMandate)) independently
    2. Compare against cart_mandate_hash in the PaymentMandate
    3. Verify the user's signature on the PaymentMandate
    4. Done — the full approval chain is reproduced from the canonical objects alone

    AP2 PR #253 implements this binding in the Python SDK (cart_mandate_hash in PaymentMandateContents) with a validate_cart_mandate_hash() helper. The JCS canonicalization (RFC 8785) is the critical step — without it, float/integer serialization differences across languages break the hash comparison.

    AlgoVoi (chopmob-cloud) — docs.algovoi.co.uk/compliance-receipt

  3. rpelevin commented on Jun 6, 2026

    @rpelevin

    I would make the verifier-facing approval artifact explicit, even if AP2 keeps the current CartMandate / PaymentMandate shape.

    The important thing is not only which object carries the final approval, but what a verifier must be able to prove from it. I would want the human-present approval boundary to bind:

    • approved cart digest or cart mandate hash
    • merchant / payee identity
    • amount, currency, and payment instrument reference
    • agent identity or delegation context
    • user-present approval method
    • approval timestamp
    • nonce or session id
    • mandate/spec version
    • policy or limit reference when applicable

    Then the verifier checks the final payment object against the approved cart/payment facts, not against an informal UI transcript.

    That keeps CartMandate as the purchase-intent object and PaymentMandate as the payment-authorization object, while still giving merchants, issuers, and dispute systems one stable question: did this payment execute under the exact human-approved terms?

    Useful regressions: cart mutation after approval invalidates the payment approval; price/currency/payee change invalidates it; a stale mandate version cannot be silently accepted; and a merchant can verify the approval chain without seeing private user-interface logs.

  4. zesttec commented on Jul 13, 2026

    @zesttec
    Author

    Thanks for the discussion above. The proposed Cart/Checkout-to-Payment hash binding addresses one part of the approval chain. Assuming that binding is correctly defined and verified, I think v0.2 still leaves a narrower Trusted Surface question which need to define semantic consistency between consent UI data and signed Mandate Content.

    AP2 requires delegate_payload to contain the Mandate Content and to be included in Key Binding. [Delegation requirements] However, the informative OpenID4VP example uses a separate payment_card object as “UI Data,” while the Checkout and Payment Mandates are carried in a separate delegate object. [OpenID4VP example]

    Key Binding protects the transaction data from later modification, but it does not by itself say how the Trusted Surface should ensure that the displayed merchant, amount, currency, items, and terms have the same meaning as the Mandate Content being signed.

    It would be better to clarify the intended behavior:

    • Should the Trusted Surface render the security-relevant fields directly from the exact delegate_payload, or compare payment_card against it and reject any mismatch?
    • When several Mandate Delegations are included, must every element be displayed, and is approval all-or-nothing or per element? AP2 currently permits multiple elements but does not define these presentation rules. [Multiple delegations]

    The current Android sample illustrates why an explicit rule may help. It constructs the UI object and Mandate objects separately; the displayed amount uses a hard-coded USD label, while the Payment Mandate uses checkoutData.currency, before both objects are independently encoded into transaction_data. [UI construction] [Mandate construction and encoding]
    The default demo values remain aligned, so this is not evidence of a current exploit. It only shows that semantic consistency is not explicitly enforced by the request builder.

    Would it make sense to require either canonical rendering from delegate_payload or a deterministic, fail-closed field comparison, together with conformance tests for mismatched fields and additional delegation elements?

    This is separate from deciding which Mandate carries final approval or how the Checkout and Payment Mandates are linked. The question here is whether the information shown for consent necessarily matches the exact Mandate Content being signed.

  5. chopmob-cloud commented on Jul 13, 2026

    @chopmob-cloud

    This is the right narrower question, and it separates cleanly from the mandate-linking one: Key Binding gives you integrity of each signed object, but integrity is not the same as consistency between two objects. In the OpenID4VP example the transaction_data array carries the payment_card UI Data at index 0 and the delegate Mandate Content at index 1, and Key Binding signs both, so neither can be altered after the fact. What it does not establish is that the fields the surface displayed from index 0 mean the same thing as the authoritative Mandate Content at index 1. Both can be signed and still disagree, and a verifier that checks only the Mandate Content is validating something that may not be what the user was actually shown.

    The content-address discipline that answers the cart-to-payment linkage extends to this seam, and there are two clean shapes:

    1. Single authoritative source. The Trusted Surface renders the security-relevant fields (payee, amount, currency, items, terms) directly from the delegate_payload it is about to sign, and payment_card becomes a presentation of those exact bytes rather than an independently built object. With one source there is no second object to diverge, which is the strongest form.

    2. Fail-closed binding, if a separate UI object is kept. Bind the displayed fields to the mandate by hash: commit SHA-256(JCS(<the exact security-relevant mandate fields>)) into the UI object (or the reverse), so a verifier recomputes the digest over the Mandate Content and rejects when the displayed object does not match. That turns "display equals signed" from an assumption into a checkable property.

    The distinction worth making explicit in the spec is that Key Binding proves each object was not modified after signing, while a hash binding between them proves the two agreed at signing time. WYSIWYS needs both; the current example has the first and not the second, which is exactly the gap your Android observation illustrates (two objects independently encoded, aligned only because the demo defaults happen to match).

    On multiple delegations, the same rule resolves the presentation question: each element's displayed fields bind to that element's signed content, and approval is per element, because an all-or-nothing surface cannot prove which element the user actually reviewed. Every element whose content is signed should be displayed and individually bound; anything signed but not displayed is exactly the surface a WYSIWYS rule exists to forbid.

    For the verifier this slots onto the procedure already discussed: after re-deriving the cart-to-payment hash, add the symmetric check that the displayed security fields recompute to the signed mandate fields (or are byte-identical because rendered from them). One point worth pinning in the rule: on a mismatch the approval is rejected outright, not resolved in favour of the signed object, since executing the signed value when it differs from what was displayed is itself the misrepresentation the rule exists to prevent. This matters because Key Binding makes both objects authentic, so the signature alone cannot tell an honest surface from one that displayed one thing and signed another. Conformance vectors then fall out naturally: a mismatched displayed-versus-signed field must fail closed, and a multi-element case must require per-element display and binding rather than a single blanket approval.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions