Skip to content
Merged
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
72 changes: 72 additions & 0 deletions python-sdk/tests/test_verification.py
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
29 changes: 29 additions & 0 deletions runtime/tests/ens-resolution.test.mjs
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/);
});
29 changes: 29 additions & 0 deletions runtime/tests/key-resolution.test.mjs
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/
);
});
15 changes: 15 additions & 0 deletions runtime/tests/key-rotation.test.mjs
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 });
Comment on lines +12 to +13

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exercise key rotation in key-rotation test

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 👍 / 👎.

assert.equal(result.ok, true);
});
38 changes: 38 additions & 0 deletions runtime/tests/receipt-verification.test.mjs
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/);
});
1 change: 1 addition & 0 deletions test_vectors/expected_hash.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a
1 change: 1 addition & 0 deletions test_vectors/public_key_base64.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
6kpsY+KcUgq+9VB7Ey7F+ZVHdq6+vnuSQh7qaRRG0iw=
29 changes: 29 additions & 0 deletions test_vectors/receipt_invalid_sig.json
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"
}
}
29 changes: 29 additions & 0 deletions test_vectors/receipt_malformed_pubkey.json
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"
}
}
29 changes: 29 additions & 0 deletions test_vectors/receipt_valid.json
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"
}
}
29 changes: 29 additions & 0 deletions test_vectors/receipt_valid_v1.json
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"
}
}
29 changes: 29 additions & 0 deletions test_vectors/receipt_wrong_kid.json
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"
}
}
5 changes: 3 additions & 2 deletions typescript-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@
"typecheck": "tsc -p tsconfig.json --noEmit",
"prepack": "npm run build",
"test:cli-smoke": "node scripts/cli-smoke.mjs",
"test:unit": "node scripts/unit-tests.mjs",
"test": "npm run test:unit && npm run test:cli-smoke"
"test:unit": "npm run build && node scripts/unit-tests.mjs && npm run test:template",
"test": "npm run test:unit && npm run test:cli-smoke",
"test:template": "node scripts/template-tests.mjs"
},
"dependencies": {
"commander": "^12.1.0",
Expand Down
16 changes: 16 additions & 0 deletions typescript-sdk/scripts/template-tests.mjs
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);
}
}
16 changes: 16 additions & 0 deletions typescript-sdk/tests/canonicalization.test.mjs
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"));
});
Loading