Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions fixtures/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# x402 Conformance Fixtures

Cross-language conformance vectors for x402 protocol extensions.

## Structure

```
fixtures/
<suite-name>/
<version>/
README.md # What the suite tests
manifest.json # Suite metadata + vector listing
vectors/
NNNN-name.json # Individual test vectors
```

## Vector format

Each vector is a JSON file:

```json
{
"name": "descriptive-name",
"spec": "suite-specific-spec-identifier",
"expected_result": "PASS | FAIL",
"notes": "What this vector tests and why"
}
```

Additional fields are suite-specific (preimage objects, expected digests, payment hashes, etc.).

## Current suites

| Suite | Version | Vectors | Tests |
|-------|---------|---------|-------|
| [action-ref-verify](action-ref-verify/v0/) | v0 | 9 | Work-receipt digest binding via SHA-256(JCS(preimage)) |

## Adding vectors

Conformance vectors from independent implementations are welcome. See each suite's README for reproduction instructions and the vector schema.
59 changes: 59 additions & 0 deletions fixtures/action-ref-verify/v0/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# action-ref-verify conformance fixtures (v0)

Conformance vectors for verifying `action_ref` work-receipt digest binding as discussed in [#2357](https://github.com/x402-foundation/x402/issues/2357).

## Derivation

```
action_ref = SHA-256(JCS(preimage))
```

Where JCS is [JSON Canonicalization Scheme (RFC 8785)](https://www.rfc-editor.org/rfc/rfc8785). The preimage is a JSON object with:

| Field | Type | Description |
|-------|------|-------------|
| `action_type` | string | What was requested |
| `agent_id` | string | Who requested it |
| `scope` | string | Scoping context |
| `timestamp_ms` | integer | Epoch milliseconds (not RFC 3339 string) |

`timestamp_ms` (epoch integer) is the canonical field name. See vector 0009 for why this is load-bearing.

## Vectors

| # | Name | Result | Tests |
|---|------|--------|-------|
| 0001 | baseline | PASS | Canonical happy path |
| 0002 | ms-precision-trap | FAIL | Float vs integer timestamp_ms |
| 0003 | trailing-whitespace | FAIL | Space in string value changes digest |
| 0004 | extra-field-ignored | PASS | Additional preimage fields (open model) |
| 0005 | key-order-resilience | PASS | Reversed key order, same digest |
| 0006 | rfc8785-negative-zero | PASS | timestamp_ms = 0 |
| 0007 | key-sorting-stress | PASS | Unicode in values |
| 0008 | interop-shared-payment-hash | PASS | Cross-layer binding with payment_hash |
| 0009 | field-name-load-bearing | FAIL | `timestamp` vs `timestamp_ms` divergence |

Vectors 0002, 0003, and 0009 are negative vectors: they MUST fail verification against the expected digest.

## Cross-layer interop

Vector 0008 includes a `payment_hash` field binding the work-receipt layer to a payment-conditions layer. Three independent implementations (Node.js, Python, Rust) produced identical `action_ref` and `payment_hash` digests from this preimage with independent JCS canonicalization and no shared code.

## Reproducing

Any language with a JCS (RFC 8785) implementation and SHA-256 can verify these vectors:

```bash
# Node.js (using the action-ref-verify CLI)
npx action-ref-verify vectors/0001-baseline.json

# Or manually:
# 1. Parse the preimage object
# 2. Canonicalize with JCS (RFC 8785)
# 3. SHA-256 hash the canonical bytes
# 4. Compare hex output to expected_digest
```

## Source

These vectors are derived from the [action-ref-verify](https://github.com/andysalvo/action-ref-verify) conformance harness (v0.3.0, Apache-2.0).
23 changes: 23 additions & 0 deletions fixtures/action-ref-verify/v0/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"suite": "action-ref-verify",
"version": "v0",
"spec": "action-ref-v1-jcs-sha256",
"digest_algorithm": "SHA-256",
"canonicalization": "RFC 8785 (JCS)",
"preimage_fields": ["action_type", "agent_id", "scope", "timestamp_ms"],
"timestamp_encoding": "epoch_ms_integer",
"derivation": "action_ref = SHA-256(JCS(preimage))",
"source": "https://github.com/andysalvo/action-ref-verify",
"issue": "https://github.com/x402-foundation/x402/issues/2357",
"vectors": [
{"file": "vectors/0001-baseline.json", "expected_result": "PASS"},
{"file": "vectors/0002-ms-precision-trap.json", "expected_result": "FAIL"},
{"file": "vectors/0003-trailing-whitespace.json", "expected_result": "FAIL"},
{"file": "vectors/0004-extra-field-ignored.json", "expected_result": "PASS"},
{"file": "vectors/0005-key-order-resilience.json", "expected_result": "PASS"},
{"file": "vectors/0006-rfc8785-negative-zero.json", "expected_result": "PASS"},
{"file": "vectors/0007-key-sorting-stress.json", "expected_result": "PASS"},
{"file": "vectors/0008-interop-shared-payment-hash.json", "expected_result": "PASS"},
{"file": "vectors/0009-field-name-load-bearing.json", "expected_result": "FAIL"}
]
}
13 changes: 13 additions & 0 deletions fixtures/action-ref-verify/v0/vectors/0001-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "baseline",
"spec": "action-ref-v1-jcs-sha256",
"expected_result": "PASS",
"preimage": {
"action_type": "sanctions_screen",
"agent_id": "did:web:agent-7.example.com",
"scope": "counterparty-due-diligence",
"timestamp_ms": 1747728000000
},
"expected_digest": "10d8a38c01d8672176aa6e5209a368fde3e1831640d69e15283142b35880c2c1",
"notes": "Canonical baseline vector. Three independent implementations (Node.js, Python, Rust) produce this digest from the same preimage via RFC 8785 JCS canonicalization."
}
13 changes: 13 additions & 0 deletions fixtures/action-ref-verify/v0/vectors/0002-ms-precision-trap.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "ms-precision-trap",
"spec": "action-ref-v1-jcs-sha256",
"expected_result": "FAIL",
"preimage": {
"action_type": "sanctions_screen",
"agent_id": "did:web:agent-7.example.com",
"scope": "counterparty-due-diligence",
"timestamp_ms": 1747728000000.0
},
"expected_digest": "10d8a38c01d8672176aa6e5209a368fde3e1831640d69e15283142b35880c2c1",
"notes": "timestamp_ms as a float (1747728000000.0) instead of integer. JCS serializes floats differently from integers. Implementations MUST reject non-integer timestamp_ms values before canonicalization."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "trailing-whitespace",
"spec": "action-ref-v1-jcs-sha256",
"expected_result": "FAIL",
"preimage": {
"action_type": "sanctions_screen ",
"agent_id": "did:web:agent-7.example.com",
"scope": "counterparty-due-diligence",
"timestamp_ms": 1747728000000
},
"expected_digest": "10d8a38c01d8672176aa6e5209a368fde3e1831640d69e15283142b35880c2c1",
"notes": "Trailing space in action_type value. JCS preserves string content byte-for-byte; the trailing space produces a different canonical output and a different digest. Verifiers MUST NOT trim string values before canonicalization."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "extra-field-ignored",
"spec": "action-ref-v1-jcs-sha256",
"expected_result": "PASS",
"preimage": {
"action_type": "sanctions_screen",
"agent_id": "did:web:agent-7.example.com",
"extra_metadata": "this field is not in the canonical set",
"scope": "counterparty-due-diligence",
"timestamp_ms": 1747728000000
},
"expected_digest": "cddb80e79d862c796afcb0a6ac23a34e43d484b095e1f71f068585b82ebb6f30",
"notes": "Extra fields in the preimage object. Implementations may choose to include all fields in the JCS input (open preimage model) or strip to a canonical field set (closed preimage model). This vector documents the ambiguity; the expected behavior depends on whether the spec adopts open or closed canonicalization. Mark as SKIP if your implementation uses a closed field set."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "key-order-resilience",
"spec": "action-ref-v1-jcs-sha256",
"expected_result": "PASS",
"preimage": {
"timestamp_ms": 1747728000000,
"scope": "counterparty-due-diligence",
"agent_id": "did:web:agent-7.example.com",
"action_type": "sanctions_screen"
},
"expected_digest": "10d8a38c01d8672176aa6e5209a368fde3e1831640d69e15283142b35880c2c1",
"notes": "Same fields as 0001 but in reversed key order. JCS (RFC 8785) sorts keys lexicographically before serialization. Input key order MUST NOT affect the output digest."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "rfc8785-negative-zero",
"spec": "action-ref-v1-jcs-sha256",
"expected_result": "PASS",
"preimage": {
"action_type": "sanctions_screen",
"agent_id": "did:web:agent-7.example.com",
"scope": "counterparty-due-diligence",
"timestamp_ms": 0
},
"expected_digest": "9434c56780711a9ad9698df99382123a339c938c018281f6a25118f7de544d75",
"notes": "timestamp_ms = 0. Per RFC 8785 Section 3.2.2.3, negative zero (-0) MUST be serialized as 0. This vector verifies that implementations handle the zero case without producing -0 in the canonical output."
}
13 changes: 13 additions & 0 deletions fixtures/action-ref-verify/v0/vectors/0007-key-sorting-stress.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "key-sorting-stress",
"spec": "action-ref-v1-jcs-sha256",
"expected_result": "PASS",
"preimage": {
"action_type": "kyc_verification",
"agent_id": "did:web:agent-é.example.com",
"scope": "identity-proof",
"timestamp_ms": 1747728000000
},
"expected_digest": "e3372ec20a90f6ac8d46bf0c3f3f257b475a8236e430291e68ca372159ccd96b",
"notes": "Unicode in agent_id (U+00E9, e-acute). RFC 8785 does not normalize Unicode; the raw UTF-8 bytes are preserved in the canonical output. JCS key sorting uses code-unit comparison. Implementations MUST NOT apply NFC/NFD normalization before canonicalization."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "interop-shared-payment-hash",
"spec": "action-ref-v1-jcs-sha256",
"expected_result": "PASS",
"preimage": {
"action_type": "sanctions_screen",
"agent_id": "did:web:agent-7.example.com",
"scope": "counterparty-due-diligence",
"timestamp_ms": 1747728000000
},
"expected_digest": "10d8a38c01d8672176aa6e5209a368fde3e1831640d69e15283142b35880c2c1",
"payment_hash": "2ed186ebc66947eaac6a05a88c7bc096ee07ac11a2c44bb5580bd72b3670f580",
"notes": "Cross-layer interop vector from x402-foundation/x402#2357. The payment_hash binds this work-receipt to a payment-conditions layer (STARK or signature-based). Three independent implementations (Node.js, Python, Rust) reproduced identical action_ref and payment_hash digests with no shared code."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "field-name-load-bearing",
"spec": "action-ref-v1-jcs-sha256",
"expected_result": "FAIL",
"preimage": {
"action_type": "sanctions_screen",
"agent_id": "did:web:agent-7.example.com",
"scope": "counterparty-due-diligence",
"timestamp": "2025-05-20T00:00:00.000Z"
},
"expected_digest": "10d8a38c01d8672176aa6e5209a368fde3e1831640d69e15283142b35880c2c1",
"notes": "Same semantic instant as 0008 (1747728000000 ms = 2025-05-20T00:00:00.000Z) but encoded as 'timestamp' (RFC 3339 string) instead of 'timestamp_ms' (epoch integer). JCS treats field names as opaque bytes; different field names produce different canonical output and different digests. This vector MUST FAIL when verified against the 0008 expected_digest. Demonstrates why the spec must pin the field name, not just the semantics."
}
Loading