Skip to content

[Bug]: x402 PSP continues without mandate verification when the agent-provider key is unavailable, while two other roles refuse #309

Description

@giorgioroth

What happened?

At commit e1ea56d, the roles that verify a mandate chain do not agree on what to do when the agent-provider public key cannot be loaded from AGENT_PROVIDER_PUB_PATH. Two refuse; the settlement component continues.

Roles that refuse:

File Line Returned
merchant_agent_mcp/server.py 663 no_public_key (in create_checkout)
merchant_agent_mcp/server.py 866 agent_provider_key_missing (in complete_checkout)
merchant_payment_processor_mcp/server.py 242 agent_provider_key_missing

The role that does not:

In x402_psp_mcp/server.py, agent_provider_pub is initialised to None at line 110 and loaded only if the path exists. A missing file, or an OSError, ValueError or JSONDecodeError while reading it, leaves it None and logs that SD-JWT verification is being skipped (lines 118 and 122).

The verification step is then guarded at line 125 by if agent_provider_pub:. That block contains MandateClient().verify(), PaymentMandateChain.parse() and parsed_chain.verify(). There is no else branch returning an error, and no other error return in the file for this condition.

Two things follow from the structure. The signature check and the constraint evaluation are skipped together, since parsed_chain.verify() is the call that evaluates the Payment Mandate constraints and is inside the same guard. And execution continues past the guard to the binding check, ecrecover, routing, and the optional on-chain broadcast controlled by BROADCAST_ON_CHAIN.

Specification context. AP2 v0.2, in docs/ap2/specification.md at the same commit, states under Verification that the Merchant Payment Processor must verify that the Payment Credential is appropriately scoped to the Checkout. In this component the verification that would establish that scoping is conditional on the key being available.

Steps to reproduce

This is a source observation. I did not reproduce an unverified settlement.

  1. Open code/samples/python/src/roles/x402_psp_mcp/server.py at commit e1ea56d.
  2. Lines 110–122: agent_provider_pub starts as None; on a missing or unreadable key file the code logs that verification is skipped and leaves it None.
  3. Line 125: the whole verification block is inside if agent_provider_pub:, with no else.
  4. Continue past the block: binding, ecrecover, routing, optional broadcast.
  5. Compare with lines 663 and 866 of merchant_agent_mcp/server.py and line 242 of merchant_payment_processor_mcp/server.py, which return an error under the corresponding condition.

What I expected: the same missing-key condition handled the same way across roles that all need the key for the same purpose, or a comment explaining why the settlement component differs.

What I have not established. Whether the branch is reachable in a provisioned deployment: in the sample scenario the key is written before the flow starts, and when I ran the x402 flow end to end with the key present, verification did occur. Whether the merchant and the PSP can encounter different key availability in a supported deployment: the path only reaches the PSP after the merchant's own verification succeeded, so the two branches are not shown firing in a single run. Whether an unverified settlement can be driven to a successful broadcast: not tested.

Question: is the asymmetry intentional? If the sample PSP is meant to tolerate a missing verification key for local demonstration, saying so in a comment would make the difference from the other two roles legible.

Prior report. I sent this to the Google security team first, since the affected path can reach a broadcast. They replied that it does not meet their threshold for tracking as a security bug and invited me to file it publicly, which is what this is. Reported there as issue 540649426, closed as Infeasible on 2026-07-30.

Relation to #308. Separate observation in the same sample flow. #308 concerns ordering — internal state persisted before settlement is confirmed. This one concerns whether verification runs at all when a configuration file is absent. They share a file only incidentally.

Relevant log output

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions