DEV Community

Michael "Mike" K. Saleme
Michael "Mike" K. Saleme

Posted on

A signed mandate is not an enforced one

Card networks are standardizing the record of what a person allowed an agent to buy.

This pattern exists in public specifications now. For example, Mastercard's Verifiable Intent, published with Google in March as an open draft (v0.1, Apache-2.0), describes its job as "cryptographic proof that an agent acted within delegated authority" (security model, §2.4).

In its autonomous flow, where the user sets limits and is not present at purchase time, the record is a signed chain of three layers:

  • First layer: the issuer, a bank or payment network, vouches for the user's key.
  • Second layer: the user signs a mandate binding the agent's key, with limits: which merchants, which items, what amount, how often.
  • Third layer: the agent presents the final checkout and payment, signed with the key the user delegated.

The spec is also explicit about what the chain cannot do alone (§4.2):

Transaction count and budget enforcement cannot be done cryptographically within VI alone — they require stateful tracking by the payment network.

That sentence is the most important one in the spec, and it deserves credit for being written down.

The pattern

A signed record proves what was allowed. It does not, by itself, stop anything.

Some properties live in the signature: the constraints the user set, the agent key they bound, the checkout the payment refers to. Others live in a party's behaviour:

  • a verifier that remembers which nonces it has seen;
  • a verifier that checks the credential was addressed to it;
  • a network that counts how many payments one mandate has produced and how much it has spent.

Those are properties of whoever receives the credential, not of the credential.

What the reference verifier returned

I probed the reference implementation at commit 356c296, offline. I built chains with the library's own issuance API and changed one thing per case. I exercised seven of the nine attack categories the spec lists: replay and audience, count and amount, constraint stripping, checkout-payment mismatch, algorithm confusion, clock skew, and split agent. I did not exercise disclosure withholding or type confusion. Code, raw results and reproduction steps: approval-binding-vectors/research/verifiable-intent-probe.

The cryptography held:

  • a mandate re-signed by another key was rejected;
  • so was a credential with its signature stripped and alg set to none;
  • so was a mandate that bound two different agent keys;
  • so was a payment that pointed at a different checkout, when one verifier held both halves.

Expiry is policy, not cryptography. An expired credential was accepted inside the default 300-second clock-skew tolerance, which the spec recommends, and rejected once the verifier tightened skew to 60 seconds.

The other checks returned what the reference API's separation of checks predicts:

  • Audience is a parameter. Called the way the repository's own integration tests call it, verify_chain returned valid=True for a payment credential addressed to another party. It checks the audience when the caller supplies the expected value, and otherwise lists the check under checks_skipped.
  • Limits are a second call. For a payment above the mandate's cap, verify_chain returned valid=True. A separate check_constraints call rejected it. Whether the API should expose a combined verdict is what upstream issue #38 asks.
  • Replay and counting are assigned elsewhere. The same payment credential returned valid=True twice, and so did a second payment from the same mandate. The spec assigns both to the verifier's nonce store and the network's issuance log.

A careful integrator reads that contract. A hurried one reads valid: true.

Two halves, two owners

In the autonomous flow the agent splits its final credential in two. The payment network receives the payment half. The merchant receives the checkout half. The spec states the consequence plainly: "Each party verifies its own L3 independently" (spec overview, §8.4). The comparison between the two references happens "when a verifier has access to both L3 credentials (e.g., during dispute resolution)."

I changed only the payment half's reference, so it matched no checkout. The network-side verifier returned valid=True, and so did the merchant-side verifier. Neither is wrong. Each checked what it holds, and neither holds what it would need to compare.

That moves the question out of the verifier and into the deployment. Which component holds both halves, and does its comparison run before the payment is authorized or the order is fulfilled, or only afterwards, in a dispute? My probe shows the limits of a partial view. It does not show how any deployed payment flow answers that question, and that is the evidence worth asking for.

A signed mandate proves what the agent was allowed to do. Adversarial tests show whether the deployed system rejects the violations you exercise.

What to test

If you integrate a mandate chain like this, as a merchant, a network, a wallet or an agent platform, the spec tells you what to check. A test tells you whether your deployment did, for the cases you ran.

  1. Negative controls against your running system, not the library. Each must be refused by the system you deploy:
    • a replayed credential;
    • a second payment from a single-use mandate;
    • cumulative spend across merchants over the budget;
    • an amount over the cap;
    • a credential addressed to someone else.
  2. A positive control. A valid chain must still pass. A system that refuses everything also passes every negative test.
  3. Declare where the cross-reference runs, then test it there. Name the component that compares the two halves and when it runs. Present a mismatched pair to that component and see whether the purchase stops at the point you declared.
  4. Sequences, not single calls. Count and budget are state. The test is the second and third payment, not the first.
  5. Restore, then pay again. If the network's issuance log can be rolled back with the rest of the system, a single-use mandate can be used twice. Checkpoint before use, authorize the first payment, restore, then attempt a second payment under the same single-use mandate. Give the second payment a fresh nonce, so you are testing mandate-use tracking rather than ordinary replay, and keep every credential inside its validity window. Count the authorizations.

A contributor has published cross-implementation vectors and linked them in upstream issue #38. What I have not found is a public set of deployment tests for role-specific decisions and stateful enforcement. I opened a proposal for one in the open-source harness I maintain: #664.

The record is getting standardized fast. Enforcement still lives with each party that receives it, and it has to be tested there.

Top comments (0)