Skip to content

Commit 2cdf311

Browse files
authored
Merge pull request #7 from commandlayer/codex/update-ens-txt-parsing-and-verification-logic
Resolve ENS signer via cl.receipt.signer and cl.sig.pub/kid; add resolveSignerKey and update verification
2 parents d910454 + ab8219b commit 2cdf311

File tree

3 files changed

+149
-52
lines changed

3 files changed

+149
-52
lines changed

typescript-sdk/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,21 +127,21 @@ console.log(result.ok);
127127

128128
**Option B — ENS-based Verification**
129129

130-
Resolves the public key from ENS TXT records.
130+
Resolves signer metadata from ENS TXT records.
131131

132132
Required ENS records:
133133

134-
- `cl.receipt.pubkey_pem`
135-
- `cl.receipt.signer_id`
136-
- `cl.receipt.alg`
137-
-
134+
- Agent ENS TXT: `cl.receipt.signer`
135+
- Signer ENS TXT: `cl.sig.pub`
136+
- Signer ENS TXT: `cl.sig.kid`
137+
138138
Example:
139139
```
140140
import { verifyReceipt } from "@commandlayer/sdk";
141141
142142
const out = await verifyReceipt(receipt, {
143143
ens: {
144-
name: "runtime.commandlayer.eth",
144+
name: "summarizeagent.eth",
145145
rpcUrl: process.env.ETH_RPC_URL!
146146
}
147147
});

typescript-sdk/scripts/unit-tests.mjs

Lines changed: 94 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,7 @@
55
import { createRequire } from "node:module";
66
const require = createRequire(import.meta.url);
77

8-
const {
9-
canonicalizeStableJsonV1,
10-
sha256HexUtf8,
11-
parseEd25519Pubkey,
12-
verifyEd25519SignatureOverUtf8HashString,
13-
recomputeReceiptHashSha256,
14-
verifyReceipt,
15-
CommandLayerError,
16-
CommandLayerClient,
17-
} = require("../dist/index.cjs");
18-
8+
const ethers = require("ethers");
199
const nacl = require("tweetnacl");
2010

2111
let passed = 0;
@@ -42,6 +32,64 @@ function assertThrows(fn, name) {
4232
}
4333
}
4434

35+
async function assertRejects(fn, expected, name) {
36+
try {
37+
await fn();
38+
failed++;
39+
console.error(`FAIL: ${name} (did not throw)`);
40+
} catch (err) {
41+
const msg = err?.message || String(err);
42+
if (!msg.includes(expected)) {
43+
failed++;
44+
console.error(`FAIL: ${name} (unexpected message: ${msg})`);
45+
return;
46+
}
47+
passed++;
48+
console.log(`PASS: ${name}`);
49+
}
50+
}
51+
52+
const kp = nacl.sign.keyPair();
53+
const b64Key = Buffer.from(kp.publicKey).toString("base64");
54+
const hexKey = Buffer.from(kp.publicKey).toString("hex");
55+
56+
const ensFixtures = {
57+
"summarizeagent.eth": { "cl.receipt.signer": "runtime.commandlayer.eth" },
58+
"runtime.commandlayer.eth": { "cl.sig.pub": `ed25519:${b64Key}`, "cl.sig.kid": "2026-01" },
59+
"missing-signer.eth": {},
60+
"missing-pub.eth": { "cl.receipt.signer": "signer-without-pub.eth" },
61+
"signer-without-pub.eth": { "cl.sig.kid": "2026-01" },
62+
"malformed-pub.eth": { "cl.receipt.signer": "signer-with-malformed-pub.eth" },
63+
"signer-with-malformed-pub.eth": { "cl.sig.pub": "ed25519:not-base64", "cl.sig.kid": "2026-01" },
64+
};
65+
66+
class MockResolver {
67+
constructor(name) {
68+
this.name = name;
69+
}
70+
71+
async getText(key) {
72+
return ensFixtures[this.name]?.[key] ?? "";
73+
}
74+
}
75+
76+
ethers.ethers.JsonRpcProvider.prototype.getResolver = async function(name) {
77+
if (!(name in ensFixtures)) return null;
78+
return new MockResolver(name);
79+
};
80+
81+
const {
82+
canonicalizeStableJsonV1,
83+
sha256HexUtf8,
84+
parseEd25519Pubkey,
85+
verifyEd25519SignatureOverUtf8HashString,
86+
recomputeReceiptHashSha256,
87+
verifyReceipt,
88+
resolveSignerKey,
89+
CommandLayerError,
90+
CommandLayerClient,
91+
} = require("../dist/index.cjs");
92+
4593
// ---- Canonicalization ----
4694

4795
assert(canonicalizeStableJsonV1(null) === "null", "canonicalize null");
@@ -84,10 +132,6 @@ assert(sha256HexUtf8("hello") !== sha256HexUtf8("world"), "sha256 differs for di
84132

85133
// ---- Ed25519 pubkey parsing ----
86134

87-
const kp = nacl.sign.keyPair();
88-
const b64Key = Buffer.from(kp.publicKey).toString("base64");
89-
const hexKey = Buffer.from(kp.publicKey).toString("hex");
90-
91135
const pk1 = parseEd25519Pubkey(b64Key);
92136
assert(pk1.length === 32, "parse base64 pubkey");
93137

@@ -123,6 +167,31 @@ assert(
123167
"wrong key rejects"
124168
);
125169

170+
// ---- ENS signer key resolution ----
171+
172+
const signerKey = await resolveSignerKey("summarizeagent.eth", "http://mock-rpc.local");
173+
assert(signerKey.algorithm === "ed25519", "resolveSignerKey returns algorithm");
174+
assert(signerKey.kid === "2026-01", "resolveSignerKey returns kid from cl.sig.kid");
175+
assert(Buffer.from(signerKey.rawPublicKeyBytes).toString("base64") === b64Key, "resolveSignerKey returns public key bytes from cl.sig.pub");
176+
177+
await assertRejects(
178+
() => resolveSignerKey("missing-signer.eth", "http://mock-rpc.local"),
179+
"ENS TXT cl.receipt.signer missing",
180+
"resolveSignerKey throws clear error when cl.receipt.signer missing"
181+
);
182+
183+
await assertRejects(
184+
() => resolveSignerKey("missing-pub.eth", "http://mock-rpc.local"),
185+
"ENS TXT cl.sig.pub missing",
186+
"resolveSignerKey throws clear error when cl.sig.pub missing"
187+
);
188+
189+
await assertRejects(
190+
() => resolveSignerKey("malformed-pub.eth", "http://mock-rpc.local"),
191+
"ENS TXT cl.sig.pub malformed",
192+
"resolveSignerKey throws clear error when cl.sig.pub malformed"
193+
);
194+
126195
// ---- Receipt verification (end-to-end) ----
127196

128197
const receipt = {
@@ -147,11 +216,20 @@ receipt.metadata.proof.signature_b64 = Buffer.from(receiptSig).toString("base64"
147216
receipt.metadata.receipt_id = hash_sha256;
148217

149218
const vr = await verifyReceipt(receipt, { publicKey: `ed25519:${b64Key}` });
150-
assert(vr.ok === true, "verifyReceipt ok for valid receipt");
219+
assert(vr.ok === true, "verifyReceipt ok for valid receipt (explicit key)");
151220
assert(vr.checks.hash_matches === true, "verifyReceipt hash matches");
152221
assert(vr.checks.signature_valid === true, "verifyReceipt signature valid");
153222
assert(vr.checks.receipt_id_matches === true, "verifyReceipt receipt_id matches");
154223

224+
const vrEns = await verifyReceipt(receipt, {
225+
ens: {
226+
name: "summarizeagent.eth",
227+
rpcUrl: "http://mock-rpc.local"
228+
}
229+
});
230+
assert(vrEns.ok === true, "verifyReceipt ok with ENS cl.receipt.signer + cl.sig.pub");
231+
assert(vrEns.values.pubkey_source === "ens", "verifyReceipt reports ENS key source");
232+
155233
// Tampered receipt
156234
const tamperedReceipt = JSON.parse(JSON.stringify(receipt));
157235
tamperedReceipt.result.summary = "tampered";

typescript-sdk/src/index.ts

Lines changed: 49 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -108,19 +108,16 @@ export class CommandLayerError extends Error {
108108
}
109109

110110
export type EnsVerifyOptions = {
111-
/** ENS name that holds TXT records (commonly runtime.commandlayer.eth) */
111+
/** Agent ENS name that holds TXT records (e.g. summarizeagent.eth) */
112112
name: string;
113113
/** Ethereum RPC URL (required for ENS resolution) */
114114
rpcUrl: string;
115-
/**
116-
* TXT record key that contains an Ed25519 public key (32 bytes).
117-
* Accepts formats:
118-
* - "ed25519:<base64>"
119-
* - "<base64>" (32 bytes)
120-
* - "0x<hex>" / "<hex>" (64 hex chars)
121-
* Default: "cl.pubkey"
122-
*/
123-
pubkeyTextKey?: string;
115+
};
116+
117+
export type SignerKeyResolution = {
118+
algorithm: "ed25519";
119+
kid: string;
120+
rawPublicKeyBytes: Uint8Array;
124121
};
125122

126123
export type VerifyOptions = {
@@ -290,25 +287,47 @@ export function verifyEd25519SignatureOverUtf8HashString(
290287
}
291288

292289
// -----------------------
293-
// ENS TXT pubkey resolution (ethers v6)
290+
// ENS TXT signer key resolution (ethers v6)
294291
// -----------------------
295-
export async function resolveEnsEd25519Pubkey(
296-
ens: EnsVerifyOptions
297-
): Promise<{ pubkey: Uint8Array | null; source: "ens" | null; error?: string; txtKey: string; txtValue?: string }> {
298-
const txtKey = ens.pubkeyTextKey || "cl.pubkey";
299-
try {
300-
const provider = new ethers.JsonRpcProvider(ens.rpcUrl);
301-
const resolver = await provider.getResolver(ens.name);
302-
if (!resolver) return { pubkey: null, source: null, error: "No resolver for ENS name", txtKey };
292+
export async function resolveSignerKey(name: string, rpcUrl: string): Promise<SignerKeyResolution> {
293+
const provider = new ethers.JsonRpcProvider(rpcUrl);
294+
const agentResolver = await provider.getResolver(name);
295+
if (!agentResolver) {
296+
throw new Error(`No resolver for agent ENS name: ${name}`);
297+
}
298+
299+
const signerName = (await agentResolver.getText("cl.receipt.signer"))?.trim();
300+
if (!signerName) {
301+
throw new Error(`ENS TXT cl.receipt.signer missing for agent ENS name: ${name}`);
302+
}
303+
304+
const signerResolver = await provider.getResolver(signerName);
305+
if (!signerResolver) {
306+
throw new Error(`No resolver for signer ENS name: ${signerName}`);
307+
}
303308

304-
const txt = (await resolver.getText(txtKey))?.trim();
305-
if (!txt) return { pubkey: null, source: null, error: `ENS TXT ${txtKey} missing`, txtKey };
309+
const pubKeyText = (await signerResolver.getText("cl.sig.pub"))?.trim();
310+
if (!pubKeyText) {
311+
throw new Error(`ENS TXT cl.sig.pub missing for signer ENS name: ${signerName}`);
312+
}
313+
314+
const kid = (await signerResolver.getText("cl.sig.kid"))?.trim();
315+
if (!kid) {
316+
throw new Error(`ENS TXT cl.sig.kid missing for signer ENS name: ${signerName}`);
317+
}
306318

307-
const pubkey = parseEd25519Pubkey(txt);
308-
return { pubkey, source: "ens", txtKey, txtValue: txt };
319+
let rawPublicKeyBytes: Uint8Array;
320+
try {
321+
rawPublicKeyBytes = parseEd25519Pubkey(pubKeyText);
309322
} catch (e: any) {
310-
return { pubkey: null, source: null, error: e?.message || "ENS resolution failed", txtKey };
323+
throw new Error(`ENS TXT cl.sig.pub malformed for signer ENS name: ${signerName}. ${e?.message || String(e)}`);
311324
}
325+
326+
return {
327+
algorithm: "ed25519",
328+
kid,
329+
rawPublicKeyBytes
330+
};
312331
}
313332

314333
// -----------------------
@@ -372,13 +391,13 @@ export async function verifyReceipt(receipt: Receipt, opts: VerifyOptions = {}):
372391
pubkey = parseEd25519Pubkey(opts.publicKey);
373392
pubkey_source = "explicit";
374393
} else if (opts.ens) {
375-
const res = await resolveEnsEd25519Pubkey(opts.ens);
376-
ens_txt_key = res.txtKey;
377-
if (!res.pubkey) {
378-
ens_error = res.error || "ENS pubkey not found";
379-
} else {
380-
pubkey = res.pubkey;
394+
ens_txt_key = "cl.receipt.signer -> cl.sig.pub, cl.sig.kid";
395+
try {
396+
const signerKey = await resolveSignerKey(opts.ens.name, opts.ens.rpcUrl);
397+
pubkey = signerKey.rawPublicKeyBytes;
381398
pubkey_source = "ens";
399+
} catch (e: any) {
400+
ens_error = e?.message || "ENS signer key resolution failed";
382401
}
383402
}
384403

0 commit comments

Comments
 (0)