Summary
Proposing a v0 conformance vector set for open_mandate_hash derivation against code/sdk/schemas/ap2/open_checkout_mandate.json. Independently cross-implementation validated, ready to land as tests/conformance/canonicalization/ (or similar) for any third implementer to derive deterministically.
Why now
In Discussion #262 (cycles-ap2-python show-and-tell), the maintainer @amavashev surfaced the gap directly:
Which raises the open question for the AP2 community: do we have, or plan to publish, conformance vectors for open_mandate_hash derivation? […] Spec-published vectors would let any third implementer derive the same hash deterministically rather than re-arrive at the assumption — the difference between "interop works because we agree" and "interop works because the spec is precise enough not to need agreement."
This issue is the artefact that answers that question, ready for spec inclusion.
Derivation rule (proposed normative text)
open_mandate_hash = SHA-256(JCS_RFC8785(unsigned open-checkout-mandate body))
Lowercase hex. Hash input is the claims object, not the JWS compact form. The latter is the load-bearing interop subtlety — JWS re-encoding by intermediaries changes the envelope even when the underlying payload is identical, which would silently break hash matching between implementations that round-trip through different JWS libraries.
v0 conformance vectors
Published at https://gist.github.com/chopmob-cloud/1dca25fd6107db4b7a30bed5dbf2ded8 — 7 vectors anchored to code/sdk/schemas/ap2/open_checkout_mandate.json at sha e3d9cafa7311d90612c7f908ae9b8821ddc8735a, structured as pairs so the canonicalisation rules are self-checking:
| Vector |
Pair invariant |
Result |
| baseline-001 |
— |
reference baseline |
| object-key-order-002 |
with 001 |
JCS sorts object members → byte-identical to 001 |
| array-order-003 |
with 001 |
arrays are order-significant; do NOT sort → differs from 001 |
| optional-fields-004 |
with 001 |
presence ≠ absence, not collapsed → differs from 001 |
| currency-minor-unit-005 |
— |
ISO-4217 integer minor units; no decimal/float |
| unicode-nfc-006a |
with 006b |
RFC 8785 does NO Unicode normalisation |
| unicode-nfd-006b |
with 006a |
NFC and NFD inputs hash differently |
The array-order and Unicode pairs are the ones that catch divergence in practice. An implementer who sorts arrays "to canonicalise" or who NFC-normalises merchant strings before hashing matches the wrong vector immediately rather than silently producing an incompatible hash.
Cross-implementation validation
| Implementation |
Authors |
Language |
Result |
rfc8785@0.1.4 |
William Woodruff |
Python 3.14.3 |
7/7 JCS bytes + 7/7 hashes + 4/4 pair invariants ✓ |
canonicalize@3.0.0 |
Samuel Erdtman + Anders Rundgren (contributor — RFC 8785 author) |
JavaScript |
7/7 JCS bytes + 7/7 hashes + 4/4 pair invariants ✓ |
Different authors, different languages, entirely different codebases — identical canonical bytes and identical hashes for every vector. The Unicode NFC-vs-NFD pair (the canonicalisation edge case where RFC 8785 implementations most often diverge) agrees byte-for-byte under both, which is the direct evidence that the no-Unicode-normalisation rule is implementation-independent.
Validation history is in Discussion #262 comments (2026-05-19 → 2026-05-20).
Scope and non-scope
In scope of this issue:
- Adopting the derivation rule as normative spec text
- Adopting the 7 v0 vectors as official conformance fixtures
- Suggested repo location:
code/sdk/schemas/ap2/conformance/open_mandate_hash/
Not in scope (separate work):
- The JWS signing layer above the hashed body (out of scope of canonicalisation)
payment_mandate.json, payment_receipt.json, checkout_mandate.json, etc. — same rule should likely apply uniformly across all mandate types, but each needs its own vector set keyed to its schema. Happy to follow up with sibling sets if v0 lands well.
Happy to
- Reshape vector JSON into whatever the maintainers prefer (current shape is
{mandate_body, expected_jcs_bytes_b64, expected_open_mandate_hash} per vector, plus pair-invariant declarations)
- Submit a PR adding the fixtures as files rather than gist-linked
- Add a build script that regenerates the vectors from the schema so the artefact is reproducible-by-construction, not trust-the-static-file
- Extend the set if maintainers want additional rules covered (numeric precision edge cases, deeply nested arrays, large-Unicode handling)
Filing this as [Proposal] per the prior-art naming on #250 / #224 / #255.
Summary
Proposing a v0 conformance vector set for
open_mandate_hashderivation againstcode/sdk/schemas/ap2/open_checkout_mandate.json. Independently cross-implementation validated, ready to land astests/conformance/canonicalization/(or similar) for any third implementer to derive deterministically.Why now
In Discussion #262 (
cycles-ap2-pythonshow-and-tell), the maintainer @amavashev surfaced the gap directly:This issue is the artefact that answers that question, ready for spec inclusion.
Derivation rule (proposed normative text)
Lowercase hex. Hash input is the claims object, not the JWS compact form. The latter is the load-bearing interop subtlety — JWS re-encoding by intermediaries changes the envelope even when the underlying payload is identical, which would silently break hash matching between implementations that round-trip through different JWS libraries.
v0 conformance vectors
Published at https://gist.github.com/chopmob-cloud/1dca25fd6107db4b7a30bed5dbf2ded8 — 7 vectors anchored to
code/sdk/schemas/ap2/open_checkout_mandate.jsonat shae3d9cafa7311d90612c7f908ae9b8821ddc8735a, structured as pairs so the canonicalisation rules are self-checking:The array-order and Unicode pairs are the ones that catch divergence in practice. An implementer who sorts arrays "to canonicalise" or who NFC-normalises merchant strings before hashing matches the wrong vector immediately rather than silently producing an incompatible hash.
Cross-implementation validation
rfc8785@0.1.4canonicalize@3.0.0Different authors, different languages, entirely different codebases — identical canonical bytes and identical hashes for every vector. The Unicode NFC-vs-NFD pair (the canonicalisation edge case where RFC 8785 implementations most often diverge) agrees byte-for-byte under both, which is the direct evidence that the no-Unicode-normalisation rule is implementation-independent.
Validation history is in Discussion #262 comments (2026-05-19 → 2026-05-20).
Scope and non-scope
In scope of this issue:
code/sdk/schemas/ap2/conformance/open_mandate_hash/Not in scope (separate work):
payment_mandate.json,payment_receipt.json,checkout_mandate.json, etc. — same rule should likely apply uniformly across all mandate types, but each needs its own vector set keyed to its schema. Happy to follow up with sibling sets if v0 lands well.Happy to
{mandate_body, expected_jcs_bytes_b64, expected_open_mandate_hash}per vector, plus pair-invariant declarations)Filing this as
[Proposal]per the prior-art naming on #250 / #224 / #255.