Skip to content

fix: allow SD-JWT+kb key binding in checkout_mandate pattern#496

Open
sakinaroufid wants to merge 6 commits into
Universal-Commerce-Protocol:mainfrom
sakinaroufid:fix/ap2-checkout-mandate-kb-pattern
Open

fix: allow SD-JWT+kb key binding in checkout_mandate pattern#496
sakinaroufid wants to merge 6 commits into
Universal-Commerce-Protocol:mainfrom
sakinaroufid:fix/ap2-checkout-mandate-kb-pattern

Conversation

@sakinaroufid

Copy link
Copy Markdown
Contributor

Description

I was reading through the AP2 mandate schema and realized checkout_mandate can't actually hold the value it's documented to hold.

The field is described as an SD-JWT+kb credential, and the business verification steps in ap2-mandates.md require the business to verify the key binding. The issue is the regex. An SD-JWT+kb is serialized as <issuer-JWT>~<disclosure>...~<KB-JWT>, and that final piece, the key-binding JWT, is itself a JWT, so it has dots in it. The current pattern only allows dot-free segments after the issuer JWT:

^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+(~[A-Za-z0-9_-]+)*$

So anything carrying a real key binding gets rejected, and the only value that passes is an SD-JWT with the key binding stripped off. That's the opposite of what we want here. The key binding is the part that proves the holder actually presented the mandate, so the schema is effectively forcing implementations to drop the property that makes the mandate trustworthy. A conformant response that includes it fails validation today.

The reason nobody hit this yet is that every checkout_mandate example in the docs elides its value, so a real one never gets run through the pattern.

The fix is to allow an optional trailing ~<KB-JWT>:

^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+(~[A-Za-z0-9_-]+)*(~[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+)?$

It's a strict superset of the old one, so nothing that validates today stops validating. I checked against ucp-schema 1.1.0: a key-bound mandate now passes, the unbound form still passes, ucp-schema lint source/ is clean, and the full example corpus is still green. I also added a regression test that reads the pattern straight from the schema and checks the key-bound case, so if the pattern gets reverted later the test fails.

Category (Required)

  • Capability: New schemas (Discovery, Cart, etc.) or extensions. (Requires Maintainer approval)

Related Issues

None.

Checklist

  • I have followed the Contributing Guide (including Conventional Commits title requirements and ! for breaking changes). Not breaking: the pattern is a strict superset.
  • I have updated the documentation (if applicable). No prose change needed; the checkout_mandate examples stay elided.
  • My changes pass all local linting and formatting checks.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • (For Core/Capability) I have included/updated the relevant JSON schemas.
  • I have regenerated Python Pydantic models by running generate_models.sh under python_sdk. Not applicable in this repo.

checkout_mandate is an SD-JWT+kb credential, serialized as
<issuer-JWT>~<disclosure>...~<KB-JWT>. The key-binding JWT contains dots,
but the pattern only allowed dot-free segments after the issuer JWT, so it
rejected every key-bound mandate and accepted only the unbound form. AP2
verification (ap2-mandates.md) requires verifying the key binding, so a
conformant value could not pass validation.

Add an optional trailing ~<KB-JWT> group. Strict superset of the old
pattern, so nothing that validates today breaks. Add a regression test
that reads the pattern from the schema and checks the key-bound form.

@kmcduffie kmcduffie left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for updating this regex!

@kmcduffie

Copy link
Copy Markdown

@sakinaroufid This change makes sense. Can you add tests to ensure this regex is correct for the expanded use case?

@kmcduffie kmcduffie self-requested a review June 16, 2026 14:16
@sakinaroufid sakinaroufid force-pushed the fix/ap2-checkout-mandate-kb-pattern branch from 5f78847 to e33c2b9 Compare June 16, 2026 20:13
@sakinaroufid

Copy link
Copy Markdown
Contributor Author

@kmcduffie Thanks for your review! I've just added tests to ensure this regex is correct for the expanded use case.


for value, why in [
("not a credential", "free text"),
(f"{j}~{j}~{j}", "two KB-JWTs"),

@GarethCOliver GarethCOliver Jun 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also want to support delegate sd-jwts which will allow for chaining kb-jwts (for the autonomous use case: see https://datatracker.ietf.org/doc/html/draft-gco-oauth-delegate-sd-jwt.

This is of the form:

<issuer-jwt>~[disclosures]~~<kb-sd-jwt>~[disclosures]~<kb-jwt>

and could potentially chain further.

IMO we'd be better off not trying to enforce this at the schema level, instead in the application-layer when it goes to validation. That'll also make it easier for us to further extend this to other credential formats without modifications to AP2 (e.g. mDocs credentials, or zkp proofs.)

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants