What happened?
Description
AP2 defines payment.allowed_payment_instruments as an allowed array of Payment Instrument objects. A Payment Instrument contains both id and type, and type identifies the instrument category.
The Python SDK's AllowedPaymentInstrumentEvaluator checks only allowed.id == instrument.id. It does not compare type.
As a result, a closed mandate using {id: "shared-id", type: "bank_account"} can satisfy an open mandate that allowed {id: "shared-id", type: "card"}.
Impact
The mandate signatures and chain may still verify, but the payment-instrument authorization check is under-bound.
The practical impact depends on whether Payment Instrument IDs are guaranteed to be globally unique across instrument types, providers, rails, and account namespaces. If IDs are scoped or can repeat across types, the SDK can accept a closed mandate selecting a payment instrument type different from the one authorized by the user.
Downstream payment may still fail if the Credential Provider or payment rail performs stricter checks. The issue is that the SDK verifier can incorrectly pass the authorization constraint before those downstream checks.
Steps to Reproduce
-
Review docs/ap2/payment_mandate.md:102-117. The closed mandate's payment_instrument must be present in the open mandate's allowed array.
-
Review docs/ap2/specification.md:389-394 and code/sdk/schemas/ap2/types/payment_instrument.json:8-24. Payment Instrument objects include both id and type, and type identifies the instrument category.
-
Review code/sdk/schemas/ap2/open_payment_mandate.json:198-219. allowed is an array of Payment Instrument objects, not an array of IDs.
-
Review code/sdk/python/ap2/sdk/constraints.py:232-251. AllowedPaymentInstrumentEvaluator.evaluate() returns success when any allowed instrument has the same id as the closed mandate's instrument.
-
Review code/sdk/python/ap2/tests/constraints_tests.py:377-410. Existing tests cover same-ID/same-type success and different-ID failure, but not same-ID/different-type rejection.
Relevant log output
Code of Conduct
What happened?
Description
AP2 defines
payment.allowed_payment_instrumentsas an allowed array of Payment Instrument objects. A Payment Instrument contains bothidandtype, andtypeidentifies the instrument category.The Python SDK's
AllowedPaymentInstrumentEvaluatorchecks onlyallowed.id == instrument.id. It does not comparetype.As a result, a closed mandate using
{id: "shared-id", type: "bank_account"}can satisfy an open mandate that allowed{id: "shared-id", type: "card"}.Impact
The mandate signatures and chain may still verify, but the payment-instrument authorization check is under-bound.
The practical impact depends on whether Payment Instrument IDs are guaranteed to be globally unique across instrument types, providers, rails, and account namespaces. If IDs are scoped or can repeat across types, the SDK can accept a closed mandate selecting a payment instrument type different from the one authorized by the user.
Downstream payment may still fail if the Credential Provider or payment rail performs stricter checks. The issue is that the SDK verifier can incorrectly pass the authorization constraint before those downstream checks.
Steps to Reproduce
Review
docs/ap2/payment_mandate.md:102-117. The closed mandate'spayment_instrumentmust be present in the open mandate'sallowedarray.Review
docs/ap2/specification.md:389-394andcode/sdk/schemas/ap2/types/payment_instrument.json:8-24. Payment Instrument objects include bothidandtype, andtypeidentifies the instrument category.Review
code/sdk/schemas/ap2/open_payment_mandate.json:198-219.allowedis an array of Payment Instrument objects, not an array of IDs.Review
code/sdk/python/ap2/sdk/constraints.py:232-251.AllowedPaymentInstrumentEvaluator.evaluate()returns success when any allowed instrument has the sameidas the closed mandate's instrument.Review
code/sdk/python/ap2/tests/constraints_tests.py:377-410. Existing tests cover same-ID/same-type success and different-ID failure, but not same-ID/different-type rejection.Relevant log output
Code of Conduct