Skip to content

fix(schema): accept valid SD-JWT serializations in checkout_mandate pattern#623

Closed
vishkaty wants to merge 2 commits into
Universal-Commerce-Protocol:mainfrom
vishkaty:fix/ap2-checkout-mandate-pattern
Closed

fix(schema): accept valid SD-JWT serializations in checkout_mandate pattern#623
vishkaty wants to merge 2 commits into
Universal-Commerce-Protocol:mainfrom
vishkaty:fix/ap2-checkout-mandate-pattern

Conversation

@vishkaty

Copy link
Copy Markdown
Contributor

Implements option 2 of #599 — opened as a draft so maintainers can confirm that widening the schema (rather than scoping the docs, option 1, or waiting for the binding to settle, option 3) is the intended direction.

Problem

ap2_mandate.json's checkout_mandate pattern cannot match any serialization that is valid for the format the field is documented to carry:

  • An RFC 9901 compact SD-JWT ends with ~ when no Key Binding JWT is present (the ABNF's trailing tilde is mandatory); the pattern's (~[A-Za-z0-9_-]+)* groups forbid a trailing tilde.
  • An SD-JWT with Key Binding ends in a KB-JWT, which contains dots; the tilde-segment character class forbids dots, so every SD-JWT+KB — the exact form the field's description names — was rejected.
  • Delegated presentations, as emitted by the AP2 reference implementation, join tokens with ~~, also rejected.

The only strings the old pattern accepted (a bare compact JWT, or dot-free tilde segments without the trailing tilde) are not valid RFC 9901 serializations. CI never surfaced this: the complete-request example validates against the base checkout schema (additionalProperties: true, no ap2 member), so the extension's pattern is never exercised.

Change

  • New pattern admitting: compact SD-JWT with or without a trailing KB-JWT, and ~~-joined delegate chains of such tokens (non-final hop's trailing tilde stripped at join, per the reference implementation's serialization). It is a strict superset of the old pattern, so no previously-valid payload breaks.
  • description updated to name the accepted serializations, and a short Wire format note added to ap2-mandates.md. (Doc edit is in the Mandate Structure section — no overlap with docs(ap2): clarify ES512 signature scope #618's algorithm-scope edits.)
  • As with any coarse syntactic filter, the pattern also admits some hybrid strings that are neither canonical form; it checks syntactic form only — signature, key-binding, and disclosure verification are cryptographic and defined by the AP2 Protocol Specification.

Validation

  • Acceptance matrix: 13 valid serialization forms (incl. real delegate-chain wires produced by the pinned reference implementation, 1–3 hops, KB and no-KB finals) + 13 malformed shapes — all classified correctly; 20,000 fuzzed valid serializations, zero rejections.
  • Superset proof: 20,000 random strings from the old pattern's grammar all still match; exhaustive brute force over every string of length ≤ 7 on the minimal alphabet found no counterexample.
  • A 3-hop delegate chain assembled via the reference SDK's own primitives verifies with the reference verifier and matches the new pattern.
  • Engines: compiles and agrees across ECMA (u-flag clean), Python, and Rust (ucp-schema); adversarial inputs up to 100 KB show linear-time matching (no catastrophic backtracking).
  • Upstream harnesses on this branch: ucp-schema lint source/ (96 files pass), scripts/validate_examples.py (276 passed, 0 failed), scripts/test_validate_examples.py (43 passed).
  • Instance-level check with the ucp-schema validator: a real reference-produced chain validates under the new pattern and reproduces the rejection under the old one.

Fixes #599.

katyalai added 2 commits July 21, 2026 12:45
…attern

The checkout_mandate pattern could not match any serialization that is valid
for the format the field is documented to carry:

- RFC 9901 compact SD-JWT ends with '~' when no KB-JWT is present; the
  pattern's (~[A-Za-z0-9_-]+)* groups forbid a trailing tilde.
- An SD-JWT with Key Binding ends in a KB-JWT, which contains dots; the
  tilde-segment character class forbids dots, so every SD-JWT+KB was rejected.
- Delegated presentations (as emitted by the AP2 reference implementation)
  join tokens with '~~', which the pattern also rejects.

The only strings the old pattern accepted (a bare compact JWT, or dot-free
tilde segments without the trailing tilde) are not valid RFC 9901
serializations. CI never caught this because the complete-request example
validates against the base checkout schema, which does not exercise the
extension's pattern.

The new pattern admits: compact SD-JWT with or without KB-JWT, and '~~'
delegate chains of such tokens; it is a strict superset of the old pattern,
so no previously-valid payload breaks. Also documents the accepted wire
forms in ap2-mandates.md.

Fixes Universal-Commerce-Protocol#599 (option 2).
Per adversarial review: the previous pattern let a delegate-chain hop be a
bare JWT (e.g. `J~~J`), a shape the AP2 reference verifier rejects (every hop
must be a well-formed SD-JWT — at least one disclosure, or a KB-JWT). The
back-compat leniency for the old pattern's bare/no-terminator forms now applies
only to a single (non-chained) token, not inside chains.

Still a strict superset of the original pattern (verified: 30k-string fuzz over
the old grammar, zero regressions), and all reference-emitted serializations
(1-3 hop chains, KB and no-KB finals, real reference-minted wires) still match.
@vishkaty

Copy link
Copy Markdown
Contributor Author

Updated the pattern after an internal adversarial pass: chain hops are now required to each be a well-formed SD-JWT (at least one disclosure, or a KB-JWT), so a bare-JWT hop like <jwt>~~<jwt> — which the AP2 reference verifier rejects — is no longer accepted. The relaxed single-token forms are kept only outside of chains, so it remains a strict superset of the original pattern (verified against a 30k-string fuzz over the old grammar). All reference-emitted serializations still validate.

@vishkaty

Copy link
Copy Markdown
Contributor Author

Closing this in favor of #610, which addresses the same issue (#599) with a schema fix that's already in review — no need for two PRs on the same pattern. Thanks @Harman-2 for picking it up.

@vishkaty vishkaty closed this Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ap2_mandate.json: checkout_mandate pattern rejects the serialization the AP2 reference SDK emits — which wire form is intended?

2 participants