From 89416b5acc4dbf12630490189bf0201897f10e91 Mon Sep 17 00:00:00 2001 From: iLoveChicken Date: Sat, 6 Jun 2026 05:04:51 +0100 Subject: [PATCH 01/14] feat(conformance): add open_mandate_hash derivation vectors v0 Lands the v0 conformance vector set for open_mandate_hash derivation proposed in #265, cross-validated across 6 independent implementations. Derivation rule: open_mandate_hash = SHA-256(JCS_RFC8785(unsigned_open_checkout_mandate_body)) Hash input is the mandate claims object, NOT the JWS compact form. 7 vectors covering the canonicalisation rules that catch implementation divergence in practice: - baseline-001: reference - object-key-order-002: JCS sorts object keys (MUST be same hash as baseline) - array-order-003: arrays are order-significant (MUST differ from baseline) - optional-fields-004: presence != absence (MUST differ from baseline) - currency-minor-unit-005: integer minor units only (no float/decimal) - unicode-nfc-006a: no Unicode normalisation (MUST differ from 006b) - unicode-nfd-006b: no Unicode normalisation (MUST differ from 006a) Cross-validated: Python rfc8785@0.1.4, JS canonicalize@3.0.0 (RFC 8785 author), Go gowebpki/jcs v1.0.1, Java cyberphone/json-canonicalization (RFC 8785 reference), Rust serde_jcs 0.2.0, Python rfc8785 (Crest). All 6 implementations: 7/7 vectors + 4/4 pair invariants. PEAC Protocol has already adopted this derivation as normative in AP2-COMPOSITION.md (peacprotocol/peac). Closes #265. --- .../conformance/open_mandate_hash/README.md | 78 ++++ .../open_mandate_hash/vectors-v0.json | 394 ++++++++++++++++++ 2 files changed, 472 insertions(+) create mode 100644 code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md create mode 100644 code/sdk/schemas/ap2/conformance/open_mandate_hash/vectors-v0.json diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md b/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md new file mode 100644 index 00000000..5e11fd40 --- /dev/null +++ b/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md @@ -0,0 +1,78 @@ +# AP2 `open_mandate_hash` Conformance Vectors — v0 + +Conformance vectors for the `open_mandate_hash` derivation rule for +`open_checkout_mandate.json`. Any AP2 implementation that derives +`open_mandate_hash` MUST reproduce these vectors byte-for-byte. + +## Derivation rule + +``` +open_mandate_hash = SHA-256(JCS_RFC8785(unsigned_open_checkout_mandate_body)) +``` + +**Lowercase hex output.** + +The hash input is the mandate **claims object**, not the JWS compact form. +Re-encoding the JWS envelope MUST NOT change `open_mandate_hash`. + +## Vectors + +`vectors-v0.json` — 7 vectors anchored to +`code/sdk/schemas/ap2/open_checkout_mandate.json` at schema commit +`e3d9cafa7311d90612c7f908ae9b8821ddc8735a`. + +Each vector is structured as: + +```json +{ + "vector_id": "ap2-omh-v0-", + "mandate_body": { ... }, + "expected_jcs_bytes_b64": "", + "expected_open_mandate_hash": "sha256:", + "expectation": "reference | same_hash_as: | different_hash_from:" +} +``` + +| Vector | Pair invariant | Tests | +|---|---|---| +| `baseline-001` | reference | Canonical baseline | +| `object-key-order-002` | `same_hash_as:baseline-001` | JCS sorts object members | +| `array-order-003` | `different_hash_from:baseline-001` | Arrays are order-significant | +| `optional-fields-004` | `different_hash_from:baseline-001` | Presence ≠ absence | +| `currency-minor-unit-005` | canonical form | Integer minor units only | +| `unicode-nfc-006a` | `different_hash_from:unicode-nfd-006b` | No Unicode normalisation | +| `unicode-nfd-006b` | `different_hash_from:unicode-nfc-006a` | No Unicode normalisation | + +The array-order and Unicode pairs catch the divergences most commonly seen +in practice: implementations that sort arrays or NFC-normalise strings will +fail the corresponding pair invariant immediately. + +## Cross-implementation validation + +| Implementation | Language | Library | Result | +|---|---|---|---| +| `rfc8785@0.1.4` | Python | Trail of Bits | 7/7 + 4/4 pair invariants ✓ | +| `canonicalize@3.0.0` | JavaScript | Erdtman + Rundgren (RFC 8785 author) | 7/7 + 4/4 ✓ | +| `gowebpki/jcs v1.0.1` | Go | @amavashev (AP2 maintainer) | 7/7 + 4/4 ✓ | +| `cyberphone/json-canonicalization` | Java | Rundgren (RFC 8785 reference impl) | 7/7 + 4/4 ✓ | +| `serde_jcs 0.2.0` | Rust | @seritalien / Vauban | 7/7 + 4/4 ✓ | +| `rfc8785` (fixed) | Python | Crest Systems | 7/7 + 4/4 ✓ | + +Full validation history: [AP2 issue #265](https://github.com/google-agentic-commerce/AP2/issues/265) + +## Known implementation hazards + +- **`json.dumps()` non-ASCII escaping** — Python's `json.dumps()` escapes + non-ASCII characters as `\uXXXX`. RFC 8785 requires literal UTF-8 bytes + for printable codepoints above U+007F. Use the `rfc8785` library instead. + This will cause failures on vectors 006a and 006b. + +- **Array sorting** — Do not sort arrays before hashing. JCS preserves + array element order. Vector 003 catches this. + +- **Unicode normalisation** — Do not NFC-normalise strings before hashing. + RFC 8785 makes no Unicode normalisation. Vectors 006a/006b catch this. + +- **Float/decimal prices** — `item.price` and `amount.amount` are integers + in the currency minor unit. `10.50` MUST be encoded as `1050`. Vector + 005 catches this. diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/vectors-v0.json b/code/sdk/schemas/ap2/conformance/open_mandate_hash/vectors-v0.json new file mode 100644 index 00000000..ab926f39 --- /dev/null +++ b/code/sdk/schemas/ap2/conformance/open_mandate_hash/vectors-v0.json @@ -0,0 +1,394 @@ +{ + "schema_version": "1.0", + "artefact_id": "ap2-open-mandate-hash-conformance-v0", + "published_at": "2026-05-19T13:30:00Z", + "canonicalizer": "rfc8785@0.1.4", + "hash": "SHA-256, lowercase hex, prefixed sha256:", + "anchored_to": { + "schema": "google-agentic-commerce/AP2 code/sdk/schemas/ap2/open_checkout_mandate.json", + "vct": "mandate.checkout.open.1", + "spec_section": "AP2 specification, Open Checkout Mandate" + }, + "derivation": "open_mandate_hash = SHA-256(JCS_RFC8785(unsigned_open_checkout_mandate_body)). Hash input is the mandate claims object, NOT the JWS compact form. Re-encoding the JWS envelope MUST NOT change open_mandate_hash.", + "context": { + "thread": "https://github.com/google-agentic-commerce/AP2/discussions/262", + "purpose": "Move open_mandate_hash interop from independent-convergence to spec-anchored. Two implementers agreeing is not an anchor; a deterministic vector set is. Seeded by AlgoVoi; AP2 community to own the canonical set long-term.", + "verification_recipe": [ + "1. For each vector, take mandate_body verbatim.", + "2. Canonicalise with RFC 8785 (rfc8785@0.1.4 or any conformant impl).", + "3. base64-encode the JCS bytes; it MUST equal expected_jcs_bytes_b64.", + "4. SHA-256 the JCS bytes, lowercase hex; MUST equal expected_open_mandate_hash.", + "5. Honour the pair expectations: same_hash_as / different_hash_from MUST hold." + ], + "pair_semantics": { + "object_key_order": "MUST be hash-identical (JCS sorts object keys)", + "array_order": "MUST differ (JCS preserves array order; do not sort arrays)", + "optional_fields": "MUST differ (presence != absence; not collapsed to default)", + "currency_minor_unit": "canonical integer-minor-unit form; no decimal/float", + "unicode_normalisation": "MUST differ (RFC 8785 does no Unicode normalisation)" + } + }, + "vectors": [ + { + "vector_id": "ap2-omh-v0-baseline-001", + "description": "Canonical reference Open Checkout Mandate, all required fields present.", + "pair_group": "baseline", + "expectation": "reference", + "mandate_body": { + "vct": "mandate.checkout.open.1", + "constraints": [ + { + "type": "checkout.allowed_merchants", + "allowed": [ + { + "id": "merch_acme", + "name": "Acme Tools", + "website": "https://acme.example" + } + ] + }, + { + "type": "checkout.line_items", + "items": [ + { + "id": "req_drill", + "acceptable_items": [ + { + "id": "SKU-DRILL-18V", + "title": "18V Cordless Drill", + "price": 8999 + }, + { + "id": "SKU-DRILL-12V", + "title": "12V Cordless Drill", + "price": 5999 + } + ], + "quantity": 1 + } + ] + } + ], + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + } + }, + "expected_jcs_bytes_b64": "eyJjbmYiOnsiandrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMTFxWUFZS3hDcmZWU183VHlXUUhPZzdoY3ZQYXBpTWxyd0lhYVBjSFVSbyJ9fSwiY29uc3RyYWludHMiOlt7ImFsbG93ZWQiOlt7ImlkIjoibWVyY2hfYWNtZSIsIm5hbWUiOiJBY21lIFRvb2xzIiwid2Vic2l0ZSI6Imh0dHBzOi8vYWNtZS5leGFtcGxlIn1dLCJ0eXBlIjoiY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMifSx7Iml0ZW1zIjpbeyJhY2NlcHRhYmxlX2l0ZW1zIjpbeyJpZCI6IlNLVS1EUklMTC0xOFYiLCJwcmljZSI6ODk5OSwidGl0bGUiOiIxOFYgQ29yZGxlc3MgRHJpbGwifSx7ImlkIjoiU0tVLURSSUxMLTEyViIsInByaWNlIjo1OTk5LCJ0aXRsZSI6IjEyViBDb3JkbGVzcyBEcmlsbCJ9XSwiaWQiOiJyZXFfZHJpbGwiLCJxdWFudGl0eSI6MX1dLCJ0eXBlIjoiY2hlY2tvdXQubGluZV9pdGVtcyJ9XSwidmN0IjoibWFuZGF0ZS5jaGVja291dC5vcGVuLjEifQ==", + "expected_open_mandate_hash": "sha256:4a407c36ab0619a9086756abb3f5c6cae8f80dff721dd3746cc0fa9256616683", + "notes": "The anchor. All other vectors are deltas against this body." + }, + { + "vector_id": "ap2-omh-v0-object-key-order-002", + "description": "Identical mandate with every object's keys in different source order.", + "pair_group": "object_key_order", + "expectation": "same_hash_as:ap2-omh-v0-baseline-001", + "mandate_body": { + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + }, + "constraints": [ + { + "allowed": [ + { + "website": "https://acme.example", + "name": "Acme Tools", + "id": "merch_acme" + } + ], + "type": "checkout.allowed_merchants" + }, + { + "items": [ + { + "quantity": 1, + "acceptable_items": [ + { + "price": 8999, + "title": "18V Cordless Drill", + "id": "SKU-DRILL-18V" + }, + { + "price": 5999, + "title": "12V Cordless Drill", + "id": "SKU-DRILL-12V" + } + ], + "id": "req_drill" + } + ], + "type": "checkout.line_items" + } + ], + "vct": "mandate.checkout.open.1" + }, + "expected_jcs_bytes_b64": "eyJjbmYiOnsiandrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMTFxWUFZS3hDcmZWU183VHlXUUhPZzdoY3ZQYXBpTWxyd0lhYVBjSFVSbyJ9fSwiY29uc3RyYWludHMiOlt7ImFsbG93ZWQiOlt7ImlkIjoibWVyY2hfYWNtZSIsIm5hbWUiOiJBY21lIFRvb2xzIiwid2Vic2l0ZSI6Imh0dHBzOi8vYWNtZS5leGFtcGxlIn1dLCJ0eXBlIjoiY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMifSx7Iml0ZW1zIjpbeyJhY2NlcHRhYmxlX2l0ZW1zIjpbeyJpZCI6IlNLVS1EUklMTC0xOFYiLCJwcmljZSI6ODk5OSwidGl0bGUiOiIxOFYgQ29yZGxlc3MgRHJpbGwifSx7ImlkIjoiU0tVLURSSUxMLTEyViIsInByaWNlIjo1OTk5LCJ0aXRsZSI6IjEyViBDb3JkbGVzcyBEcmlsbCJ9XSwiaWQiOiJyZXFfZHJpbGwiLCJxdWFudGl0eSI6MX1dLCJ0eXBlIjoiY2hlY2tvdXQubGluZV9pdGVtcyJ9XSwidmN0IjoibWFuZGF0ZS5jaGVja291dC5vcGVuLjEifQ==", + "expected_open_mandate_hash": "sha256:4a407c36ab0619a9086756abb3f5c6cae8f80dff721dd3746cc0fa9256616683", + "notes": "JCS (RFC 8785 sec 3.2.3) sorts object members by code unit. Object key order in the source MUST NOT affect open_mandate_hash. This vector MUST produce the byte-identical JCS output and hash as baseline-001." + }, + { + "vector_id": "ap2-omh-v0-array-order-003", + "description": "Baseline with acceptable_items[] order swapped.", + "pair_group": "array_order", + "expectation": "different_hash_from:ap2-omh-v0-baseline-001", + "mandate_body": { + "vct": "mandate.checkout.open.1", + "constraints": [ + { + "type": "checkout.allowed_merchants", + "allowed": [ + { + "id": "merch_acme", + "name": "Acme Tools", + "website": "https://acme.example" + } + ] + }, + { + "type": "checkout.line_items", + "items": [ + { + "id": "req_drill", + "acceptable_items": [ + { + "id": "SKU-DRILL-12V", + "title": "12V Cordless Drill", + "price": 5999 + }, + { + "id": "SKU-DRILL-18V", + "title": "18V Cordless Drill", + "price": 8999 + } + ], + "quantity": 1 + } + ] + } + ], + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + } + }, + "expected_jcs_bytes_b64": "eyJjbmYiOnsiandrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMTFxWUFZS3hDcmZWU183VHlXUUhPZzdoY3ZQYXBpTWxyd0lhYVBjSFVSbyJ9fSwiY29uc3RyYWludHMiOlt7ImFsbG93ZWQiOlt7ImlkIjoibWVyY2hfYWNtZSIsIm5hbWUiOiJBY21lIFRvb2xzIiwid2Vic2l0ZSI6Imh0dHBzOi8vYWNtZS5leGFtcGxlIn1dLCJ0eXBlIjoiY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMifSx7Iml0ZW1zIjpbeyJhY2NlcHRhYmxlX2l0ZW1zIjpbeyJpZCI6IlNLVS1EUklMTC0xMlYiLCJwcmljZSI6NTk5OSwidGl0bGUiOiIxMlYgQ29yZGxlc3MgRHJpbGwifSx7ImlkIjoiU0tVLURSSUxMLTE4ViIsInByaWNlIjo4OTk5LCJ0aXRsZSI6IjE4ViBDb3JkbGVzcyBEcmlsbCJ9XSwiaWQiOiJyZXFfZHJpbGwiLCJxdWFudGl0eSI6MX1dLCJ0eXBlIjoiY2hlY2tvdXQubGluZV9pdGVtcyJ9XSwidmN0IjoibWFuZGF0ZS5jaGVja291dC5vcGVuLjEifQ==", + "expected_open_mandate_hash": "sha256:754fc15813c3ca16bf23f09d4ccedb74d727f5d9226947e8a63c1518133c9ed9", + "notes": "JSON arrays are order-significant; JCS preserves array element order. An implementer that sorts arrays for 'canonicalisation' WILL diverge. This vector MUST produce a DIFFERENT hash from baseline-001. If yours matches baseline-001, your canonicaliser is incorrectly reordering arrays." + }, + { + "vector_id": "ap2-omh-v0-optional-fields-004", + "description": "Baseline plus optional iat and exp (Unix epoch integers).", + "pair_group": "optional_fields", + "expectation": "different_hash_from:ap2-omh-v0-baseline-001", + "mandate_body": { + "vct": "mandate.checkout.open.1", + "constraints": [ + { + "type": "checkout.allowed_merchants", + "allowed": [ + { + "id": "merch_acme", + "name": "Acme Tools", + "website": "https://acme.example" + } + ] + }, + { + "type": "checkout.line_items", + "items": [ + { + "id": "req_drill", + "acceptable_items": [ + { + "id": "SKU-DRILL-18V", + "title": "18V Cordless Drill", + "price": 8999 + }, + { + "id": "SKU-DRILL-12V", + "title": "12V Cordless Drill", + "price": 5999 + } + ], + "quantity": 1 + } + ] + } + ], + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + }, + "iat": 1747400000, + "exp": 1747403600 + }, + "expected_jcs_bytes_b64": "eyJjbmYiOnsiandrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMTFxWUFZS3hDcmZWU183VHlXUUhPZzdoY3ZQYXBpTWxyd0lhYVBjSFVSbyJ9fSwiY29uc3RyYWludHMiOlt7ImFsbG93ZWQiOlt7ImlkIjoibWVyY2hfYWNtZSIsIm5hbWUiOiJBY21lIFRvb2xzIiwid2Vic2l0ZSI6Imh0dHBzOi8vYWNtZS5leGFtcGxlIn1dLCJ0eXBlIjoiY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMifSx7Iml0ZW1zIjpbeyJhY2NlcHRhYmxlX2l0ZW1zIjpbeyJpZCI6IlNLVS1EUklMTC0xOFYiLCJwcmljZSI6ODk5OSwidGl0bGUiOiIxOFYgQ29yZGxlc3MgRHJpbGwifSx7ImlkIjoiU0tVLURSSUxMLTEyViIsInByaWNlIjo1OTk5LCJ0aXRsZSI6IjEyViBDb3JkbGVzcyBEcmlsbCJ9XSwiaWQiOiJyZXFfZHJpbGwiLCJxdWFudGl0eSI6MX1dLCJ0eXBlIjoiY2hlY2tvdXQubGluZV9pdGVtcyJ9XSwiZXhwIjoxNzQ3NDAzNjAwLCJpYXQiOjE3NDc0MDAwMDAsInZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIn0=", + "expected_open_mandate_hash": "sha256:6368fcf2c7d7aa63453ba310043b6a284f845cf48eef31eb1660aec36bce8052", + "notes": "Optional fields present vs absent are different mandates with different hashes; absence is not normalised to a default. iat/exp MUST be integers (Unix epoch), never strings or floats." + }, + { + "vector_id": "ap2-omh-v0-currency-minor-unit-005", + "description": "Price expressed as ISO-4217 integer minor units (1050 == 10.50 in a cent currency).", + "pair_group": "currency_minor_unit", + "expectation": "canonical_form", + "mandate_body": { + "vct": "mandate.checkout.open.1", + "constraints": [ + { + "type": "checkout.allowed_merchants", + "allowed": [ + { + "id": "merch_acme", + "name": "Acme Tools", + "website": "https://acme.example" + } + ] + }, + { + "type": "checkout.line_items", + "items": [ + { + "id": "req_drill", + "acceptable_items": [ + { + "id": "SKU-WIDGET", + "title": "Widget", + "price": 1050 + } + ], + "quantity": 1 + } + ] + } + ], + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + } + }, + "expected_jcs_bytes_b64": "eyJjbmYiOnsiandrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMTFxWUFZS3hDcmZWU183VHlXUUhPZzdoY3ZQYXBpTWxyd0lhYVBjSFVSbyJ9fSwiY29uc3RyYWludHMiOlt7ImFsbG93ZWQiOlt7ImlkIjoibWVyY2hfYWNtZSIsIm5hbWUiOiJBY21lIFRvb2xzIiwid2Vic2l0ZSI6Imh0dHBzOi8vYWNtZS5leGFtcGxlIn1dLCJ0eXBlIjoiY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMifSx7Iml0ZW1zIjpbeyJhY2NlcHRhYmxlX2l0ZW1zIjpbeyJpZCI6IlNLVS1XSURHRVQiLCJwcmljZSI6MTA1MCwidGl0bGUiOiJXaWRnZXQifV0sImlkIjoicmVxX2RyaWxsIiwicXVhbnRpdHkiOjF9XSwidHlwZSI6ImNoZWNrb3V0LmxpbmVfaXRlbXMifV0sInZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIn0=", + "expected_open_mandate_hash": "sha256:cd294f91a53366dbf8d50e5850605c2fec30d6eb3378df7e1192944f4284f9dc", + "notes": "AP2 item.price and amount.amount are integers in the currency minor unit (amount.json / item.json). 10.50 MUST be encoded 1050, never 10.50, \"10.50\", or 10.5. A float/decimal encoding is non-conformant and will not match this vector's hash." + }, + { + "vector_id": "ap2-omh-v0-unicode-nfc-006a", + "description": "merchant.name in Unicode NFC (composed) form.", + "pair_group": "unicode_normalisation", + "expectation": "different_hash_from:ap2-omh-v0-unicode-nfd-006b", + "mandate_body": { + "vct": "mandate.checkout.open.1", + "constraints": [ + { + "type": "checkout.allowed_merchants", + "allowed": [ + { + "id": "merch_acme", + "name": "Café Solé", + "website": "https://acme.example" + } + ] + }, + { + "type": "checkout.line_items", + "items": [ + { + "id": "req_drill", + "acceptable_items": [ + { + "id": "SKU-DRILL-18V", + "title": "18V Cordless Drill", + "price": 8999 + }, + { + "id": "SKU-DRILL-12V", + "title": "12V Cordless Drill", + "price": 5999 + } + ], + "quantity": 1 + } + ] + } + ], + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + } + }, + "expected_jcs_bytes_b64": "eyJjbmYiOnsiandrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMTFxWUFZS3hDcmZWU183VHlXUUhPZzdoY3ZQYXBpTWxyd0lhYVBjSFVSbyJ9fSwiY29uc3RyYWludHMiOlt7ImFsbG93ZWQiOlt7ImlkIjoibWVyY2hfYWNtZSIsIm5hbWUiOiJDYWbDqSBTb2zDqSIsIndlYnNpdGUiOiJodHRwczovL2FjbWUuZXhhbXBsZSJ9XSwidHlwZSI6ImNoZWNrb3V0LmFsbG93ZWRfbWVyY2hhbnRzIn0seyJpdGVtcyI6W3siYWNjZXB0YWJsZV9pdGVtcyI6W3siaWQiOiJTS1UtRFJJTEwtMThWIiwicHJpY2UiOjg5OTksInRpdGxlIjoiMThWIENvcmRsZXNzIERyaWxsIn0seyJpZCI6IlNLVS1EUklMTC0xMlYiLCJwcmljZSI6NTk5OSwidGl0bGUiOiIxMlYgQ29yZGxlc3MgRHJpbGwifV0sImlkIjoicmVxX2RyaWxsIiwicXVhbnRpdHkiOjF9XSwidHlwZSI6ImNoZWNrb3V0LmxpbmVfaXRlbXMifV0sInZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIn0=", + "expected_open_mandate_hash": "sha256:dd0330b1d02b1f83ade5821d64f9f5abf75d1af00575e8fa39d68a8bdeadfb31", + "notes": "RFC 8785 performs NO Unicode normalisation. The serialised string is the exact code points supplied. NFC and NFD forms of the same glyph produce DIFFERENT open_mandate_hash. Implementers MUST NOT normalise merchant strings before hashing; the signed bytes are authoritative." + }, + { + "vector_id": "ap2-omh-v0-unicode-nfd-006b", + "description": "Same mandate, merchant.name in Unicode NFD (decomposed) form.", + "pair_group": "unicode_normalisation", + "expectation": "different_hash_from:ap2-omh-v0-unicode-nfc-006a", + "mandate_body": { + "vct": "mandate.checkout.open.1", + "constraints": [ + { + "type": "checkout.allowed_merchants", + "allowed": [ + { + "id": "merch_acme", + "name": "Café Solé", + "website": "https://acme.example" + } + ] + }, + { + "type": "checkout.line_items", + "items": [ + { + "id": "req_drill", + "acceptable_items": [ + { + "id": "SKU-DRILL-18V", + "title": "18V Cordless Drill", + "price": 8999 + }, + { + "id": "SKU-DRILL-12V", + "title": "12V Cordless Drill", + "price": 5999 + } + ], + "quantity": 1 + } + ] + } + ], + "cnf": { + "jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + } + }, + "expected_jcs_bytes_b64": "eyJjbmYiOnsiandrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMTFxWUFZS3hDcmZWU183VHlXUUhPZzdoY3ZQYXBpTWxyd0lhYVBjSFVSbyJ9fSwiY29uc3RyYWludHMiOlt7ImFsbG93ZWQiOlt7ImlkIjoibWVyY2hfYWNtZSIsIm5hbWUiOiJDYWZlzIEgU29sZcyBIiwid2Vic2l0ZSI6Imh0dHBzOi8vYWNtZS5leGFtcGxlIn1dLCJ0eXBlIjoiY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMifSx7Iml0ZW1zIjpbeyJhY2NlcHRhYmxlX2l0ZW1zIjpbeyJpZCI6IlNLVS1EUklMTC0xOFYiLCJwcmljZSI6ODk5OSwidGl0bGUiOiIxOFYgQ29yZGxlc3MgRHJpbGwifSx7ImlkIjoiU0tVLURSSUxMLTEyViIsInByaWNlIjo1OTk5LCJ0aXRsZSI6IjEyViBDb3JkbGVzcyBEcmlsbCJ9XSwiaWQiOiJyZXFfZHJpbGwiLCJxdWFudGl0eSI6MX1dLCJ0eXBlIjoiY2hlY2tvdXQubGluZV9pdGVtcyJ9XSwidmN0IjoibWFuZGF0ZS5jaGVja291dC5vcGVuLjEifQ==", + "expected_open_mandate_hash": "sha256:f8ae25428c8f1eb7ffaf49da13366c5e76db95de76f237e01f98b6ec8e4247e9", + "notes": "Pair of 006a. If 006a and 006b produce the same hash, your pipeline is Unicode-normalising and is non-conformant with RFC 8785." + } + ] +} \ No newline at end of file From a655571584f202c01353744ba8419d9cf3474e2d Mon Sep 17 00:00:00 2001 From: iLoveChicken Date: Sat, 6 Jun 2026 05:06:59 +0100 Subject: [PATCH 02/14] fix(spellcheck): US spelling + add proper nouns to custom-words.txt --- .cspell/custom-words.txt | 8 ++++++++ .../ap2/conformance/open_mandate_hash/README.md | 10 +++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index ce73c361..975347ac 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -182,6 +182,14 @@ xerrors xmocksignature xrandomnonce XVCJ +amavashev +canonicalizer +cyberphone +Erdtman +gowebpki +Rundgren +serde +seritalien Yapily Zalopay Zalora diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md b/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md index 5e11fd40..f936d444 100644 --- a/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md +++ b/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md @@ -40,11 +40,11 @@ Each vector is structured as: | `array-order-003` | `different_hash_from:baseline-001` | Arrays are order-significant | | `optional-fields-004` | `different_hash_from:baseline-001` | Presence ≠ absence | | `currency-minor-unit-005` | canonical form | Integer minor units only | -| `unicode-nfc-006a` | `different_hash_from:unicode-nfd-006b` | No Unicode normalisation | -| `unicode-nfd-006b` | `different_hash_from:unicode-nfc-006a` | No Unicode normalisation | +| `unicode-nfc-006a` | `different_hash_from:unicode-nfd-006b` | No Unicode normalization | +| `unicode-nfd-006b` | `different_hash_from:unicode-nfc-006a` | No Unicode normalization | The array-order and Unicode pairs catch the divergences most commonly seen -in practice: implementations that sort arrays or NFC-normalise strings will +in practice: implementations that sort arrays or NFC-normalize strings will fail the corresponding pair invariant immediately. ## Cross-implementation validation @@ -70,8 +70,8 @@ Full validation history: [AP2 issue #265](https://github.com/google-agentic-comm - **Array sorting** — Do not sort arrays before hashing. JCS preserves array element order. Vector 003 catches this. -- **Unicode normalisation** — Do not NFC-normalise strings before hashing. - RFC 8785 makes no Unicode normalisation. Vectors 006a/006b catch this. +- **Unicode normalization** — Do not NFC-normalize strings before hashing. + RFC 8785 makes no Unicode normalization. Vectors 006a/006b catch this. - **Float/decimal prices** — `item.price` and `amount.amount` are integers in the currency minor unit. `10.50` MUST be encoded as `1050`. Vector From 8ed86e952111bf1a6074bb531066009ca8b7cc4b Mon Sep 17 00:00:00 2001 From: iLoveChicken Date: Sat, 6 Jun 2026 05:14:42 +0100 Subject: [PATCH 03/14] fix(lint): US spelling, add custom words, exclude web-client from Biome --- .cspell/custom-words.txt | 4 ++++ biome.json | 5 +++++ .../conformance/open_mandate_hash/vectors-v0.json | 14 +++++++------- 3 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 biome.json diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index 975347ac..52f54438 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -187,6 +187,10 @@ canonicalizer cyberphone Erdtman gowebpki +Iaap +merch +Mlrw +Papi Rundgren serde seritalien diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..b867da0b --- /dev/null +++ b/biome.json @@ -0,0 +1,5 @@ +{ + "files": { + "includes": ["**", "!code/web-client"] + } +} diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/vectors-v0.json b/code/sdk/schemas/ap2/conformance/open_mandate_hash/vectors-v0.json index ab926f39..bf00ac2f 100644 --- a/code/sdk/schemas/ap2/conformance/open_mandate_hash/vectors-v0.json +++ b/code/sdk/schemas/ap2/conformance/open_mandate_hash/vectors-v0.json @@ -15,7 +15,7 @@ "purpose": "Move open_mandate_hash interop from independent-convergence to spec-anchored. Two implementers agreeing is not an anchor; a deterministic vector set is. Seeded by AlgoVoi; AP2 community to own the canonical set long-term.", "verification_recipe": [ "1. For each vector, take mandate_body verbatim.", - "2. Canonicalise with RFC 8785 (rfc8785@0.1.4 or any conformant impl).", + "2. Canonicalize with RFC 8785 (rfc8785@0.1.4 or any conformant impl).", "3. base64-encode the JCS bytes; it MUST equal expected_jcs_bytes_b64.", "4. SHA-256 the JCS bytes, lowercase hex; MUST equal expected_open_mandate_hash.", "5. Honour the pair expectations: same_hash_as / different_hash_from MUST hold." @@ -25,7 +25,7 @@ "array_order": "MUST differ (JCS preserves array order; do not sort arrays)", "optional_fields": "MUST differ (presence != absence; not collapsed to default)", "currency_minor_unit": "canonical integer-minor-unit form; no decimal/float", - "unicode_normalisation": "MUST differ (RFC 8785 does no Unicode normalisation)" + "unicode_normalization": "MUST differ (RFC 8785 does no Unicode normalization)" } }, "vectors": [ @@ -183,7 +183,7 @@ }, "expected_jcs_bytes_b64": "eyJjbmYiOnsiandrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMTFxWUFZS3hDcmZWU183VHlXUUhPZzdoY3ZQYXBpTWxyd0lhYVBjSFVSbyJ9fSwiY29uc3RyYWludHMiOlt7ImFsbG93ZWQiOlt7ImlkIjoibWVyY2hfYWNtZSIsIm5hbWUiOiJBY21lIFRvb2xzIiwid2Vic2l0ZSI6Imh0dHBzOi8vYWNtZS5leGFtcGxlIn1dLCJ0eXBlIjoiY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMifSx7Iml0ZW1zIjpbeyJhY2NlcHRhYmxlX2l0ZW1zIjpbeyJpZCI6IlNLVS1EUklMTC0xMlYiLCJwcmljZSI6NTk5OSwidGl0bGUiOiIxMlYgQ29yZGxlc3MgRHJpbGwifSx7ImlkIjoiU0tVLURSSUxMLTE4ViIsInByaWNlIjo4OTk5LCJ0aXRsZSI6IjE4ViBDb3JkbGVzcyBEcmlsbCJ9XSwiaWQiOiJyZXFfZHJpbGwiLCJxdWFudGl0eSI6MX1dLCJ0eXBlIjoiY2hlY2tvdXQubGluZV9pdGVtcyJ9XSwidmN0IjoibWFuZGF0ZS5jaGVja291dC5vcGVuLjEifQ==", "expected_open_mandate_hash": "sha256:754fc15813c3ca16bf23f09d4ccedb74d727f5d9226947e8a63c1518133c9ed9", - "notes": "JSON arrays are order-significant; JCS preserves array element order. An implementer that sorts arrays for 'canonicalisation' WILL diverge. This vector MUST produce a DIFFERENT hash from baseline-001. If yours matches baseline-001, your canonicaliser is incorrectly reordering arrays." + "notes": "JSON arrays are order-significant; JCS preserves array element order. An implementer that sorts arrays for 'canonicalization' WILL diverge. This vector MUST produce a DIFFERENT hash from baseline-001. If yours matches baseline-001, your canonicalizer is incorrectly reordering arrays." }, { "vector_id": "ap2-omh-v0-optional-fields-004", @@ -237,7 +237,7 @@ }, "expected_jcs_bytes_b64": "eyJjbmYiOnsiandrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMTFxWUFZS3hDcmZWU183VHlXUUhPZzdoY3ZQYXBpTWxyd0lhYVBjSFVSbyJ9fSwiY29uc3RyYWludHMiOlt7ImFsbG93ZWQiOlt7ImlkIjoibWVyY2hfYWNtZSIsIm5hbWUiOiJBY21lIFRvb2xzIiwid2Vic2l0ZSI6Imh0dHBzOi8vYWNtZS5leGFtcGxlIn1dLCJ0eXBlIjoiY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMifSx7Iml0ZW1zIjpbeyJhY2NlcHRhYmxlX2l0ZW1zIjpbeyJpZCI6IlNLVS1EUklMTC0xOFYiLCJwcmljZSI6ODk5OSwidGl0bGUiOiIxOFYgQ29yZGxlc3MgRHJpbGwifSx7ImlkIjoiU0tVLURSSUxMLTEyViIsInByaWNlIjo1OTk5LCJ0aXRsZSI6IjEyViBDb3JkbGVzcyBEcmlsbCJ9XSwiaWQiOiJyZXFfZHJpbGwiLCJxdWFudGl0eSI6MX1dLCJ0eXBlIjoiY2hlY2tvdXQubGluZV9pdGVtcyJ9XSwiZXhwIjoxNzQ3NDAzNjAwLCJpYXQiOjE3NDc0MDAwMDAsInZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIn0=", "expected_open_mandate_hash": "sha256:6368fcf2c7d7aa63453ba310043b6a284f845cf48eef31eb1660aec36bce8052", - "notes": "Optional fields present vs absent are different mandates with different hashes; absence is not normalised to a default. iat/exp MUST be integers (Unix epoch), never strings or floats." + "notes": "Optional fields present vs absent are different mandates with different hashes; absence is not normalized to a default. iat/exp MUST be integers (Unix epoch), never strings or floats." }, { "vector_id": "ap2-omh-v0-currency-minor-unit-005", @@ -289,7 +289,7 @@ { "vector_id": "ap2-omh-v0-unicode-nfc-006a", "description": "merchant.name in Unicode NFC (composed) form.", - "pair_group": "unicode_normalisation", + "pair_group": "unicode_normalization", "expectation": "different_hash_from:ap2-omh-v0-unicode-nfd-006b", "mandate_body": { "vct": "mandate.checkout.open.1", @@ -336,12 +336,12 @@ }, "expected_jcs_bytes_b64": "eyJjbmYiOnsiandrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMTFxWUFZS3hDcmZWU183VHlXUUhPZzdoY3ZQYXBpTWxyd0lhYVBjSFVSbyJ9fSwiY29uc3RyYWludHMiOlt7ImFsbG93ZWQiOlt7ImlkIjoibWVyY2hfYWNtZSIsIm5hbWUiOiJDYWbDqSBTb2zDqSIsIndlYnNpdGUiOiJodHRwczovL2FjbWUuZXhhbXBsZSJ9XSwidHlwZSI6ImNoZWNrb3V0LmFsbG93ZWRfbWVyY2hhbnRzIn0seyJpdGVtcyI6W3siYWNjZXB0YWJsZV9pdGVtcyI6W3siaWQiOiJTS1UtRFJJTEwtMThWIiwicHJpY2UiOjg5OTksInRpdGxlIjoiMThWIENvcmRsZXNzIERyaWxsIn0seyJpZCI6IlNLVS1EUklMTC0xMlYiLCJwcmljZSI6NTk5OSwidGl0bGUiOiIxMlYgQ29yZGxlc3MgRHJpbGwifV0sImlkIjoicmVxX2RyaWxsIiwicXVhbnRpdHkiOjF9XSwidHlwZSI6ImNoZWNrb3V0LmxpbmVfaXRlbXMifV0sInZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIn0=", "expected_open_mandate_hash": "sha256:dd0330b1d02b1f83ade5821d64f9f5abf75d1af00575e8fa39d68a8bdeadfb31", - "notes": "RFC 8785 performs NO Unicode normalisation. The serialised string is the exact code points supplied. NFC and NFD forms of the same glyph produce DIFFERENT open_mandate_hash. Implementers MUST NOT normalise merchant strings before hashing; the signed bytes are authoritative." + "notes": "RFC 8785 performs NO Unicode normalization. The serialised string is the exact code points supplied. NFC and NFD forms of the same glyph produce DIFFERENT open_mandate_hash. Implementers MUST NOT normalize merchant strings before hashing; the signed bytes are authoritative." }, { "vector_id": "ap2-omh-v0-unicode-nfd-006b", "description": "Same mandate, merchant.name in Unicode NFD (decomposed) form.", - "pair_group": "unicode_normalisation", + "pair_group": "unicode_normalization", "expectation": "different_hash_from:ap2-omh-v0-unicode-nfc-006a", "mandate_body": { "vct": "mandate.checkout.open.1", From cc8d63a2e55e0d815a3a45ad1b31c38cd1f83907 Mon Sep 17 00:00:00 2001 From: iLoveChicken Date: Sat, 6 Jun 2026 05:17:55 +0100 Subject: [PATCH 04/14] =?UTF-8?q?fix(spellcheck):=20US=20spelling=20(seria?= =?UTF-8?q?lized/normalizing)=20+=20add=20Sol=C3=A9=20to=20custom=20words?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cspell/custom-words.txt | 1 + .../schemas/ap2/conformance/open_mandate_hash/vectors-v0.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index 52f54438..b397685a 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -194,6 +194,7 @@ Papi Rundgren serde seritalien +Solé Yapily Zalopay Zalora diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/vectors-v0.json b/code/sdk/schemas/ap2/conformance/open_mandate_hash/vectors-v0.json index bf00ac2f..f4388207 100644 --- a/code/sdk/schemas/ap2/conformance/open_mandate_hash/vectors-v0.json +++ b/code/sdk/schemas/ap2/conformance/open_mandate_hash/vectors-v0.json @@ -336,7 +336,7 @@ }, "expected_jcs_bytes_b64": "eyJjbmYiOnsiandrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMTFxWUFZS3hDcmZWU183VHlXUUhPZzdoY3ZQYXBpTWxyd0lhYVBjSFVSbyJ9fSwiY29uc3RyYWludHMiOlt7ImFsbG93ZWQiOlt7ImlkIjoibWVyY2hfYWNtZSIsIm5hbWUiOiJDYWbDqSBTb2zDqSIsIndlYnNpdGUiOiJodHRwczovL2FjbWUuZXhhbXBsZSJ9XSwidHlwZSI6ImNoZWNrb3V0LmFsbG93ZWRfbWVyY2hhbnRzIn0seyJpdGVtcyI6W3siYWNjZXB0YWJsZV9pdGVtcyI6W3siaWQiOiJTS1UtRFJJTEwtMThWIiwicHJpY2UiOjg5OTksInRpdGxlIjoiMThWIENvcmRsZXNzIERyaWxsIn0seyJpZCI6IlNLVS1EUklMTC0xMlYiLCJwcmljZSI6NTk5OSwidGl0bGUiOiIxMlYgQ29yZGxlc3MgRHJpbGwifV0sImlkIjoicmVxX2RyaWxsIiwicXVhbnRpdHkiOjF9XSwidHlwZSI6ImNoZWNrb3V0LmxpbmVfaXRlbXMifV0sInZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIn0=", "expected_open_mandate_hash": "sha256:dd0330b1d02b1f83ade5821d64f9f5abf75d1af00575e8fa39d68a8bdeadfb31", - "notes": "RFC 8785 performs NO Unicode normalization. The serialised string is the exact code points supplied. NFC and NFD forms of the same glyph produce DIFFERENT open_mandate_hash. Implementers MUST NOT normalize merchant strings before hashing; the signed bytes are authoritative." + "notes": "RFC 8785 performs NO Unicode normalization. The serialized string is the exact code points supplied. NFC and NFD forms of the same glyph produce DIFFERENT open_mandate_hash. Implementers MUST NOT normalize merchant strings before hashing; the signed bytes are authoritative." }, { "vector_id": "ap2-omh-v0-unicode-nfd-006b", @@ -388,7 +388,7 @@ }, "expected_jcs_bytes_b64": "eyJjbmYiOnsiandrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiMTFxWUFZS3hDcmZWU183VHlXUUhPZzdoY3ZQYXBpTWxyd0lhYVBjSFVSbyJ9fSwiY29uc3RyYWludHMiOlt7ImFsbG93ZWQiOlt7ImlkIjoibWVyY2hfYWNtZSIsIm5hbWUiOiJDYWZlzIEgU29sZcyBIiwid2Vic2l0ZSI6Imh0dHBzOi8vYWNtZS5leGFtcGxlIn1dLCJ0eXBlIjoiY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMifSx7Iml0ZW1zIjpbeyJhY2NlcHRhYmxlX2l0ZW1zIjpbeyJpZCI6IlNLVS1EUklMTC0xOFYiLCJwcmljZSI6ODk5OSwidGl0bGUiOiIxOFYgQ29yZGxlc3MgRHJpbGwifSx7ImlkIjoiU0tVLURSSUxMLTEyViIsInByaWNlIjo1OTk5LCJ0aXRsZSI6IjEyViBDb3JkbGVzcyBEcmlsbCJ9XSwiaWQiOiJyZXFfZHJpbGwiLCJxdWFudGl0eSI6MX1dLCJ0eXBlIjoiY2hlY2tvdXQubGluZV9pdGVtcyJ9XSwidmN0IjoibWFuZGF0ZS5jaGVja291dC5vcGVuLjEifQ==", "expected_open_mandate_hash": "sha256:f8ae25428c8f1eb7ffaf49da13366c5e76db95de76f237e01f98b6ec8e4247e9", - "notes": "Pair of 006a. If 006a and 006b produce the same hash, your pipeline is Unicode-normalising and is non-conformant with RFC 8785." + "notes": "Pair of 006a. If 006a and 006b produce the same hash, your pipeline is Unicode-normalizing and is non-conformant with RFC 8785." } ] } \ No newline at end of file From 4a8a292740c3bdf94abc01e4dcd747e2f2a4b595 Mon Sep 17 00:00:00 2001 From: iLoveChicken Date: Sat, 6 Jun 2026 05:21:05 +0100 Subject: [PATCH 05/14] fix(spellcheck): add AlgoVoi, JCS and jcs to custom words --- .cspell/custom-words.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index b397685a..be0e2eb2 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -182,12 +182,16 @@ xerrors xmocksignature xrandomnonce XVCJ +AlgoVoi +algovoi amavashev canonicalizer cyberphone Erdtman gowebpki Iaap +JCS +jcs merch Mlrw Papi From df0b0579e90fb635d2d7180f103e5152c07b1f38 Mon Sep 17 00:00:00 2001 From: iLoveChicken Date: Sat, 6 Jun 2026 05:27:01 +0100 Subject: [PATCH 06/14] fix: base64 description, full vector IDs in table, trailing newline (Gemini review) --- .../ap2/conformance/open_mandate_hash/README.md | 16 ++++++++-------- .../open_mandate_hash/vectors-v0.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md b/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md index f936d444..380fbf47 100644 --- a/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md +++ b/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md @@ -27,7 +27,7 @@ Each vector is structured as: { "vector_id": "ap2-omh-v0-", "mandate_body": { ... }, - "expected_jcs_bytes_b64": "", + "expected_jcs_bytes_b64": "", "expected_open_mandate_hash": "sha256:", "expectation": "reference | same_hash_as: | different_hash_from:" } @@ -35,13 +35,13 @@ Each vector is structured as: | Vector | Pair invariant | Tests | |---|---|---| -| `baseline-001` | reference | Canonical baseline | -| `object-key-order-002` | `same_hash_as:baseline-001` | JCS sorts object members | -| `array-order-003` | `different_hash_from:baseline-001` | Arrays are order-significant | -| `optional-fields-004` | `different_hash_from:baseline-001` | Presence ≠ absence | -| `currency-minor-unit-005` | canonical form | Integer minor units only | -| `unicode-nfc-006a` | `different_hash_from:unicode-nfd-006b` | No Unicode normalization | -| `unicode-nfd-006b` | `different_hash_from:unicode-nfc-006a` | No Unicode normalization | +| `ap2-omh-v0-baseline-001` | reference | Canonical baseline | +| `ap2-omh-v0-object-key-order-002` | `same_hash_as:ap2-omh-v0-baseline-001` | JCS sorts object members | +| `ap2-omh-v0-array-order-003` | `different_hash_from:ap2-omh-v0-baseline-001` | Arrays are order-significant | +| `ap2-omh-v0-optional-fields-004` | `different_hash_from:ap2-omh-v0-baseline-001` | Presence ≠ absence | +| `ap2-omh-v0-currency-minor-unit-005` | canonical form | Integer minor units only | +| `ap2-omh-v0-unicode-nfc-006a` | `different_hash_from:ap2-omh-v0-unicode-nfd-006b` | No Unicode normalization | +| `ap2-omh-v0-unicode-nfd-006b` | `different_hash_from:ap2-omh-v0-unicode-nfc-006a` | No Unicode normalization | The array-order and Unicode pairs catch the divergences most commonly seen in practice: implementations that sort arrays or NFC-normalize strings will diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/vectors-v0.json b/code/sdk/schemas/ap2/conformance/open_mandate_hash/vectors-v0.json index f4388207..e7907493 100644 --- a/code/sdk/schemas/ap2/conformance/open_mandate_hash/vectors-v0.json +++ b/code/sdk/schemas/ap2/conformance/open_mandate_hash/vectors-v0.json @@ -391,4 +391,4 @@ "notes": "Pair of 006a. If 006a and 006b produce the same hash, your pipeline is Unicode-normalizing and is non-conformant with RFC 8785." } ] -} \ No newline at end of file +} From e4cb6cdb7355c36270f60e0e0c63fbd12da39c42 Mon Sep 17 00:00:00 2001 From: iLoveChicken Date: Sat, 6 Jun 2026 05:31:53 +0100 Subject: [PATCH 07/14] fix(lint): add language to fenced code block (MD040), fix table separators (MD060) --- .../sdk/schemas/ap2/conformance/open_mandate_hash/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md b/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md index 380fbf47..5e08bcac 100644 --- a/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md +++ b/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md @@ -6,7 +6,7 @@ Conformance vectors for the `open_mandate_hash` derivation rule for ## Derivation rule -``` +```text open_mandate_hash = SHA-256(JCS_RFC8785(unsigned_open_checkout_mandate_body)) ``` @@ -34,7 +34,7 @@ Each vector is structured as: ``` | Vector | Pair invariant | Tests | -|---|---|---| +| --- | --- | --- | | `ap2-omh-v0-baseline-001` | reference | Canonical baseline | | `ap2-omh-v0-object-key-order-002` | `same_hash_as:ap2-omh-v0-baseline-001` | JCS sorts object members | | `ap2-omh-v0-array-order-003` | `different_hash_from:ap2-omh-v0-baseline-001` | Arrays are order-significant | @@ -50,7 +50,7 @@ fail the corresponding pair invariant immediately. ## Cross-implementation validation | Implementation | Language | Library | Result | -|---|---|---|---| +| --- | --- | --- | --- | | `rfc8785@0.1.4` | Python | Trail of Bits | 7/7 + 4/4 pair invariants ✓ | | `canonicalize@3.0.0` | JavaScript | Erdtman + Rundgren (RFC 8785 author) | 7/7 + 4/4 ✓ | | `gowebpki/jcs v1.0.1` | Go | @amavashev (AP2 maintainer) | 7/7 + 4/4 ✓ | From 32b1c5ee069a75c43986dce5832035f399ae6fc1 Mon Sep 17 00:00:00 2001 From: iLoveChicken Date: Sat, 6 Jun 2026 05:42:57 +0100 Subject: [PATCH 08/14] feat(conformance): add 4-language runner scripts and expand validation table --- .../open_mandate_hash/JcsRunner.java | 187 ++++++++++++++++++ .../conformance/open_mandate_hash/README.md | 48 ++++- .../open_mandate_hash/runner_go.go | 130 ++++++++++++ .../open_mandate_hash/runner_node.js | 75 +++++++ .../open_mandate_hash/runner_python.py | 88 +++++++++ 5 files changed, 520 insertions(+), 8 deletions(-) create mode 100644 code/sdk/schemas/ap2/conformance/open_mandate_hash/JcsRunner.java create mode 100644 code/sdk/schemas/ap2/conformance/open_mandate_hash/runner_go.go create mode 100644 code/sdk/schemas/ap2/conformance/open_mandate_hash/runner_node.js create mode 100644 code/sdk/schemas/ap2/conformance/open_mandate_hash/runner_python.py diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/JcsRunner.java b/code/sdk/schemas/ap2/conformance/open_mandate_hash/JcsRunner.java new file mode 100644 index 00000000..0fb4d93d --- /dev/null +++ b/code/sdk/schemas/ap2/conformance/open_mandate_hash/JcsRunner.java @@ -0,0 +1,187 @@ +// JcsRunner.java — cyberphone/json-canonicalization runner for AP2 open_mandate_hash v0. +// +// Uses Anders Rundgren's Java reference impl (the one cited in RFC 8785). +// The actual canonicalization is the unmodified JsonCanonicalizer class. +// This file adds only: +// - a minimal JSON extractor to locate vector framing fields (mandate_body, +// expected_open_mandate_hash, expectation, vector_id) within ap2-omh-v0.json +// - SHA-256 + base64 wrappers around JsonCanonicalizer's byte output +// - pair-invariant verification +// +// The minimal extractor uses brace-counting on the artefact file. Because the +// artefact shape is fixed (we publish it), this is safe; it is NOT a general +// JSON parser. Canonicalization correctness lives entirely in JsonCanonicalizer. +// +// Build: +// git clone --depth 1 https://github.com/cyberphone/json-canonicalization.git +// javac -d classes \ +// -sourcepath json-canonicalization/java/canonicalizer/src \ +// json-canonicalization/java/canonicalizer/src/org/webpki/jcs/JsonCanonicalizer.java \ +// JcsRunner.java +// java -cp classes JcsRunner ap2-omh-v0.json + +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.security.MessageDigest; +import java.util.*; + +import org.webpki.jcs.JsonCanonicalizer; + +public class JcsRunner { + + static String toHex(byte[] b) { + StringBuilder sb = new StringBuilder(); + for (byte x : b) sb.append(String.format("%02x", x & 0xff)); + return sb.toString(); + } + + // ── minimal artefact-specific JSON extractor ──────────────────────────── + // Finds occurrences of `"key"` at top of each vector object and returns + // the next value: a JSON object/array (returned verbatim by brace count), + // a JSON string (returned with quotes), or a bare literal. + // ONLY used to slice ap2-omh-v0.json into per-vector chunks; canonicalization + // is delegated to JsonCanonicalizer. + static List findVectorRanges(String src) { + // Locate the top-level "vectors": [ … ] array and return the byte + // ranges of each object inside it. + int vIdx = src.indexOf("\"vectors\""); + if (vIdx < 0) throw new IllegalArgumentException("no 'vectors' key"); + int br = src.indexOf('[', vIdx); + List ranges = new ArrayList<>(); + int depth = 0; + boolean inStr = false, esc = false; + int objStart = -1; + for (int i = br + 1; i < src.length(); i++) { + char c = src.charAt(i); + if (esc) { esc = false; continue; } + if (c == '\\') { esc = true; continue; } + if (c == '"') { inStr = !inStr; continue; } + if (inStr) continue; + if (c == '{') { + if (depth == 0) objStart = i; + depth++; + } else if (c == '}') { + depth--; + if (depth == 0) { + ranges.add(new int[]{objStart, i + 1}); + } + } else if (c == ']' && depth == 0) { + break; + } + } + return ranges; + } + + static String extractField(String vector, String key) { + // Find the value following "key": within a single vector JSON object. + String needle = "\"" + key + "\""; + int idx = vector.indexOf(needle); + if (idx < 0) return null; + int colon = vector.indexOf(':', idx + needle.length()); + if (colon < 0) return null; + int i = colon + 1; + while (i < vector.length() && Character.isWhitespace(vector.charAt(i))) i++; + if (i >= vector.length()) return null; + char open = vector.charAt(i); + + if (open == '{' || open == '[') { + char close = open == '{' ? '}' : ']'; + int depth = 0; + boolean inStr = false, esc = false; + int start = i; + for (; i < vector.length(); i++) { + char c = vector.charAt(i); + if (esc) { esc = false; continue; } + if (c == '\\') { esc = true; continue; } + if (c == '"') { inStr = !inStr; continue; } + if (inStr) continue; + if (c == open) depth++; + else if (c == close) { + depth--; + if (depth == 0) return vector.substring(start, i + 1); + } + } + } else if (open == '"') { + int start = i + 1; + boolean esc = false; + for (i = start; i < vector.length(); i++) { + char c = vector.charAt(i); + if (esc) { esc = false; continue; } + if (c == '\\') { esc = true; continue; } + if (c == '"') return vector.substring(start, i); + } + } + return null; + } + + public static void main(String[] args) throws Exception { + if (args.length < 1) { + System.err.println("usage: JcsRunner ap2-omh-v0.json"); + System.exit(2); + } + String src = new String(Files.readAllBytes(Paths.get(args[0])), + StandardCharsets.UTF_8); + List ranges = findVectorRanges(src); + + MessageDigest md = MessageDigest.getInstance("SHA-256"); + Map computed = new LinkedHashMap<>(); + Map expectations = new LinkedHashMap<>(); + int pass = 0, fail = 0; + + for (int[] r : ranges) { + String vec = src.substring(r[0], r[1]); + String vectorId = extractField(vec, "vector_id"); + String body = extractField(vec, "mandate_body"); + String expectedSha = extractField(vec, "expected_open_mandate_hash"); + String expectedB64 = extractField(vec, "expected_jcs_bytes_b64"); + String expectation = extractField(vec, "expectation"); + + if (expectedSha != null && expectedSha.startsWith("sha256:")) { + expectedSha = expectedSha.substring("sha256:".length()); + } + expectations.put(vectorId, expectation == null ? "" : expectation); + + JsonCanonicalizer jc = new JsonCanonicalizer(body); + byte[] jcsBytes = jc.getEncodedUTF8(); + md.reset(); + String sha = toHex(md.digest(jcsBytes)); + String b64 = Base64.getEncoder().encodeToString(jcsBytes); + computed.put(vectorId, sha); + + boolean shaOk = expectedSha == null || expectedSha.equals(sha); + boolean bytesOk = expectedB64 == null || expectedB64.equals(b64); + boolean ok = shaOk && bytesOk; + String mark = ok ? "OK " : "FAIL"; + System.out.printf(" %s %-34s sha256:%s%n", mark, vectorId, sha); + if (!ok) { + if (!shaOk) System.out.printf(" expected sha256:%s%n", expectedSha); + if (!bytesOk) System.out.println(" bytes mismatch"); + fail++; + } else { + pass++; + } + } + + System.out.println("\n--- pair invariants ---"); + int pairFail = 0; + for (Map.Entry e : expectations.entrySet()) { + String exp = e.getValue(); + if (exp.startsWith("same_hash_as:")) { + String other = exp.substring("same_hash_as:".length()); + boolean ok = computed.get(e.getKey()).equals(computed.get(other)); + System.out.printf(" %s %s == %s%n", ok ? "OK " : "FAIL", e.getKey(), other); + if (!ok) pairFail++; + } else if (exp.startsWith("different_hash_from:")) { + String other = exp.substring("different_hash_from:".length()); + boolean ok = !computed.get(e.getKey()).equals(computed.get(other)); + System.out.printf(" %s %s != %s%n", ok ? "OK " : "FAIL", e.getKey(), other); + if (!ok) pairFail++; + } + } + + System.out.printf("%n%d/%d vectors match (cyberphone/json-canonicalization)%n", + pass, pass + fail); + System.out.printf("%d pair-invariant failures%n", pairFail); + System.exit((fail == 0 && pairFail == 0) ? 0 : 1); + } +} diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md b/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md index 5e08bcac..704639a6 100644 --- a/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md +++ b/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md @@ -47,16 +47,48 @@ The array-order and Unicode pairs catch the divergences most commonly seen in practice: implementations that sort arrays or NFC-normalize strings will fail the corresponding pair invariant immediately. +## Reproduce locally + +Four runner scripts are included. Each reads `vectors-v0.json` and verifies +all 7 vectors and 4 pair invariants independently: + +| Runner | Language | Library | +| --- | --- | --- | +| `runner_python.py` | Python | `rfc8785@0.1.4` (Trail of Bits) | +| `runner_node.js` | JavaScript | `canonicalize@3.0.0` (Erdtman + Rundgren) | +| `runner_go.go` | Go | `gowebpki/jcs v1.0.1` | +| `JcsRunner.java` | Java | `cyberphone/json-canonicalization` (RFC 8785 reference impl) | + +```bash +# Python +pip install rfc8785==0.1.4 +python runner_python.py vectors-v0.json + +# Node.js +npm install canonicalize@3.0.0 +node runner_node.js vectors-v0.json + +# Go +go run runner_go.go vectors-v0.json + +# Java +javac JcsRunner.java && java JcsRunner vectors-v0.json +``` + ## Cross-implementation validation -| Implementation | Language | Library | Result | -| --- | --- | --- | --- | -| `rfc8785@0.1.4` | Python | Trail of Bits | 7/7 + 4/4 pair invariants ✓ | -| `canonicalize@3.0.0` | JavaScript | Erdtman + Rundgren (RFC 8785 author) | 7/7 + 4/4 ✓ | -| `gowebpki/jcs v1.0.1` | Go | @amavashev (AP2 maintainer) | 7/7 + 4/4 ✓ | -| `cyberphone/json-canonicalization` | Java | Rundgren (RFC 8785 reference impl) | 7/7 + 4/4 ✓ | -| `serde_jcs 0.2.0` | Rust | @seritalien / Vauban | 7/7 + 4/4 ✓ | -| `rfc8785` (fixed) | Python | Crest Systems | 7/7 + 4/4 ✓ | +All 6 independent implementations produce byte-identical results for all 7 vectors +and all 4 pair invariants. Five different authors, four author sets, independently +attested. + +| Implementation | Language | Library | Vectors | Pair invariants | +| --- | --- | --- | --- | --- | +| `rfc8785@0.1.4` | Python | Trail of Bits | 7/7 ✓ | 4/4 ✓ | +| `canonicalize@3.0.0` | JavaScript | Erdtman + Rundgren (RFC 8785 author) | 7/7 ✓ | 4/4 ✓ | +| `gowebpki/jcs v1.0.1` | Go | @amavashev (AP2 maintainer) | 7/7 ✓ | 4/4 ✓ | +| `cyberphone/json-canonicalization` | Java | Rundgren (RFC 8785 reference impl) | 7/7 ✓ | 4/4 ✓ | +| `serde_jcs 0.2.0` | Rust | @seritalien / Vauban | 7/7 ✓ | 4/4 ✓ | +| `rfc8785` (corrected) | Python | Crest Systems (@andysalvo) | 7/7 ✓ | 4/4 ✓ | Full validation history: [AP2 issue #265](https://github.com/google-agentic-commerce/AP2/issues/265) diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/runner_go.go b/code/sdk/schemas/ap2/conformance/open_mandate_hash/runner_go.go new file mode 100644 index 00000000..fb5105d7 --- /dev/null +++ b/code/sdk/schemas/ap2/conformance/open_mandate_hash/runner_go.go @@ -0,0 +1,130 @@ +// runner_go.go — gowebpki/jcs v1.0.1 runner for AP2 open_mandate_hash v0. +// +// Reads ap2-omh-v0.json, recomputes JCS + SHA-256 for each vector, and verifies +// recomputed hashes match expected_open_mandate_hash and pair expectations. +// +// Usage: +// +// go mod init jcs_runner && go get github.com/gowebpki/jcs@v1.0.1 +// go run runner_go.go ap2-omh-v0.json +package main + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/gowebpki/jcs" +) + +type vector struct { + VectorID string `json:"vector_id"` + MandateBody map[string]interface{} `json:"mandate_body"` + ExpectedJcsBytesB64 string `json:"expected_jcs_bytes_b64"` + ExpectedOpenMandateHash string `json:"expected_open_mandate_hash"` + Expectation string `json:"expectation"` +} + +type artefact struct { + Vectors []vector `json:"vectors"` +} + +func hashVector(body map[string]interface{}) (string, string, error) { + raw, err := json.Marshal(body) + if err != nil { + return "", "", err + } + jcsBytes, err := jcs.Transform(raw) + if err != nil { + return "", "", err + } + sum := sha256.Sum256(jcsBytes) + return base64.StdEncoding.EncodeToString(jcsBytes), + hex.EncodeToString(sum[:]), nil +} + +func main() { + if len(os.Args) < 2 { + fmt.Fprintln(os.Stderr, "usage: go run runner_go.go ap2-omh-v0.json") + os.Exit(2) + } + raw, err := ioutil.ReadFile(os.Args[1]) + if err != nil { + fmt.Fprintf(os.Stderr, "read: %v\n", err) + os.Exit(2) + } + var data artefact + if err := json.Unmarshal(raw, &data); err != nil { + fmt.Fprintf(os.Stderr, "parse: %v\n", err) + os.Exit(2) + } + + computed := map[string]string{} + pass, fail := 0, 0 + for _, v := range data.Vectors { + b64, sha, err := hashVector(v.MandateBody) + if err != nil { + fmt.Printf(" FAIL %s jcs error: %v\n", v.VectorID, err) + fail++ + continue + } + computed[v.VectorID] = sha + expectedSha := strings.TrimPrefix(v.ExpectedOpenMandateHash, "sha256:") + bytesOk := b64 == v.ExpectedJcsBytesB64 + shaOk := sha == expectedSha + ok := bytesOk && shaOk + mark := "OK " + if !ok { + mark = "FAIL" + } + fmt.Printf(" %s %-34s sha256:%s\n", mark, v.VectorID, sha) + if !ok { + if !bytesOk { + fmt.Println(" bytes mismatch") + } + if !shaOk { + fmt.Printf(" expected sha256:%s\n", expectedSha) + } + fail++ + } else { + pass++ + } + } + + fmt.Println("\n--- pair invariants ---") + pairFail := 0 + for _, v := range data.Vectors { + exp := v.Expectation + if strings.HasPrefix(exp, "same_hash_as:") { + other := strings.TrimPrefix(exp, "same_hash_as:") + ok := computed[v.VectorID] == computed[other] + mark := "OK " + if !ok { + mark = "FAIL" + pairFail++ + } + fmt.Printf(" %s %s == %s\n", mark, v.VectorID, other) + } else if strings.HasPrefix(exp, "different_hash_from:") { + other := strings.TrimPrefix(exp, "different_hash_from:") + ok := computed[v.VectorID] != computed[other] + mark := "OK " + if !ok { + mark = "FAIL" + pairFail++ + } + fmt.Printf(" %s %s != %s\n", mark, v.VectorID, other) + } + } + + fmt.Printf("\n%d/%d vectors match (gowebpki/jcs v1.0.1)\n", pass, pass+fail) + fmt.Printf("%d pair-invariant failures\n", pairFail) + if fail == 0 && pairFail == 0 { + os.Exit(0) + } + os.Exit(1) +} diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/runner_node.js b/code/sdk/schemas/ap2/conformance/open_mandate_hash/runner_node.js new file mode 100644 index 00000000..b41a0359 --- /dev/null +++ b/code/sdk/schemas/ap2/conformance/open_mandate_hash/runner_node.js @@ -0,0 +1,75 @@ +#!/usr/bin/env node +/** + * runner_node.js — canonicalize@3.0.0 (Erdtman) runner for AP2 open_mandate_hash v0. + * + * Reads ap2-omh-v0.json, recomputes JCS + SHA-256 for each vector, and verifies + * recomputed hashes match `expected_open_mandate_hash` and pair expectations. + * + * Usage: + * npm install canonicalize@3.0.0 + * node --input-type=module runner_node.js ap2-omh-v0.json + * + * Or save next to a package.json with `{ "type": "module" }`. + */ +import fs from 'fs'; +import crypto from 'crypto'; +import canonicalize from 'canonicalize'; + +function hashVector(body) { + const jcs = canonicalize(body); + const jcsBytes = Buffer.from(jcs, 'utf8'); + return { + bytes_b64: jcsBytes.toString('base64'), + sha256: crypto.createHash('sha256').update(jcsBytes).digest('hex'), + }; +} + +function main() { + if (process.argv.length < 3) { + console.error('usage: node runner_node.js ap2-omh-v0.json'); + process.exit(2); + } + const data = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); + const vectors = data.vectors; + const computed = {}; + let pass = 0, fail = 0; + + for (const v of vectors) { + const { bytes_b64, sha256 } = hashVector(v.mandate_body); + computed[v.vector_id] = sha256; + const expectedSha = v.expected_open_mandate_hash.replace(/^sha256:/, ''); + const bytesOk = bytes_b64 === v.expected_jcs_bytes_b64; + const shaOk = sha256 === expectedSha; + const ok = bytesOk && shaOk; + const mark = ok ? 'OK ' : 'FAIL'; + console.log(` ${mark} ${v.vector_id.padEnd(34)} sha256:${sha256}`); + if (!ok) { + if (!bytesOk) console.log(` bytes mismatch`); + if (!shaOk) console.log(` expected sha256:${expectedSha}`); + } + if (ok) pass++; else fail++; + } + + console.log('\n--- pair invariants ---'); + let pairFail = 0; + for (const v of vectors) { + const exp = v.expectation || ''; + if (exp.startsWith('same_hash_as:')) { + const other = exp.split(':')[1]; + const ok = computed[v.vector_id] === computed[other]; + console.log(` ${ok ? 'OK ' : 'FAIL'} ${v.vector_id} == ${other}`); + if (!ok) pairFail++; + } else if (exp.startsWith('different_hash_from:')) { + const other = exp.split(':')[1]; + const ok = computed[v.vector_id] !== computed[other]; + console.log(` ${ok ? 'OK ' : 'FAIL'} ${v.vector_id} != ${other}`); + if (!ok) pairFail++; + } + } + + console.log(`\n${pass}/${pass + fail} vectors match (canonicalize@3.0.0)`); + console.log(`${pairFail} pair-invariant failures`); + process.exit(fail === 0 && pairFail === 0 ? 0 : 1); +} + +main(); diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/runner_python.py b/code/sdk/schemas/ap2/conformance/open_mandate_hash/runner_python.py new file mode 100644 index 00000000..4cbe9e1c --- /dev/null +++ b/code/sdk/schemas/ap2/conformance/open_mandate_hash/runner_python.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python +""" +runner_python.py — reference JCS runner for AP2 open_mandate_hash v0 vectors. + +Reads ap2-omh-v0.json, recomputes JCS(RFC 8785)(mandate_body) + SHA-256 for +each vector, and verifies: + + 1. recomputed SHA-256 == expected_open_mandate_hash + 2. base64(JCS bytes) == expected_jcs_bytes_b64 + 3. pair expectations (same_hash_as / different_hash_from) + +Usage: + pip install rfc8785==0.1.4 + python runner_python.py ap2-omh-v0.json +""" +from __future__ import annotations + +import base64 +import hashlib +import json +import sys +from pathlib import Path + +import rfc8785 + + +def hash_vector(body: dict) -> tuple[str, str]: + """Return (jcs_bytes_b64, sha256_hex) for the canonical form of body.""" + jcs = rfc8785.dumps(body) + return base64.b64encode(jcs).decode("ascii"), hashlib.sha256(jcs).hexdigest() + + +def main() -> int: + if len(sys.argv) < 2: + print("usage: runner_python.py ap2-omh-v0.json", file=sys.stderr) + return 2 + + data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) + vectors = data["vectors"] + by_id = {v["vector_id"]: v for v in vectors} + + pass_, fail = 0, 0 + computed: dict[str, str] = {} + + for v in vectors: + bytes_b64, sha = hash_vector(v["mandate_body"]) + computed[v["vector_id"]] = sha + expected_sha = v["expected_open_mandate_hash"].removeprefix("sha256:") + bytes_ok = bytes_b64 == v["expected_jcs_bytes_b64"] + sha_ok = sha == expected_sha + ok = bytes_ok and sha_ok + mark = "OK " if ok else "FAIL" + print(f" {mark} {v['vector_id']:<34} sha256:{sha}") + if not ok: + if not bytes_ok: + print(f" bytes mismatch") + if not sha_ok: + print(f" expected sha256:{expected_sha}") + pass_ += int(ok) + fail += int(not ok) + + # Pair-invariant verification + print("\n--- pair invariants ---") + pair_fail = 0 + for v in vectors: + exp = v.get("expectation", "") + if exp.startswith("same_hash_as:"): + other = exp.split(":", 1)[1] + ok = computed[v["vector_id"]] == computed[other] + mark = "OK " if ok else "FAIL" + print(f" {mark} {v['vector_id']} == {other}") + if not ok: + pair_fail += 1 + elif exp.startswith("different_hash_from:"): + other = exp.split(":", 1)[1] + ok = computed[v["vector_id"]] != computed[other] + mark = "OK " if ok else "FAIL" + print(f" {mark} {v['vector_id']} != {other}") + if not ok: + pair_fail += 1 + + print(f"\n{pass_}/{pass_ + fail} vectors match (rfc8785@0.1.4)") + print(f"{pair_fail} pair-invariant failures") + return 0 if (fail == 0 and pair_fail == 0) else 1 + + +if __name__ == "__main__": + sys.exit(main()) From 14b97a876635253ed35fd35108485333718dfa05 Mon Sep 17 00:00:00 2001 From: iLoveChicken Date: Sat, 6 Jun 2026 05:45:20 +0100 Subject: [PATCH 09/14] fix(spellcheck): add Anders, andysalvo, javac, webpki to custom words --- .cspell/custom-words.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index be0e2eb2..3d8b0eaa 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -185,10 +185,14 @@ XVCJ AlgoVoi algovoi amavashev +Anders +andysalvo canonicalizer cyberphone Erdtman gowebpki +javac +webpki Iaap JCS jcs From c994746995431b7ffb42a88516a5759b0525fa8d Mon Sep 17 00:00:00 2001 From: iLoveChicken Date: Sat, 6 Jun 2026 05:52:39 +0100 Subject: [PATCH 10/14] fix(lint): rewrite JcsRunner.java to pass Checkstyle + Google Java Format --- .../open_mandate_hash/JcsRunner.java | 429 +++++++++++------- .../open_mandate_hash/package-info.java | 1 + 2 files changed, 264 insertions(+), 166 deletions(-) create mode 100644 code/sdk/schemas/ap2/conformance/open_mandate_hash/package-info.java diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/JcsRunner.java b/code/sdk/schemas/ap2/conformance/open_mandate_hash/JcsRunner.java index 0fb4d93d..2701a78e 100644 --- a/code/sdk/schemas/ap2/conformance/open_mandate_hash/JcsRunner.java +++ b/code/sdk/schemas/ap2/conformance/open_mandate_hash/JcsRunner.java @@ -1,187 +1,284 @@ -// JcsRunner.java — cyberphone/json-canonicalization runner for AP2 open_mandate_hash v0. -// -// Uses Anders Rundgren's Java reference impl (the one cited in RFC 8785). -// The actual canonicalization is the unmodified JsonCanonicalizer class. -// This file adds only: -// - a minimal JSON extractor to locate vector framing fields (mandate_body, -// expected_open_mandate_hash, expectation, vector_id) within ap2-omh-v0.json -// - SHA-256 + base64 wrappers around JsonCanonicalizer's byte output -// - pair-invariant verification -// -// The minimal extractor uses brace-counting on the artefact file. Because the -// artefact shape is fixed (we publish it), this is safe; it is NOT a general -// JSON parser. Canonicalization correctness lives entirely in JsonCanonicalizer. -// -// Build: -// git clone --depth 1 https://github.com/cyberphone/json-canonicalization.git -// javac -d classes \ -// -sourcepath json-canonicalization/java/canonicalizer/src \ -// json-canonicalization/java/canonicalizer/src/org/webpki/jcs/JsonCanonicalizer.java \ -// JcsRunner.java -// java -cp classes JcsRunner ap2-omh-v0.json +/** + * JCS runner for AP2 open_mandate_hash v0 conformance vectors. + * + *

Uses Anders Rundgren's Java reference implementation + * (cyberphone/json-canonicalization), cited in RFC 8785. + * + *

Build: + * + *

+ *   git clone --depth 1 \
+ *     https://github.com/cyberphone/json-canonicalization.git
+ *   javac -d classes \
+ *     -sourcepath \
+ *     json-canonicalization/java/canonicalizer/src \
+ *     json-canonicalization/java/canonicalizer/src/org/webpki/jcs/\
+ *     JsonCanonicalizer.java \
+ *     JcsRunner.java
+ *   java -cp classes JcsRunner vectors-v0.json
+ * 
+ */ +import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.nio.file.*; +import java.nio.file.Files; +import java.nio.file.Paths; import java.security.MessageDigest; -import java.util.*; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import org.webpki.jcs.JsonCanonicalizer; -public class JcsRunner { +/** JCS runner for AP2 open_mandate_hash v0 conformance vectors. */ +public final class JcsRunner { - static String toHex(byte[] b) { - StringBuilder sb = new StringBuilder(); - for (byte x : b) sb.append(String.format("%02x", x & 0xff)); - return sb.toString(); + private static final int BYTE_MASK = 0xff; + + private JcsRunner() {} + + private static String toHex(final byte[] b) { + final StringBuilder sb = new StringBuilder(); + for (final byte x : b) { + sb.append(String.format("%02x", x & BYTE_MASK)); } + return sb.toString(); + } - // ── minimal artefact-specific JSON extractor ──────────────────────────── - // Finds occurrences of `"key"` at top of each vector object and returns - // the next value: a JSON object/array (returned verbatim by brace count), - // a JSON string (returned with quotes), or a bare literal. - // ONLY used to slice ap2-omh-v0.json into per-vector chunks; canonicalization - // is delegated to JsonCanonicalizer. - static List findVectorRanges(String src) { - // Locate the top-level "vectors": [ … ] array and return the byte - // ranges of each object inside it. - int vIdx = src.indexOf("\"vectors\""); - if (vIdx < 0) throw new IllegalArgumentException("no 'vectors' key"); - int br = src.indexOf('[', vIdx); - List ranges = new ArrayList<>(); - int depth = 0; - boolean inStr = false, esc = false; - int objStart = -1; - for (int i = br + 1; i < src.length(); i++) { - char c = src.charAt(i); - if (esc) { esc = false; continue; } - if (c == '\\') { esc = true; continue; } - if (c == '"') { inStr = !inStr; continue; } - if (inStr) continue; - if (c == '{') { - if (depth == 0) objStart = i; - depth++; - } else if (c == '}') { - depth--; - if (depth == 0) { - ranges.add(new int[]{objStart, i + 1}); - } - } else if (c == ']' && depth == 0) { - break; - } + private static List findVectorRanges(final String src) { + final int vIdx = src.indexOf("\"vectors\""); + if (vIdx < 0) { + throw new IllegalArgumentException("no 'vectors' key"); + } + final int br = src.indexOf('[', vIdx); + final List ranges = new ArrayList<>(); + int depth = 0; + boolean inStr = false; + boolean esc = false; + int objStart = -1; + for (int i = br + 1; i < src.length(); i++) { + final char c = src.charAt(i); + if (esc) { + esc = false; + continue; + } + if (c == '\\') { + esc = true; + continue; + } + if (c == '"') { + inStr = !inStr; + continue; + } + if (inStr) { + continue; + } + if (c == '{') { + if (depth == 0) { + objStart = i; } - return ranges; + depth++; + } else if (c == '}') { + depth--; + if (depth == 0) { + ranges.add(new int[] {objStart, i + 1}); + } + } else if (c == ']' && depth == 0) { + break; + } } + return ranges; + } - static String extractField(String vector, String key) { - // Find the value following "key": within a single vector JSON object. - String needle = "\"" + key + "\""; - int idx = vector.indexOf(needle); - if (idx < 0) return null; - int colon = vector.indexOf(':', idx + needle.length()); - if (colon < 0) return null; - int i = colon + 1; - while (i < vector.length() && Character.isWhitespace(vector.charAt(i))) i++; - if (i >= vector.length()) return null; - char open = vector.charAt(i); - - if (open == '{' || open == '[') { - char close = open == '{' ? '}' : ']'; - int depth = 0; - boolean inStr = false, esc = false; - int start = i; - for (; i < vector.length(); i++) { - char c = vector.charAt(i); - if (esc) { esc = false; continue; } - if (c == '\\') { esc = true; continue; } - if (c == '"') { inStr = !inStr; continue; } - if (inStr) continue; - if (c == open) depth++; - else if (c == close) { - depth--; - if (depth == 0) return vector.substring(start, i + 1); - } - } - } else if (open == '"') { - int start = i + 1; - boolean esc = false; - for (i = start; i < vector.length(); i++) { - char c = vector.charAt(i); - if (esc) { esc = false; continue; } - if (c == '\\') { esc = true; continue; } - if (c == '"') return vector.substring(start, i); - } + private static String extractField(final String vector, final String key) { + final String needle = "\"" + key + "\""; + final int idx = vector.indexOf(needle); + if (idx < 0) { + return null; + } + final int colon = vector.indexOf(':', idx + needle.length()); + if (colon < 0) { + return null; + } + int i = colon + 1; + while (i < vector.length() + && Character.isWhitespace(vector.charAt(i))) { + i++; + } + if (i >= vector.length()) { + return null; + } + final char open = vector.charAt(i); + if (open == '{' || open == '[') { + return extractObject(vector, i, open); + } else if (open == '"') { + return extractString(vector, i + 1); + } + return null; + } + + private static String extractObject( + final String vector, final int start, final char open) { + final char close = open == '{' ? '}' : ']'; + int depth = 0; + boolean inStr = false; + boolean esc = false; + for (int i = start; i < vector.length(); i++) { + final char c = vector.charAt(i); + if (esc) { + esc = false; + continue; + } + if (c == '\\') { + esc = true; + continue; + } + if (c == '"') { + inStr = !inStr; + continue; + } + if (inStr) { + continue; + } + if (c == open) { + depth++; + } else if (c == close) { + depth--; + if (depth == 0) { + return vector.substring(start, i + 1); } - return null; + } } + return null; + } - public static void main(String[] args) throws Exception { - if (args.length < 1) { - System.err.println("usage: JcsRunner ap2-omh-v0.json"); - System.exit(2); + private static String extractString( + final String vector, final int start) { + boolean esc = false; + for (int i = start; i < vector.length(); i++) { + final char c = vector.charAt(i); + if (esc) { + esc = false; + continue; + } + if (c == '\\') { + esc = true; + continue; + } + if (c == '"') { + return vector.substring(start, i); + } + } + return null; + } + + /** + * Entry point. + * + * @param args command-line arguments; args[0] is the vector file path + * @throws IOException on file read error + * @throws NoSuchAlgorithmException if SHA-256 is unavailable + */ + public static void main(final String[] args) + throws IOException, NoSuchAlgorithmException { + if (args.length < 1) { + System.err.println("usage: JcsRunner vectors-v0.json"); + System.exit(2); + } + final String src = new String( + Files.readAllBytes(Paths.get(args[0])), StandardCharsets.UTF_8); + final List ranges = findVectorRanges(src); + final MessageDigest md = MessageDigest.getInstance("SHA-256"); + final Map computed = new LinkedHashMap<>(); + final Map expectations = new LinkedHashMap<>(); + int pass = 0; + int fail = 0; + + for (final int[] r : ranges) { + final String vec = src.substring(r[0], r[1]); + final String vectorId = extractField(vec, "vector_id"); + final String body = extractField(vec, "mandate_body"); + String expectedSha = extractField(vec, "expected_open_mandate_hash"); + final String expectedB64 = + extractField(vec, "expected_jcs_bytes_b64"); + final String expectation = extractField(vec, "expectation"); + + if (expectedSha != null && expectedSha.startsWith("sha256:")) { + expectedSha = expectedSha.substring("sha256:".length()); + } + expectations.put(vectorId, expectation == null ? "" : expectation); + + final JsonCanonicalizer jc = new JsonCanonicalizer(body); + final byte[] jcsBytes = jc.getEncodedUTF8(); + md.reset(); + final String sha = toHex(md.digest(jcsBytes)); + final String b64 = Base64.getEncoder().encodeToString(jcsBytes); + computed.put(vectorId, sha); + + final boolean shaOk = + expectedSha == null || expectedSha.equals(sha); + final boolean bytesOk = + expectedB64 == null || expectedB64.equals(b64); + final boolean ok = shaOk && bytesOk; + final String mark = ok ? "OK " : "FAIL"; + System.out.printf( + " %s %-34s sha256:%s%n", mark, vectorId, sha); + if (!ok) { + if (!shaOk) { + System.out.printf( + " expected sha256:%s%n", expectedSha); } - String src = new String(Files.readAllBytes(Paths.get(args[0])), - StandardCharsets.UTF_8); - List ranges = findVectorRanges(src); - - MessageDigest md = MessageDigest.getInstance("SHA-256"); - Map computed = new LinkedHashMap<>(); - Map expectations = new LinkedHashMap<>(); - int pass = 0, fail = 0; - - for (int[] r : ranges) { - String vec = src.substring(r[0], r[1]); - String vectorId = extractField(vec, "vector_id"); - String body = extractField(vec, "mandate_body"); - String expectedSha = extractField(vec, "expected_open_mandate_hash"); - String expectedB64 = extractField(vec, "expected_jcs_bytes_b64"); - String expectation = extractField(vec, "expectation"); - - if (expectedSha != null && expectedSha.startsWith("sha256:")) { - expectedSha = expectedSha.substring("sha256:".length()); - } - expectations.put(vectorId, expectation == null ? "" : expectation); - - JsonCanonicalizer jc = new JsonCanonicalizer(body); - byte[] jcsBytes = jc.getEncodedUTF8(); - md.reset(); - String sha = toHex(md.digest(jcsBytes)); - String b64 = Base64.getEncoder().encodeToString(jcsBytes); - computed.put(vectorId, sha); - - boolean shaOk = expectedSha == null || expectedSha.equals(sha); - boolean bytesOk = expectedB64 == null || expectedB64.equals(b64); - boolean ok = shaOk && bytesOk; - String mark = ok ? "OK " : "FAIL"; - System.out.printf(" %s %-34s sha256:%s%n", mark, vectorId, sha); - if (!ok) { - if (!shaOk) System.out.printf(" expected sha256:%s%n", expectedSha); - if (!bytesOk) System.out.println(" bytes mismatch"); - fail++; - } else { - pass++; - } + if (!bytesOk) { + System.out.println(" bytes mismatch"); } + fail++; + } else { + pass++; + } + } - System.out.println("\n--- pair invariants ---"); - int pairFail = 0; - for (Map.Entry e : expectations.entrySet()) { - String exp = e.getValue(); - if (exp.startsWith("same_hash_as:")) { - String other = exp.substring("same_hash_as:".length()); - boolean ok = computed.get(e.getKey()).equals(computed.get(other)); - System.out.printf(" %s %s == %s%n", ok ? "OK " : "FAIL", e.getKey(), other); - if (!ok) pairFail++; - } else if (exp.startsWith("different_hash_from:")) { - String other = exp.substring("different_hash_from:".length()); - boolean ok = !computed.get(e.getKey()).equals(computed.get(other)); - System.out.printf(" %s %s != %s%n", ok ? "OK " : "FAIL", e.getKey(), other); - if (!ok) pairFail++; - } + System.out.println("\n--- pair invariants ---"); + int pairFail = 0; + for (final Map.Entry entry + : expectations.entrySet()) { + final String exp = entry.getValue(); + if (exp.startsWith("same_hash_as:")) { + final String other = + exp.substring("same_hash_as:".length()); + final boolean ok = + computed.get(entry.getKey()).equals(computed.get(other)); + System.out.printf( + " %s %s == %s%n", + ok ? "OK " : "FAIL", + entry.getKey(), + other); + if (!ok) { + pairFail++; } - - System.out.printf("%n%d/%d vectors match (cyberphone/json-canonicalization)%n", - pass, pass + fail); - System.out.printf("%d pair-invariant failures%n", pairFail); - System.exit((fail == 0 && pairFail == 0) ? 0 : 1); + } else if (exp.startsWith("different_hash_from:")) { + final String other = + exp.substring("different_hash_from:".length()); + final boolean ok = + !computed.get(entry.getKey()).equals(computed.get(other)); + System.out.printf( + " %s %s != %s%n", + ok ? "OK " : "FAIL", + entry.getKey(), + other); + if (!ok) { + pairFail++; + } + } } + + System.out.printf( + "%n%d/%d vectors match (cyberphone/json-canonicalization)%n", + pass, + pass + fail); + System.out.printf("%d pair-invariant failures%n", pairFail); + System.exit((fail == 0 && pairFail == 0) ? 0 : 1); + } } diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/package-info.java b/code/sdk/schemas/ap2/conformance/open_mandate_hash/package-info.java new file mode 100644 index 00000000..25aeeb3e --- /dev/null +++ b/code/sdk/schemas/ap2/conformance/open_mandate_hash/package-info.java @@ -0,0 +1 @@ +/** Conformance vectors for AP2 open_mandate_hash derivation. */ From effc481d96217caac371de76beb6f65a99998409 Mon Sep 17 00:00:00 2001 From: iLoveChicken Date: Sat, 6 Jun 2026 08:44:19 +0100 Subject: [PATCH 11/14] fix(lint): block comment (not Javadoc) for file header, Javadoc on constant, whitespace in constructor --- .../ap2/conformance/open_mandate_hash/JcsRunner.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/JcsRunner.java b/code/sdk/schemas/ap2/conformance/open_mandate_hash/JcsRunner.java index 2701a78e..614ecac0 100644 --- a/code/sdk/schemas/ap2/conformance/open_mandate_hash/JcsRunner.java +++ b/code/sdk/schemas/ap2/conformance/open_mandate_hash/JcsRunner.java @@ -1,12 +1,10 @@ -/** +/* * JCS runner for AP2 open_mandate_hash v0 conformance vectors. * - *

Uses Anders Rundgren's Java reference implementation + * Uses Anders Rundgren's Java reference implementation * (cyberphone/json-canonicalization), cited in RFC 8785. * - *

Build: - * - *

+ * Build:
  *   git clone --depth 1 \
  *     https://github.com/cyberphone/json-canonicalization.git
  *   javac -d classes \
@@ -16,7 +14,6 @@
  *     JsonCanonicalizer.java \
  *     JcsRunner.java
  *   java -cp classes JcsRunner vectors-v0.json
- * 
*/ import java.io.IOException; @@ -36,9 +33,10 @@ /** JCS runner for AP2 open_mandate_hash v0 conformance vectors. */ public final class JcsRunner { + /** Bit mask for extracting unsigned byte value in hex conversion. */ private static final int BYTE_MASK = 0xff; - private JcsRunner() {} + private JcsRunner() { } private static String toHex(final byte[] b) { final StringBuilder sb = new StringBuilder(); From ca887f1129b1eb13bd5fcc645c89809672095173 Mon Sep 17 00:00:00 2001 From: iLoveChicken Date: Sat, 6 Jun 2026 08:56:09 +0100 Subject: [PATCH 12/14] style(conformance): format JcsRunner with google-java-format Apply google-java-format 1.35.0 (the version super-linter runs) to satisfy GOOGLE_JAVA_FORMAT. Whitespace and line-wrapping only, no logic changes. --- .../open_mandate_hash/JcsRunner.java | 64 ++++++------------- 1 file changed, 19 insertions(+), 45 deletions(-) diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/JcsRunner.java b/code/sdk/schemas/ap2/conformance/open_mandate_hash/JcsRunner.java index 614ecac0..d498ecbf 100644 --- a/code/sdk/schemas/ap2/conformance/open_mandate_hash/JcsRunner.java +++ b/code/sdk/schemas/ap2/conformance/open_mandate_hash/JcsRunner.java @@ -27,7 +27,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; - import org.webpki.jcs.JsonCanonicalizer; /** JCS runner for AP2 open_mandate_hash v0 conformance vectors. */ @@ -36,7 +35,7 @@ public final class JcsRunner { /** Bit mask for extracting unsigned byte value in hex conversion. */ private static final int BYTE_MASK = 0xff; - private JcsRunner() { } + private JcsRunner() {} private static String toHex(final byte[] b) { final StringBuilder sb = new StringBuilder(); @@ -102,8 +101,7 @@ private static String extractField(final String vector, final String key) { return null; } int i = colon + 1; - while (i < vector.length() - && Character.isWhitespace(vector.charAt(i))) { + while (i < vector.length() && Character.isWhitespace(vector.charAt(i))) { i++; } if (i >= vector.length()) { @@ -118,8 +116,7 @@ private static String extractField(final String vector, final String key) { return null; } - private static String extractObject( - final String vector, final int start, final char open) { + private static String extractObject(final String vector, final int start, final char open) { final char close = open == '{' ? '}' : ']'; int depth = 0; boolean inStr = false; @@ -153,8 +150,7 @@ private static String extractObject( return null; } - private static String extractString( - final String vector, final int start) { + private static String extractString(final String vector, final int start) { boolean esc = false; for (int i = start; i < vector.length(); i++) { final char c = vector.charAt(i); @@ -180,14 +176,12 @@ private static String extractString( * @throws IOException on file read error * @throws NoSuchAlgorithmException if SHA-256 is unavailable */ - public static void main(final String[] args) - throws IOException, NoSuchAlgorithmException { + public static void main(final String[] args) throws IOException, NoSuchAlgorithmException { if (args.length < 1) { System.err.println("usage: JcsRunner vectors-v0.json"); System.exit(2); } - final String src = new String( - Files.readAllBytes(Paths.get(args[0])), StandardCharsets.UTF_8); + final String src = new String(Files.readAllBytes(Paths.get(args[0])), StandardCharsets.UTF_8); final List ranges = findVectorRanges(src); final MessageDigest md = MessageDigest.getInstance("SHA-256"); final Map computed = new LinkedHashMap<>(); @@ -200,8 +194,7 @@ public static void main(final String[] args) final String vectorId = extractField(vec, "vector_id"); final String body = extractField(vec, "mandate_body"); String expectedSha = extractField(vec, "expected_open_mandate_hash"); - final String expectedB64 = - extractField(vec, "expected_jcs_bytes_b64"); + final String expectedB64 = extractField(vec, "expected_jcs_bytes_b64"); final String expectation = extractField(vec, "expectation"); if (expectedSha != null && expectedSha.startsWith("sha256:")) { @@ -216,18 +209,14 @@ public static void main(final String[] args) final String b64 = Base64.getEncoder().encodeToString(jcsBytes); computed.put(vectorId, sha); - final boolean shaOk = - expectedSha == null || expectedSha.equals(sha); - final boolean bytesOk = - expectedB64 == null || expectedB64.equals(b64); + final boolean shaOk = expectedSha == null || expectedSha.equals(sha); + final boolean bytesOk = expectedB64 == null || expectedB64.equals(b64); final boolean ok = shaOk && bytesOk; final String mark = ok ? "OK " : "FAIL"; - System.out.printf( - " %s %-34s sha256:%s%n", mark, vectorId, sha); + System.out.printf(" %s %-34s sha256:%s%n", mark, vectorId, sha); if (!ok) { if (!shaOk) { - System.out.printf( - " expected sha256:%s%n", expectedSha); + System.out.printf(" expected sha256:%s%n", expectedSha); } if (!bytesOk) { System.out.println(" bytes mismatch"); @@ -240,32 +229,19 @@ public static void main(final String[] args) System.out.println("\n--- pair invariants ---"); int pairFail = 0; - for (final Map.Entry entry - : expectations.entrySet()) { + for (final Map.Entry entry : expectations.entrySet()) { final String exp = entry.getValue(); if (exp.startsWith("same_hash_as:")) { - final String other = - exp.substring("same_hash_as:".length()); - final boolean ok = - computed.get(entry.getKey()).equals(computed.get(other)); - System.out.printf( - " %s %s == %s%n", - ok ? "OK " : "FAIL", - entry.getKey(), - other); + final String other = exp.substring("same_hash_as:".length()); + final boolean ok = computed.get(entry.getKey()).equals(computed.get(other)); + System.out.printf(" %s %s == %s%n", ok ? "OK " : "FAIL", entry.getKey(), other); if (!ok) { pairFail++; } } else if (exp.startsWith("different_hash_from:")) { - final String other = - exp.substring("different_hash_from:".length()); - final boolean ok = - !computed.get(entry.getKey()).equals(computed.get(other)); - System.out.printf( - " %s %s != %s%n", - ok ? "OK " : "FAIL", - entry.getKey(), - other); + final String other = exp.substring("different_hash_from:".length()); + final boolean ok = !computed.get(entry.getKey()).equals(computed.get(other)); + System.out.printf(" %s %s != %s%n", ok ? "OK " : "FAIL", entry.getKey(), other); if (!ok) { pairFail++; } @@ -273,9 +249,7 @@ public static void main(final String[] args) } System.out.printf( - "%n%d/%d vectors match (cyberphone/json-canonicalization)%n", - pass, - pass + fail); + "%n%d/%d vectors match (cyberphone/json-canonicalization)%n", pass, pass + fail); System.out.printf("%d pair-invariant failures%n", pairFail); System.exit((fail == 0 && pairFail == 0) ? 0 : 1); } From a0b8544116c0f4951f7ffd320f6d1adf8376c6ed Mon Sep 17 00:00:00 2001 From: iLoveChicken Date: Sat, 6 Jun 2026 09:21:34 +0100 Subject: [PATCH 13/14] chore(conformance): remove Java runner The repo runs google-java-format and Sun-style Checkstyle together, whose rules contradict (gjf: {} and 100-col; checkstyle: { } and 80-col), so no Java formatting passes both validators. Drop JcsRunner and package-info; the Python, Node, and Go runners and the vectors remain. The Java runner can return once the repo adds a Checkstyle config. --- .../open_mandate_hash/JcsRunner.java | 256 ------------------ .../conformance/open_mandate_hash/README.md | 5 +- .../open_mandate_hash/package-info.java | 1 - 3 files changed, 1 insertion(+), 261 deletions(-) delete mode 100644 code/sdk/schemas/ap2/conformance/open_mandate_hash/JcsRunner.java delete mode 100644 code/sdk/schemas/ap2/conformance/open_mandate_hash/package-info.java diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/JcsRunner.java b/code/sdk/schemas/ap2/conformance/open_mandate_hash/JcsRunner.java deleted file mode 100644 index d498ecbf..00000000 --- a/code/sdk/schemas/ap2/conformance/open_mandate_hash/JcsRunner.java +++ /dev/null @@ -1,256 +0,0 @@ -/* - * JCS runner for AP2 open_mandate_hash v0 conformance vectors. - * - * Uses Anders Rundgren's Java reference implementation - * (cyberphone/json-canonicalization), cited in RFC 8785. - * - * Build: - * git clone --depth 1 \ - * https://github.com/cyberphone/json-canonicalization.git - * javac -d classes \ - * -sourcepath \ - * json-canonicalization/java/canonicalizer/src \ - * json-canonicalization/java/canonicalizer/src/org/webpki/jcs/\ - * JsonCanonicalizer.java \ - * JcsRunner.java - * java -cp classes JcsRunner vectors-v0.json - */ - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Base64; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.webpki.jcs.JsonCanonicalizer; - -/** JCS runner for AP2 open_mandate_hash v0 conformance vectors. */ -public final class JcsRunner { - - /** Bit mask for extracting unsigned byte value in hex conversion. */ - private static final int BYTE_MASK = 0xff; - - private JcsRunner() {} - - private static String toHex(final byte[] b) { - final StringBuilder sb = new StringBuilder(); - for (final byte x : b) { - sb.append(String.format("%02x", x & BYTE_MASK)); - } - return sb.toString(); - } - - private static List findVectorRanges(final String src) { - final int vIdx = src.indexOf("\"vectors\""); - if (vIdx < 0) { - throw new IllegalArgumentException("no 'vectors' key"); - } - final int br = src.indexOf('[', vIdx); - final List ranges = new ArrayList<>(); - int depth = 0; - boolean inStr = false; - boolean esc = false; - int objStart = -1; - for (int i = br + 1; i < src.length(); i++) { - final char c = src.charAt(i); - if (esc) { - esc = false; - continue; - } - if (c == '\\') { - esc = true; - continue; - } - if (c == '"') { - inStr = !inStr; - continue; - } - if (inStr) { - continue; - } - if (c == '{') { - if (depth == 0) { - objStart = i; - } - depth++; - } else if (c == '}') { - depth--; - if (depth == 0) { - ranges.add(new int[] {objStart, i + 1}); - } - } else if (c == ']' && depth == 0) { - break; - } - } - return ranges; - } - - private static String extractField(final String vector, final String key) { - final String needle = "\"" + key + "\""; - final int idx = vector.indexOf(needle); - if (idx < 0) { - return null; - } - final int colon = vector.indexOf(':', idx + needle.length()); - if (colon < 0) { - return null; - } - int i = colon + 1; - while (i < vector.length() && Character.isWhitespace(vector.charAt(i))) { - i++; - } - if (i >= vector.length()) { - return null; - } - final char open = vector.charAt(i); - if (open == '{' || open == '[') { - return extractObject(vector, i, open); - } else if (open == '"') { - return extractString(vector, i + 1); - } - return null; - } - - private static String extractObject(final String vector, final int start, final char open) { - final char close = open == '{' ? '}' : ']'; - int depth = 0; - boolean inStr = false; - boolean esc = false; - for (int i = start; i < vector.length(); i++) { - final char c = vector.charAt(i); - if (esc) { - esc = false; - continue; - } - if (c == '\\') { - esc = true; - continue; - } - if (c == '"') { - inStr = !inStr; - continue; - } - if (inStr) { - continue; - } - if (c == open) { - depth++; - } else if (c == close) { - depth--; - if (depth == 0) { - return vector.substring(start, i + 1); - } - } - } - return null; - } - - private static String extractString(final String vector, final int start) { - boolean esc = false; - for (int i = start; i < vector.length(); i++) { - final char c = vector.charAt(i); - if (esc) { - esc = false; - continue; - } - if (c == '\\') { - esc = true; - continue; - } - if (c == '"') { - return vector.substring(start, i); - } - } - return null; - } - - /** - * Entry point. - * - * @param args command-line arguments; args[0] is the vector file path - * @throws IOException on file read error - * @throws NoSuchAlgorithmException if SHA-256 is unavailable - */ - public static void main(final String[] args) throws IOException, NoSuchAlgorithmException { - if (args.length < 1) { - System.err.println("usage: JcsRunner vectors-v0.json"); - System.exit(2); - } - final String src = new String(Files.readAllBytes(Paths.get(args[0])), StandardCharsets.UTF_8); - final List ranges = findVectorRanges(src); - final MessageDigest md = MessageDigest.getInstance("SHA-256"); - final Map computed = new LinkedHashMap<>(); - final Map expectations = new LinkedHashMap<>(); - int pass = 0; - int fail = 0; - - for (final int[] r : ranges) { - final String vec = src.substring(r[0], r[1]); - final String vectorId = extractField(vec, "vector_id"); - final String body = extractField(vec, "mandate_body"); - String expectedSha = extractField(vec, "expected_open_mandate_hash"); - final String expectedB64 = extractField(vec, "expected_jcs_bytes_b64"); - final String expectation = extractField(vec, "expectation"); - - if (expectedSha != null && expectedSha.startsWith("sha256:")) { - expectedSha = expectedSha.substring("sha256:".length()); - } - expectations.put(vectorId, expectation == null ? "" : expectation); - - final JsonCanonicalizer jc = new JsonCanonicalizer(body); - final byte[] jcsBytes = jc.getEncodedUTF8(); - md.reset(); - final String sha = toHex(md.digest(jcsBytes)); - final String b64 = Base64.getEncoder().encodeToString(jcsBytes); - computed.put(vectorId, sha); - - final boolean shaOk = expectedSha == null || expectedSha.equals(sha); - final boolean bytesOk = expectedB64 == null || expectedB64.equals(b64); - final boolean ok = shaOk && bytesOk; - final String mark = ok ? "OK " : "FAIL"; - System.out.printf(" %s %-34s sha256:%s%n", mark, vectorId, sha); - if (!ok) { - if (!shaOk) { - System.out.printf(" expected sha256:%s%n", expectedSha); - } - if (!bytesOk) { - System.out.println(" bytes mismatch"); - } - fail++; - } else { - pass++; - } - } - - System.out.println("\n--- pair invariants ---"); - int pairFail = 0; - for (final Map.Entry entry : expectations.entrySet()) { - final String exp = entry.getValue(); - if (exp.startsWith("same_hash_as:")) { - final String other = exp.substring("same_hash_as:".length()); - final boolean ok = computed.get(entry.getKey()).equals(computed.get(other)); - System.out.printf(" %s %s == %s%n", ok ? "OK " : "FAIL", entry.getKey(), other); - if (!ok) { - pairFail++; - } - } else if (exp.startsWith("different_hash_from:")) { - final String other = exp.substring("different_hash_from:".length()); - final boolean ok = !computed.get(entry.getKey()).equals(computed.get(other)); - System.out.printf(" %s %s != %s%n", ok ? "OK " : "FAIL", entry.getKey(), other); - if (!ok) { - pairFail++; - } - } - } - - System.out.printf( - "%n%d/%d vectors match (cyberphone/json-canonicalization)%n", pass, pass + fail); - System.out.printf("%d pair-invariant failures%n", pairFail); - System.exit((fail == 0 && pairFail == 0) ? 0 : 1); - } -} diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md b/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md index 704639a6..92f63595 100644 --- a/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md +++ b/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md @@ -49,7 +49,7 @@ fail the corresponding pair invariant immediately. ## Reproduce locally -Four runner scripts are included. Each reads `vectors-v0.json` and verifies +Three runner scripts are included. Each reads `vectors-v0.json` and verifies all 7 vectors and 4 pair invariants independently: | Runner | Language | Library | @@ -57,7 +57,6 @@ all 7 vectors and 4 pair invariants independently: | `runner_python.py` | Python | `rfc8785@0.1.4` (Trail of Bits) | | `runner_node.js` | JavaScript | `canonicalize@3.0.0` (Erdtman + Rundgren) | | `runner_go.go` | Go | `gowebpki/jcs v1.0.1` | -| `JcsRunner.java` | Java | `cyberphone/json-canonicalization` (RFC 8785 reference impl) | ```bash # Python @@ -71,8 +70,6 @@ node runner_node.js vectors-v0.json # Go go run runner_go.go vectors-v0.json -# Java -javac JcsRunner.java && java JcsRunner vectors-v0.json ``` ## Cross-implementation validation diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/package-info.java b/code/sdk/schemas/ap2/conformance/open_mandate_hash/package-info.java deleted file mode 100644 index 25aeeb3e..00000000 --- a/code/sdk/schemas/ap2/conformance/open_mandate_hash/package-info.java +++ /dev/null @@ -1 +0,0 @@ -/** Conformance vectors for AP2 open_mandate_hash derivation. */ From 0990c9cd522038df1225abde33a7b6fe988449ce Mon Sep 17 00:00:00 2001 From: iLoveChicken Date: Sat, 6 Jun 2026 09:41:23 +0100 Subject: [PATCH 14/14] docs(conformance): clarify expected_jcs_bytes_b64 is standard base64 Addresses review note: the field is standard base64 (with padding and +// characters), not base64url. --- code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md b/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md index 92f63595..d52075eb 100644 --- a/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md +++ b/code/sdk/schemas/ap2/conformance/open_mandate_hash/README.md @@ -27,7 +27,7 @@ Each vector is structured as: { "vector_id": "ap2-omh-v0-", "mandate_body": { ... }, - "expected_jcs_bytes_b64": "", + "expected_jcs_bytes_b64": "", "expected_open_mandate_hash": "sha256:", "expectation": "reference | same_hash_as: | different_hash_from:" }