Skip to content

Commit 6d6545c

Browse files
committed
Make receipt verification ENS-first with explicit local fallback
1 parent 4f72730 commit 6d6545c

6 files changed

Lines changed: 52 additions & 6 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ Tampered receipt:
6565
- `hash_matches = false`
6666
- `signature_valid = false`
6767

68+
69+
## Key resolution policy
70+
71+
- Production verification resolves signer keys from ENS TXT records: `cl.sig.pub`, `cl.sig.kid`, `cl.sig.canonical`, and `cl.receipt.signer`.
72+
- Local key fallback is test/demo only and must be explicitly enabled with `COMMANDLAYER_ALLOW_LOCAL_KEY_FALLBACK=true` (or test mode).
73+
- Verifier responses expose `public_key_source` as `ens_txt` or `local_test_fallback`.
74+
6875
## Trust boundaries
6976

7077
- Runtime signs.

api/agents/verifyagent.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ module.exports = async function handler(req, res) {
8787
signature_valid: verification.signature_valid,
8888
ens_resolved: verification.ens_resolved,
8989
key_id: verification.key_id,
90+
public_key_source: verification.public_key_source,
9091
},
9192
});
9293
} catch (error) {

lib/verifyReceipt.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ async function defaultTextResolver() {
7575

7676
async function resolveSignerFromEns(signerEnsName, options = {}) {
7777
const resolver = options.textResolver || defaultTextResolver;
78+
const allowLocalFallback = options.allowLocalFallback === true || process.env.COMMANDLAYER_ALLOW_LOCAL_KEY_FALLBACK === 'true' || process.env.NODE_ENV === 'test';
7879
const requiredKeys = ['cl.sig.pub', 'cl.sig.kid', 'cl.sig.canonical', 'cl.receipt.signer'];
7980
const records = {};
8081

@@ -98,24 +99,27 @@ async function resolveSignerFromEns(signerEnsName, options = {}) {
9899
signer: signerEnsName,
99100
records,
100101
ensResolved: true,
101-
keySource: 'live ENS text record',
102+
keySource: 'ens_txt',
103+
errorCode: null,
102104
};
103105
}
104106

105-
if (signerEnsName === FALLBACK_SIGNER) {
107+
if (allowLocalFallback && signerEnsName === FALLBACK_SIGNER) {
106108
return {
107109
signer: signerEnsName,
108110
records: { ...FALLBACK_RECORDS },
109111
ensResolved: true,
110-
keySource: 'local demo fallback (runtime.commandlayer.eth only)',
112+
keySource: 'local_test_fallback',
113+
errorCode: null,
111114
};
112115
}
113116

114117
return {
115118
signer: signerEnsName || 'unknown',
116119
records: {},
117120
ensResolved: false,
118-
keySource: 'not resolved',
121+
keySource: 'ens_txt',
122+
errorCode: 'ens_key_unavailable',
119123
};
120124
}
121125

@@ -215,7 +219,9 @@ async function verifyReceipt(receiptInput, options = {}) {
215219
return {
216220
ok,
217221
status: ok ? 'VERIFIED' : 'INVALID',
218-
reason: ok ? 'Receipt verification passed.' : 'Receipt is invalid, tampered, or does not match the signer key metadata.',
222+
reason: ok
223+
? 'Receipt verification passed.'
224+
: (ens.errorCode === 'ens_key_unavailable' ? 'ens_key_unavailable' : 'Receipt is invalid, tampered, or does not match the signer key metadata.'),
219225
signer: receipt?.signer || null,
220226
verb: receipt?.verb || null,
221227
hash: recomputedHash,
@@ -232,6 +238,7 @@ async function verifyReceipt(receiptInput, options = {}) {
232238
key_id_matched: keyIdMatched,
233239
canonicalization_matched: canonicalizationOk,
234240
signer_matched: signerMatched,
241+
key_resolution_error: ens.errorCode,
235242
},
236243
};
237244
}

tests/api-agents-verifyagent.test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ test('POST /api/agents/verifyagent with canonical sample fixture => INVALID', as
3333
assert.equal(res.body.action, 'verify_receipt');
3434
assert.equal(res.body.ok, false);
3535
assert.equal(res.body.status, 'INVALID');
36-
assert.equal(res.body.result.reason, 'Receipt is invalid, tampered, or does not match the signer key metadata.');
36+
assert.equal(res.body.result.reason, 'ens_key_unavailable');
37+
assert.equal(typeof res.body.result.public_key_source, 'string');
3738
});
3839

3940
test('POST /api/agents/verifyagent with tampered receipt => INVALID', async () => {

tests/api-verify.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ test('POST /api/verify with canonical sample fixture => INVALID', async () => {
3131
assert.equal(res.statusCode, 200);
3232
assert.equal(res.body.ok, false);
3333
assert.equal(res.body.status, 'INVALID');
34+
assert.equal(typeof res.body.public_key_source, 'string');
3435
});
3536

3637

tests/verifyReceipt-runtime.test.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,35 @@ test('valid runtime-style receipt verifies', async () => {
8383
const { receipt, rawPub } = await makeRuntimeReceipt();
8484
const out = await verifyReceipt(receipt, { ens: { textResolver: makeTextResolver(rawPub) } });
8585
assert.equal(out.status, 'VERIFIED');
86+
assert.equal(out.public_key_source, 'ens_txt');
87+
});
88+
89+
test('fails when ENS key is unavailable and fallback is disabled', async () => {
90+
const { receipt } = await makeRuntimeReceipt();
91+
const out = await verifyReceipt(receipt, {
92+
ens: {
93+
textResolver: async () => null,
94+
allowLocalFallback: false,
95+
},
96+
});
97+
98+
assert.equal(out.status, 'INVALID');
99+
assert.equal(out.reason, 'ens_key_unavailable');
100+
assert.equal(out.public_key_source, 'ens_txt');
101+
});
102+
103+
test('allows explicit local fallback for test/demo mode only when enabled', async () => {
104+
const { receipt } = await makeRuntimeReceipt();
105+
const out = await verifyReceipt(receipt, {
106+
ens: {
107+
textResolver: async () => null,
108+
allowLocalFallback: true,
109+
},
110+
});
111+
112+
assert.equal(out.status, 'INVALID');
113+
assert.equal(out.public_key_source, 'local_test_fallback');
114+
assert.equal(out.reason, 'Receipt is invalid, tampered, or does not match the signer key metadata.');
86115
});
87116

88117
test('tampered receipt invalidates', async () => {

0 commit comments

Comments
 (0)