Skip to content

Commit 4ff3a8e

Browse files
authored
Merge pull request #217 from commandlayer/claude/audit-protocol-infrastructure-vF6oo
Support non-legacy receipt signature verification
2 parents e913786 + 99ded15 commit 4ff3a8e

4 files changed

Lines changed: 59 additions & 15 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
RUNTIME_BASE_URL=https://runtime.commandlayer.org

.github/workflows/ci.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: CI
2+
on:
3+
push:
4+
branches: [main, "claude/**"]
5+
pull_request:
6+
branches: [main]
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
- uses: actions/setup-node@v4
13+
with:
14+
node-version: "20"
15+
- run: npm install
16+
- run: npm test

api/_receipt-model.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,10 +243,10 @@ function validateRuntimeMetadata(runtimeMetadata, options = {}) {
243243
if (!proof) {
244244
errors.push({ message: 'runtime_metadata.proof is required.' });
245245
} else {
246-
if (proof.alg !== 'ed25519-sha256') {
247-
errors.push({ message: `runtime_metadata.proof.alg must be ed25519-sha256 (got ${proof.alg || 'missing'}).` });
246+
if (proof.alg !== 'ed25519') {
247+
errors.push({ message: `runtime_metadata.proof.alg must be ed25519 (got ${proof.alg || 'missing'}).` });
248248
}
249-
const canonicalId = proof.canonical || proof.canonical_id || null;
249+
const canonicalId = proof.canonical || proof.canonicalization || null;
250250
if (canonicalId !== CANONICAL_PROOF_ID) {
251251
errors.push({ message: `runtime_metadata.proof canonical id must be ${CANONICAL_PROOF_ID} (got ${canonicalId || 'missing'}).` });
252252
}

lib/verifyReceipt.js

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ async function verifyHashHexSignature(hashHex, signatureBase64, publicKey) {
6060
);
6161
}
6262

63+
async function verifyCanonicalSignature(canonicalStr, signatureBase64, publicKey) {
64+
return subtle.verify(
65+
{ name: 'Ed25519' },
66+
publicKey,
67+
base64ToBytes(signatureBase64),
68+
new TextEncoder().encode(canonicalStr),
69+
);
70+
}
71+
6372
async function defaultTextResolver() {
6473
return null;
6574
}
@@ -135,44 +144,56 @@ async function verifyReceipt(receiptInput, options = {}) {
135144
};
136145
}
137146

147+
const proof = receipt?.metadata?.proof || null;
148+
const canonicalization = proof?.canonical || proof?.canonicalization || null;
149+
const kid = receipt?.signature?.kid || proof?.kid || null;
150+
const sig = receipt?.signature?.sig || proof?.signature || null;
151+
138152
const schemaValid = Boolean(
139153
receipt &&
140154
typeof receipt === 'object' &&
141155
typeof receipt.signer === 'string' &&
142156
typeof receipt.verb === 'string' &&
143157
typeof receipt.ts === 'string' &&
144-
receipt.metadata?.proof?.canonicalization &&
145-
receipt.metadata?.proof?.hash_sha256 &&
146-
receipt.signature?.kid &&
147-
receipt.signature?.sig,
158+
canonicalization &&
159+
kid &&
160+
sig,
148161
);
149162

150163
const ens = await resolveSignerFromEns(receipt?.signer, options.ens || {});
151-
const expectedHash = receipt?.metadata?.proof?.hash_sha256 || null;
152-
const canonicalization = receipt?.metadata?.proof?.canonicalization || null;
164+
const expectedHash = proof?.hash_sha256 || null;
165+
const isLegacyMode = Boolean(expectedHash);
153166
const canonicalPayload = canonicalReceiptPayload(receipt);
154-
const recomputedHash = await sha256Hex(canonicalize(canonicalPayload));
167+
const canonicalStr = canonicalize(canonicalPayload);
168+
const recomputedHash = await sha256Hex(canonicalStr);
155169

156170
const expectedCanonical = ens.records['cl.sig.canonical'];
157171
const canonicalizationOk = canonicalization === expectedCanonical;
158172
const hashMatched = Boolean(
159173
schemaValid &&
160174
canonicalizationOk &&
175+
isLegacyMode &&
161176
typeof expectedHash === 'string' &&
162177
expectedHash === recomputedHash,
163178
);
164179

165-
const keyIdMatched = receipt?.signature?.kid === ens.records['cl.sig.kid'];
180+
const keyIdMatched = kid === ens.records['cl.sig.kid'];
166181
const prefixedPubkey = ens.records['cl.sig.pub'];
167182
const pubkeyBase64 = typeof prefixedPubkey === 'string'
168183
? prefixedPubkey.replace(/^ed25519:/, '')
169184
: null;
170185

171186
let signatureValid = false;
172-
if (hashMatched && keyIdMatched && pubkeyBase64 && receipt?.signature?.sig) {
187+
if (keyIdMatched && pubkeyBase64 && sig) {
173188
try {
174189
const publicKey = await importEd25519PublicKey(pubkeyBase64);
175-
signatureValid = await verifyHashHexSignature(recomputedHash, receipt.signature.sig, publicKey);
190+
if (isLegacyMode) {
191+
if (hashMatched) {
192+
signatureValid = await verifyHashHexSignature(recomputedHash, sig, publicKey);
193+
}
194+
} else {
195+
signatureValid = await verifyCanonicalSignature(canonicalStr, sig, publicKey);
196+
}
176197
} catch {
177198
signatureValid = false;
178199
}
@@ -182,7 +203,13 @@ async function verifyReceipt(receiptInput, options = {}) {
182203
ens.records['cl.receipt.signer'] && receipt?.signer === ens.records['cl.receipt.signer'],
183204
);
184205

185-
const ok = Boolean(schemaValid && hashMatched && signatureValid && signerMatched && ens.ensResolved);
206+
const ok = Boolean(
207+
schemaValid &&
208+
signatureValid &&
209+
signerMatched &&
210+
ens.ensResolved &&
211+
(isLegacyMode ? hashMatched : true),
212+
);
186213

187214
return {
188215
ok,
@@ -194,7 +221,7 @@ async function verifyReceipt(receiptInput, options = {}) {
194221
hash_matches: hashMatched,
195222
signature_valid: signatureValid,
196223
ens_resolved: Boolean(ens.ensResolved),
197-
key_id: receipt?.signature?.kid || null,
224+
key_id: kid || null,
198225
public_key_source: ens.keySource,
199226
debug: {
200227
expected_hash_sha256: expectedHash,

0 commit comments

Comments
 (0)