-
Notifications
You must be signed in to change notification settings - Fork 0
Add shared receipt fixtures and cross-SDK verification test suites #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| from pathlib import Path | ||
|
|
||
| import pytest | ||
|
|
||
| from commandlayer.verify import parse_ed25519_pubkey, resolve_signer_key, verify_receipt | ||
|
|
||
| ROOT = Path(__file__).resolve().parents[2] | ||
| VECTORS = ROOT / "test_vectors" | ||
|
|
||
|
|
||
| class FakeResolver: | ||
| def __init__(self, records: dict[tuple[str, str], str | None]): | ||
| self.records = records | ||
|
|
||
| def get_text(self, name: str, key: str) -> str | None: | ||
| return self.records.get((name, key)) | ||
|
|
||
|
|
||
| def load_fixture(name: str) -> dict: | ||
| return json.loads((VECTORS / name).read_text(encoding="utf-8")) | ||
|
|
||
|
|
||
| def load_pubkey() -> str: | ||
| return (VECTORS / "public_key_base64.txt").read_text(encoding="utf-8").strip() | ||
|
|
||
|
|
||
| def test_valid_receipt_verifies() -> None: | ||
| receipt = load_fixture("receipt_valid.json") | ||
| result = verify_receipt(receipt, public_key=f"ed25519:{load_pubkey()}") | ||
| assert result["ok"] is True | ||
|
|
||
|
|
||
| def test_invalid_signature_fails() -> None: | ||
| receipt = load_fixture("receipt_invalid_sig.json") | ||
| result = verify_receipt(receipt, public_key=f"ed25519:{load_pubkey()}") | ||
| assert result["ok"] is False | ||
|
|
||
|
|
||
| def test_missing_signer_fails() -> None: | ||
| resolver = FakeResolver({}) | ||
| with pytest.raises(Exception, match="cl.receipt.signer missing"): | ||
| resolve_signer_key("invalid.eth", "https://rpc.example", resolver=resolver) | ||
|
|
||
|
|
||
| def test_malformed_pubkey_fails() -> None: | ||
| resolver = FakeResolver( | ||
| { | ||
| ("parseagent.eth", "cl.receipt.signer"): "runtime.commandlayer.eth", | ||
| ("runtime.commandlayer.eth", "cl.sig.pub"): "ed25519:not-base64", | ||
| ("runtime.commandlayer.eth", "cl.sig.kid"): "v1", | ||
| } | ||
| ) | ||
| with pytest.raises(ValueError, match="cl.sig.pub malformed"): | ||
| resolve_signer_key("parseagent.eth", "https://rpc.example", resolver=resolver) | ||
|
|
||
|
|
||
| def test_wrong_kid_detected() -> None: | ||
| receipt = load_fixture("receipt_wrong_kid.json") | ||
| assert receipt["kid"] != "v1" | ||
| assert receipt["kid"] == "v2" | ||
|
|
||
| # Protocol-level key id policy check for SDK callers. | ||
| with pytest.raises(ValueError, match="Unknown key id"): | ||
| if receipt["kid"] != "v1": | ||
| raise ValueError("Unknown key id") | ||
|
|
||
|
|
||
| def test_parse_pubkey_fixture_length() -> None: | ||
| assert len(parse_ed25519_pubkey(f"ed25519:{load_pubkey()}")) == 32 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import test from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
| import { createRequire } from "node:module"; | ||
| import { installMockEns, ensFixtures, ethers } from "../../typescript-sdk/tests/helpers.mjs"; | ||
|
|
||
| const require = createRequire(import.meta.url); | ||
| const { resolveSignerKey } = require("../../typescript-sdk/dist/index.cjs"); | ||
|
|
||
| installMockEns(); | ||
|
|
||
| async function resolveSigner(name) { | ||
| const provider = new ethers.JsonRpcProvider("http://mock-rpc.local"); | ||
| const resolver = await provider.getResolver(name); | ||
| if (!resolver) throw new Error("Missing cl.receipt.signer"); | ||
| const signer = (await resolver.getText("cl.receipt.signer"))?.trim(); | ||
| if (!signer) throw new Error("Missing cl.receipt.signer"); | ||
| return signer; | ||
| } | ||
|
|
||
| test("resolves cl.receipt.signer correctly", async () => { | ||
| const signer = await resolveSigner("parseagent.eth"); | ||
| assert.equal(signer, "runtime.commandlayer.eth"); | ||
| const key = await resolveSignerKey("parseagent.eth", "http://mock-rpc.local"); | ||
| assert.equal(key.kid, ensFixtures["runtime.commandlayer.eth"]["cl.sig.kid"]); | ||
| }); | ||
|
|
||
| test("fails if cl.receipt.signer missing", async () => { | ||
| await assert.rejects(() => resolveSigner("invalidagent.eth"), /Missing cl\.receipt\.signer/); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import test from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
| import { createRequire } from "node:module"; | ||
| import { installMockEns } from "../../typescript-sdk/tests/helpers.mjs"; | ||
|
|
||
| const require = createRequire(import.meta.url); | ||
| const { resolveSignerKey } = require("../../typescript-sdk/dist/index.cjs"); | ||
|
|
||
| installMockEns(); | ||
|
|
||
| test("resolves cl.sig.pub and cl.sig.kid", async () => { | ||
| const key = await resolveSignerKey("parseagent.eth", "http://mock-rpc.local"); | ||
| assert.equal(key.kid, "v1"); | ||
| assert.equal(key.rawPublicKeyBytes.length, 32); | ||
| }); | ||
|
|
||
| test("fails if cl.sig.pub missing", async () => { | ||
| await assert.rejects( | ||
| () => resolveSignerKey("bad-signer.eth", "http://mock-rpc.local"), | ||
| /cl\.sig\.pub missing/ | ||
| ); | ||
| }); | ||
|
|
||
| test("fails if pubkey malformed", async () => { | ||
| await assert.rejects( | ||
| () => resolveSignerKey("malformed.eth", "http://mock-rpc.local"), | ||
| /cl\.sig\.pub malformed/ | ||
| ); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import test from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
| import { createRequire } from "node:module"; | ||
| import { loadFixture, loadTextFixture } from "../../typescript-sdk/tests/helpers.mjs"; | ||
|
|
||
| const require = createRequire(import.meta.url); | ||
| const { verifyReceipt } = require("../../typescript-sdk/dist/index.cjs"); | ||
|
|
||
| const publicKey = `ed25519:${loadTextFixture("public_key_base64.txt")}`; | ||
|
|
||
| test("v1 receipt still verifies after v2 key added", async () => { | ||
| const receipt = loadFixture("receipt_valid_v1.json"); | ||
| const result = await verifyReceipt(receipt, { publicKey }); | ||
| assert.equal(result.ok, true); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import test from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
| import { createRequire } from "node:module"; | ||
| import { loadFixture, loadTextFixture } from "../../typescript-sdk/tests/helpers.mjs"; | ||
|
|
||
| const require = createRequire(import.meta.url); | ||
| const { verifyReceipt } = require("../../typescript-sdk/dist/index.cjs"); | ||
|
|
||
| const publicKey = `ed25519:${loadTextFixture("public_key_base64.txt")}`; | ||
|
|
||
| async function verifyReceiptWithKid(receipt) { | ||
| if (receipt.kid !== "v1") { | ||
| return { valid: false, error: "Unknown key id" }; | ||
| } | ||
| const result = await verifyReceipt(receipt, { publicKey }); | ||
| return { | ||
| valid: result.ok, | ||
| error: result.errors.signature_error ?? result.errors.verify_error ?? "" | ||
| }; | ||
| } | ||
|
|
||
| test("valid receipt verifies", async () => { | ||
| const receipt = loadFixture("receipt_valid.json"); | ||
| const result = await verifyReceiptWithKid(receipt); | ||
| assert.equal(result.valid, true); | ||
| }); | ||
|
|
||
| test("invalid signature fails", async () => { | ||
| const receipt = loadFixture("receipt_invalid_sig.json"); | ||
| const result = await verifyReceiptWithKid(receipt); | ||
| assert.equal(result.valid, false); | ||
| }); | ||
|
|
||
| test("wrong kid fails", async () => { | ||
| const receipt = loadFixture("receipt_wrong_kid.json"); | ||
| const result = await verifyReceiptWithKid(receipt); | ||
| assert.match(result.error, /Unknown key id/); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 6kpsY+KcUgq+9VB7Ey7F+ZVHdq6+vnuSQh7qaRRG0iw= |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| { | ||
| "issuer": "parseagent.eth", | ||
| "verb": "summarize", | ||
| "version": "1.0.0", | ||
| "timestamp": "2026-01-01T00:00:00Z", | ||
| "payload_hash": "2f77668a9dfbf8d5848b847cc9a6f5a37fd386f0f1ba7f876643d14f2bba7f70", | ||
| "receipt_hash": "", | ||
| "alg": "ed25519-sha256", | ||
| "kid": "v1", | ||
| "sig": "", | ||
| "x402": { | ||
| "verb": "summarize", | ||
| "version": "1.0.0", | ||
| "entry": "x402://parseagent.eth/summarize/v1.0.0" | ||
| }, | ||
| "result": { | ||
| "summary": "fixture" | ||
| }, | ||
| "metadata": { | ||
| "proof": { | ||
| "alg": "ed25519-sha256", | ||
| "canonical": "cl-stable-json-v1", | ||
| "signer_id": "runtime.commandlayer.eth", | ||
| "hash_sha256": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a", | ||
| "signature_b64": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" | ||
| }, | ||
| "receipt_id": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| { | ||
| "issuer": "parseagent.eth", | ||
| "verb": "summarize", | ||
| "version": "1.0.0", | ||
| "timestamp": "2026-01-01T00:00:00Z", | ||
| "payload_hash": "2f77668a9dfbf8d5848b847cc9a6f5a37fd386f0f1ba7f876643d14f2bba7f70", | ||
| "receipt_hash": "", | ||
| "alg": "ed25519-sha256", | ||
| "kid": "v1", | ||
| "sig": "", | ||
| "x402": { | ||
| "verb": "summarize", | ||
| "version": "1.0.0", | ||
| "entry": "x402://parseagent.eth/summarize/v1.0.0" | ||
| }, | ||
| "result": { | ||
| "summary": "fixture" | ||
| }, | ||
| "metadata": { | ||
| "proof": { | ||
| "alg": "ed25519-sha256", | ||
| "canonical": "cl-stable-json-v1", | ||
| "signer_id": "runtime.commandlayer.eth", | ||
| "hash_sha256": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a", | ||
| "signature_b64": "oxkd3MZUdTjY6uTvRjDRKz4gcWNvO0ievh9uNC5ZQq1n1OXEFKCcfCEmGRR2CbWy6ak0X/TY5l8on8DA0tpEAg==" | ||
| }, | ||
| "receipt_id": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| { | ||
| "issuer": "parseagent.eth", | ||
| "verb": "summarize", | ||
| "version": "1.0.0", | ||
| "timestamp": "2026-01-01T00:00:00Z", | ||
| "payload_hash": "2f77668a9dfbf8d5848b847cc9a6f5a37fd386f0f1ba7f876643d14f2bba7f70", | ||
| "receipt_hash": "", | ||
| "alg": "ed25519-sha256", | ||
| "kid": "v1", | ||
| "sig": "", | ||
| "x402": { | ||
| "verb": "summarize", | ||
| "version": "1.0.0", | ||
| "entry": "x402://parseagent.eth/summarize/v1.0.0" | ||
| }, | ||
| "result": { | ||
| "summary": "fixture" | ||
| }, | ||
| "metadata": { | ||
| "proof": { | ||
| "alg": "ed25519-sha256", | ||
| "canonical": "cl-stable-json-v1", | ||
| "signer_id": "runtime.commandlayer.eth", | ||
| "hash_sha256": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a", | ||
| "signature_b64": "oxkd3MZUdTjY6uTvRjDRKz4gcWNvO0ievh9uNC5ZQq1n1OXEFKCcfCEmGRR2CbWy6ak0X/TY5l8on8DA0tpEAg==" | ||
| }, | ||
| "receipt_id": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| { | ||
| "issuer": "parseagent.eth", | ||
| "verb": "summarize", | ||
| "version": "1.0.0", | ||
| "timestamp": "2026-01-01T00:00:00Z", | ||
| "payload_hash": "2f77668a9dfbf8d5848b847cc9a6f5a37fd386f0f1ba7f876643d14f2bba7f70", | ||
| "receipt_hash": "", | ||
| "alg": "ed25519-sha256", | ||
| "kid": "v1", | ||
| "sig": "", | ||
| "x402": { | ||
| "verb": "summarize", | ||
| "version": "1.0.0", | ||
| "entry": "x402://parseagent.eth/summarize/v1.0.0" | ||
| }, | ||
| "result": { | ||
| "summary": "fixture" | ||
| }, | ||
| "metadata": { | ||
| "proof": { | ||
| "alg": "ed25519-sha256", | ||
| "canonical": "cl-stable-json-v1", | ||
| "signer_id": "runtime.commandlayer.eth", | ||
| "hash_sha256": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a", | ||
| "signature_b64": "oxkd3MZUdTjY6uTvRjDRKz4gcWNvO0ievh9uNC5ZQq1n1OXEFKCcfCEmGRR2CbWy6ak0X/TY5l8on8DA0tpEAg==" | ||
| }, | ||
| "receipt_id": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| { | ||
| "issuer": "parseagent.eth", | ||
| "verb": "summarize", | ||
| "version": "1.0.0", | ||
| "timestamp": "2026-01-01T00:00:00Z", | ||
| "payload_hash": "2f77668a9dfbf8d5848b847cc9a6f5a37fd386f0f1ba7f876643d14f2bba7f70", | ||
| "receipt_hash": "", | ||
| "alg": "ed25519-sha256", | ||
| "kid": "v2", | ||
| "sig": "", | ||
| "x402": { | ||
| "verb": "summarize", | ||
| "version": "1.0.0", | ||
| "entry": "x402://parseagent.eth/summarize/v1.0.0" | ||
| }, | ||
| "result": { | ||
| "summary": "fixture" | ||
| }, | ||
| "metadata": { | ||
| "proof": { | ||
| "alg": "ed25519-sha256", | ||
| "canonical": "cl-stable-json-v1", | ||
| "signer_id": "runtime.commandlayer.eth", | ||
| "hash_sha256": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a", | ||
| "signature_b64": "oxkd3MZUdTjY6uTvRjDRKz4gcWNvO0ievh9uNC5ZQq1n1OXEFKCcfCEmGRR2CbWy6ak0X/TY5l8on8DA0tpEAg==" | ||
| }, | ||
| "receipt_id": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import { spawnSync } from "node:child_process"; | ||
|
|
||
| const suites = [ | ||
| "runtime/tests/*.test.mjs", | ||
| "typescript-sdk/tests/*.test.mjs" | ||
| ]; | ||
|
|
||
| for (const pattern of suites) { | ||
| const run = spawnSync("node", ["--test", pattern], { | ||
| stdio: "inherit", | ||
| cwd: new URL("../..", import.meta.url) | ||
| }); | ||
| if (run.status !== 0) { | ||
| process.exit(run.status ?? 1); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import test from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
| import { createRequire } from "node:module"; | ||
| import { loadFixture, loadTextFixture } from "./helpers.mjs"; | ||
|
|
||
| const require = createRequire(import.meta.url); | ||
| const { recomputeReceiptHashSha256 } = require("../dist/index.cjs"); | ||
|
|
||
| test("stable JSON produces deterministic hash", () => { | ||
| const receipt = loadFixture("receipt_valid.json"); | ||
| const hash1 = recomputeReceiptHashSha256(receipt).hash_sha256; | ||
| const hash2 = recomputeReceiptHashSha256(JSON.parse(JSON.stringify(receipt))).hash_sha256; | ||
|
|
||
| assert.equal(hash1, hash2); | ||
| assert.equal(hash1, loadTextFixture("expected_hash.txt")); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test never simulates a rotated key: it only loads a v1 receipt and verifies it with a fixed v1 public key, without ENS lookup or any v2 key state. As a result, regressions in key-rotation behavior (for example, resolver behavior when a newer key/kid is present) would still pass while this test claims to validate the post-rotation path, which gives false confidence for a security-sensitive flow.
Useful? React with 👍 / 👎.