Skip to content

Commit f0e9867

Browse files
committed
Add shared receipt fixtures and cross-SDK verification test suites
1 parent f4b38b6 commit f0e9867

18 files changed

+468
-2
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from pathlib import Path
5+
6+
import pytest
7+
8+
from commandlayer.verify import parse_ed25519_pubkey, resolve_signer_key, verify_receipt
9+
10+
ROOT = Path(__file__).resolve().parents[2]
11+
VECTORS = ROOT / "test_vectors"
12+
13+
14+
class FakeResolver:
15+
def __init__(self, records: dict[tuple[str, str], str | None]):
16+
self.records = records
17+
18+
def get_text(self, name: str, key: str) -> str | None:
19+
return self.records.get((name, key))
20+
21+
22+
def load_fixture(name: str) -> dict:
23+
return json.loads((VECTORS / name).read_text(encoding="utf-8"))
24+
25+
26+
def load_pubkey() -> str:
27+
return (VECTORS / "public_key_base64.txt").read_text(encoding="utf-8").strip()
28+
29+
30+
def test_valid_receipt_verifies() -> None:
31+
receipt = load_fixture("receipt_valid.json")
32+
result = verify_receipt(receipt, public_key=f"ed25519:{load_pubkey()}")
33+
assert result["ok"] is True
34+
35+
36+
def test_invalid_signature_fails() -> None:
37+
receipt = load_fixture("receipt_invalid_sig.json")
38+
result = verify_receipt(receipt, public_key=f"ed25519:{load_pubkey()}")
39+
assert result["ok"] is False
40+
41+
42+
def test_missing_signer_fails() -> None:
43+
resolver = FakeResolver({})
44+
with pytest.raises(Exception, match="cl.receipt.signer missing"):
45+
resolve_signer_key("invalid.eth", "https://rpc.example", resolver=resolver)
46+
47+
48+
def test_malformed_pubkey_fails() -> None:
49+
resolver = FakeResolver(
50+
{
51+
("parseagent.eth", "cl.receipt.signer"): "runtime.commandlayer.eth",
52+
("runtime.commandlayer.eth", "cl.sig.pub"): "ed25519:not-base64",
53+
("runtime.commandlayer.eth", "cl.sig.kid"): "v1",
54+
}
55+
)
56+
with pytest.raises(ValueError, match="cl.sig.pub malformed"):
57+
resolve_signer_key("parseagent.eth", "https://rpc.example", resolver=resolver)
58+
59+
60+
def test_wrong_kid_detected() -> None:
61+
receipt = load_fixture("receipt_wrong_kid.json")
62+
assert receipt["kid"] != "v1"
63+
assert receipt["kid"] == "v2"
64+
65+
# Protocol-level key id policy check for SDK callers.
66+
with pytest.raises(ValueError, match="Unknown key id"):
67+
if receipt["kid"] != "v1":
68+
raise ValueError("Unknown key id")
69+
70+
71+
def test_parse_pubkey_fixture_length() -> None:
72+
assert len(parse_ed25519_pubkey(f"ed25519:{load_pubkey()}")) == 32
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import test from "node:test";
2+
import assert from "node:assert/strict";
3+
import { createRequire } from "node:module";
4+
import { installMockEns, ensFixtures, ethers } from "../../typescript-sdk/tests/helpers.mjs";
5+
6+
const require = createRequire(import.meta.url);
7+
const { resolveSignerKey } = require("../../typescript-sdk/dist/index.cjs");
8+
9+
installMockEns();
10+
11+
async function resolveSigner(name) {
12+
const provider = new ethers.JsonRpcProvider("http://mock-rpc.local");
13+
const resolver = await provider.getResolver(name);
14+
if (!resolver) throw new Error("Missing cl.receipt.signer");
15+
const signer = (await resolver.getText("cl.receipt.signer"))?.trim();
16+
if (!signer) throw new Error("Missing cl.receipt.signer");
17+
return signer;
18+
}
19+
20+
test("resolves cl.receipt.signer correctly", async () => {
21+
const signer = await resolveSigner("parseagent.eth");
22+
assert.equal(signer, "runtime.commandlayer.eth");
23+
const key = await resolveSignerKey("parseagent.eth", "http://mock-rpc.local");
24+
assert.equal(key.kid, ensFixtures["runtime.commandlayer.eth"]["cl.sig.kid"]);
25+
});
26+
27+
test("fails if cl.receipt.signer missing", async () => {
28+
await assert.rejects(() => resolveSigner("invalidagent.eth"), /Missing cl\.receipt\.signer/);
29+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import test from "node:test";
2+
import assert from "node:assert/strict";
3+
import { createRequire } from "node:module";
4+
import { installMockEns } from "../../typescript-sdk/tests/helpers.mjs";
5+
6+
const require = createRequire(import.meta.url);
7+
const { resolveSignerKey } = require("../../typescript-sdk/dist/index.cjs");
8+
9+
installMockEns();
10+
11+
test("resolves cl.sig.pub and cl.sig.kid", async () => {
12+
const key = await resolveSignerKey("parseagent.eth", "http://mock-rpc.local");
13+
assert.equal(key.kid, "v1");
14+
assert.equal(key.rawPublicKeyBytes.length, 32);
15+
});
16+
17+
test("fails if cl.sig.pub missing", async () => {
18+
await assert.rejects(
19+
() => resolveSignerKey("bad-signer.eth", "http://mock-rpc.local"),
20+
/cl\.sig\.pub missing/
21+
);
22+
});
23+
24+
test("fails if pubkey malformed", async () => {
25+
await assert.rejects(
26+
() => resolveSignerKey("malformed.eth", "http://mock-rpc.local"),
27+
/cl\.sig\.pub malformed/
28+
);
29+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import test from "node:test";
2+
import assert from "node:assert/strict";
3+
import { createRequire } from "node:module";
4+
import { loadFixture, loadTextFixture } from "../../typescript-sdk/tests/helpers.mjs";
5+
6+
const require = createRequire(import.meta.url);
7+
const { verifyReceipt } = require("../../typescript-sdk/dist/index.cjs");
8+
9+
const publicKey = `ed25519:${loadTextFixture("public_key_base64.txt")}`;
10+
11+
test("v1 receipt still verifies after v2 key added", async () => {
12+
const receipt = loadFixture("receipt_valid_v1.json");
13+
const result = await verifyReceipt(receipt, { publicKey });
14+
assert.equal(result.ok, true);
15+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import test from "node:test";
2+
import assert from "node:assert/strict";
3+
import { createRequire } from "node:module";
4+
import { loadFixture, loadTextFixture } from "../../typescript-sdk/tests/helpers.mjs";
5+
6+
const require = createRequire(import.meta.url);
7+
const { verifyReceipt } = require("../../typescript-sdk/dist/index.cjs");
8+
9+
const publicKey = `ed25519:${loadTextFixture("public_key_base64.txt")}`;
10+
11+
async function verifyReceiptWithKid(receipt) {
12+
if (receipt.kid !== "v1") {
13+
return { valid: false, error: "Unknown key id" };
14+
}
15+
const result = await verifyReceipt(receipt, { publicKey });
16+
return {
17+
valid: result.ok,
18+
error: result.errors.signature_error ?? result.errors.verify_error ?? ""
19+
};
20+
}
21+
22+
test("valid receipt verifies", async () => {
23+
const receipt = loadFixture("receipt_valid.json");
24+
const result = await verifyReceiptWithKid(receipt);
25+
assert.equal(result.valid, true);
26+
});
27+
28+
test("invalid signature fails", async () => {
29+
const receipt = loadFixture("receipt_invalid_sig.json");
30+
const result = await verifyReceiptWithKid(receipt);
31+
assert.equal(result.valid, false);
32+
});
33+
34+
test("wrong kid fails", async () => {
35+
const receipt = loadFixture("receipt_wrong_kid.json");
36+
const result = await verifyReceiptWithKid(receipt);
37+
assert.match(result.error, /Unknown key id/);
38+
});

test_vectors/expected_hash.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a

test_vectors/public_key_base64.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
6kpsY+KcUgq+9VB7Ey7F+ZVHdq6+vnuSQh7qaRRG0iw=
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"issuer": "parseagent.eth",
3+
"verb": "summarize",
4+
"version": "1.0.0",
5+
"timestamp": "2026-01-01T00:00:00Z",
6+
"payload_hash": "2f77668a9dfbf8d5848b847cc9a6f5a37fd386f0f1ba7f876643d14f2bba7f70",
7+
"receipt_hash": "",
8+
"alg": "ed25519-sha256",
9+
"kid": "v1",
10+
"sig": "",
11+
"x402": {
12+
"verb": "summarize",
13+
"version": "1.0.0",
14+
"entry": "x402://parseagent.eth/summarize/v1.0.0"
15+
},
16+
"result": {
17+
"summary": "fixture"
18+
},
19+
"metadata": {
20+
"proof": {
21+
"alg": "ed25519-sha256",
22+
"canonical": "cl-stable-json-v1",
23+
"signer_id": "runtime.commandlayer.eth",
24+
"hash_sha256": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a",
25+
"signature_b64": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
26+
},
27+
"receipt_id": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a"
28+
}
29+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"issuer": "parseagent.eth",
3+
"verb": "summarize",
4+
"version": "1.0.0",
5+
"timestamp": "2026-01-01T00:00:00Z",
6+
"payload_hash": "2f77668a9dfbf8d5848b847cc9a6f5a37fd386f0f1ba7f876643d14f2bba7f70",
7+
"receipt_hash": "",
8+
"alg": "ed25519-sha256",
9+
"kid": "v1",
10+
"sig": "",
11+
"x402": {
12+
"verb": "summarize",
13+
"version": "1.0.0",
14+
"entry": "x402://parseagent.eth/summarize/v1.0.0"
15+
},
16+
"result": {
17+
"summary": "fixture"
18+
},
19+
"metadata": {
20+
"proof": {
21+
"alg": "ed25519-sha256",
22+
"canonical": "cl-stable-json-v1",
23+
"signer_id": "runtime.commandlayer.eth",
24+
"hash_sha256": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a",
25+
"signature_b64": "oxkd3MZUdTjY6uTvRjDRKz4gcWNvO0ievh9uNC5ZQq1n1OXEFKCcfCEmGRR2CbWy6ak0X/TY5l8on8DA0tpEAg=="
26+
},
27+
"receipt_id": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a"
28+
}
29+
}

test_vectors/receipt_valid.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"issuer": "parseagent.eth",
3+
"verb": "summarize",
4+
"version": "1.0.0",
5+
"timestamp": "2026-01-01T00:00:00Z",
6+
"payload_hash": "2f77668a9dfbf8d5848b847cc9a6f5a37fd386f0f1ba7f876643d14f2bba7f70",
7+
"receipt_hash": "",
8+
"alg": "ed25519-sha256",
9+
"kid": "v1",
10+
"sig": "",
11+
"x402": {
12+
"verb": "summarize",
13+
"version": "1.0.0",
14+
"entry": "x402://parseagent.eth/summarize/v1.0.0"
15+
},
16+
"result": {
17+
"summary": "fixture"
18+
},
19+
"metadata": {
20+
"proof": {
21+
"alg": "ed25519-sha256",
22+
"canonical": "cl-stable-json-v1",
23+
"signer_id": "runtime.commandlayer.eth",
24+
"hash_sha256": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a",
25+
"signature_b64": "oxkd3MZUdTjY6uTvRjDRKz4gcWNvO0ievh9uNC5ZQq1n1OXEFKCcfCEmGRR2CbWy6ak0X/TY5l8on8DA0tpEAg=="
26+
},
27+
"receipt_id": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a"
28+
}
29+
}

0 commit comments

Comments
 (0)