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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ Tampered receipt:
- `hash_matches = false`
- `signature_valid = false`


## Key resolution policy

- Production verification resolves signer keys from ENS TXT records: `cl.sig.pub`, `cl.sig.kid`, `cl.sig.canonical`, and `cl.receipt.signer`.
- Local key fallback is test/demo only and must be explicitly enabled with `COMMANDLAYER_ALLOW_LOCAL_KEY_FALLBACK=true` (or test mode).
- Verifier responses expose `public_key_source` as `ens_txt` or `local_test_fallback`.

## Trust boundaries

- Runtime signs.
Expand Down
1 change: 1 addition & 0 deletions api/agents/verifyagent.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ module.exports = async function handler(req, res) {
signature_valid: verification.signature_valid,
ens_resolved: verification.ens_resolved,
key_id: verification.key_id,
public_key_source: verification.public_key_source,
},
});
} catch (error) {
Expand Down
17 changes: 12 additions & 5 deletions lib/verifyReceipt.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ async function defaultTextResolver() {

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

Expand All @@ -98,24 +99,27 @@ async function resolveSignerFromEns(signerEnsName, options = {}) {
signer: signerEnsName,
records,
ensResolved: true,
keySource: 'live ENS text record',
keySource: 'ens_txt',
errorCode: null,
};
}

if (signerEnsName === FALLBACK_SIGNER) {
if (allowLocalFallback && signerEnsName === FALLBACK_SIGNER) {
return {
signer: signerEnsName,
records: { ...FALLBACK_RECORDS },
ensResolved: true,
keySource: 'local demo fallback (runtime.commandlayer.eth only)',
keySource: 'local_test_fallback',
errorCode: null,
};
}

return {
signer: signerEnsName || 'unknown',
records: {},
ensResolved: false,
keySource: 'not resolved',
keySource: 'ens_txt',
errorCode: 'ens_key_unavailable',
};
}

Expand Down Expand Up @@ -215,7 +219,9 @@ async function verifyReceipt(receiptInput, options = {}) {
return {
ok,
status: ok ? 'VERIFIED' : 'INVALID',
reason: ok ? 'Receipt verification passed.' : 'Receipt is invalid, tampered, or does not match the signer key metadata.',
reason: ok
? 'Receipt verification passed.'
: (ens.errorCode === 'ens_key_unavailable' ? 'ens_key_unavailable' : 'Receipt is invalid, tampered, or does not match the signer key metadata.'),
signer: receipt?.signer || null,
verb: receipt?.verb || null,
hash: recomputedHash,
Expand All @@ -232,6 +238,7 @@ async function verifyReceipt(receiptInput, options = {}) {
key_id_matched: keyIdMatched,
canonicalization_matched: canonicalizationOk,
signer_matched: signerMatched,
key_resolution_error: ens.errorCode,
},
};
}
Expand Down
3 changes: 2 additions & 1 deletion tests/api-agents-verifyagent.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ test('POST /api/agents/verifyagent with canonical sample fixture => INVALID', as
assert.equal(res.body.action, 'verify_receipt');
assert.equal(res.body.ok, false);
assert.equal(res.body.status, 'INVALID');
assert.equal(res.body.result.reason, 'Receipt is invalid, tampered, or does not match the signer key metadata.');
assert.equal(res.body.result.reason, 'ens_key_unavailable');
assert.equal(typeof res.body.result.public_key_source, 'string');
});

test('POST /api/agents/verifyagent with tampered receipt => INVALID', async () => {
Expand Down
1 change: 1 addition & 0 deletions tests/api-verify.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ test('POST /api/verify with canonical sample fixture => INVALID', async () => {
assert.equal(res.statusCode, 200);
assert.equal(res.body.ok, false);
assert.equal(res.body.status, 'INVALID');
assert.equal(typeof res.body.public_key_source, 'string');
});


Expand Down
29 changes: 29 additions & 0 deletions tests/verifyReceipt-runtime.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,35 @@ test('valid runtime-style receipt verifies', async () => {
const { receipt, rawPub } = await makeRuntimeReceipt();
const out = await verifyReceipt(receipt, { ens: { textResolver: makeTextResolver(rawPub) } });
assert.equal(out.status, 'VERIFIED');
assert.equal(out.public_key_source, 'ens_txt');
});

test('fails when ENS key is unavailable and fallback is disabled', async () => {
const { receipt } = await makeRuntimeReceipt();
const out = await verifyReceipt(receipt, {
ens: {
textResolver: async () => null,
allowLocalFallback: false,
},
});

assert.equal(out.status, 'INVALID');
assert.equal(out.reason, 'ens_key_unavailable');
assert.equal(out.public_key_source, 'ens_txt');
});

test('allows explicit local fallback for test/demo mode only when enabled', async () => {
const { receipt } = await makeRuntimeReceipt();
const out = await verifyReceipt(receipt, {
ens: {
textResolver: async () => null,
allowLocalFallback: true,
},
});

assert.equal(out.status, 'INVALID');
assert.equal(out.public_key_source, 'local_test_fallback');
assert.equal(out.reason, 'Receipt is invalid, tampered, or does not match the signer key metadata.');
});

test('tampered receipt invalidates', async () => {
Expand Down
Loading