83 lines
4.0 KiB
JavaScript
83 lines
4.0 KiB
JavaScript
import { createHash, createPublicKey, sign, verify } from 'node:crypto';
|
|
|
|
const domain = Buffer.from('portfolio-drive-trace-result/v1\0', 'utf8');
|
|
|
|
const canonicalize = (value) => {
|
|
if (value === null || typeof value === 'boolean' || typeof value === 'string') return JSON.stringify(value);
|
|
if (typeof value === 'number') {
|
|
if (!Number.isFinite(value)) throw new Error('Reference evidence cannot contain non-finite numbers.');
|
|
return JSON.stringify(value);
|
|
}
|
|
if (Array.isArray(value)) return `[${value.map(canonicalize).join(',')}]`;
|
|
if (typeof value === 'object') {
|
|
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right, 'en-US'));
|
|
if (entries.some(([, item]) => item === undefined)) throw new Error('Reference evidence cannot contain undefined values.');
|
|
return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${canonicalize(item)}`).join(',')}}`;
|
|
}
|
|
throw new Error('Reference evidence contains an unsupported value.');
|
|
};
|
|
|
|
const signingPayload = (run) => {
|
|
if (run === null || typeof run !== 'object' || Array.isArray(run)) throw new Error('Reference result must be an object.');
|
|
const { referenceProof: _proof, ...payload } = run;
|
|
return payload;
|
|
};
|
|
|
|
const encodedPayload = (payload) => Buffer.concat([domain, Buffer.from(canonicalize(payload), 'utf8')]);
|
|
|
|
export const signReferenceCandidate = (candidate, signer) => {
|
|
if (candidate?.schema !== 'drive-trace.v1' || candidate.evidenceKind !== 'validated-reference-candidate-unsigned'
|
|
|| candidate.gatingReferenceResult !== false || candidate.referenceCandidateValidated !== true
|
|
|| candidate.referenceBudgetsPass !== true || candidate.validReferenceCadence !== true
|
|
|| !Object.values(candidate.budgetResults ?? {}).every((value) => value === true)) {
|
|
throw new Error('Only a fully validated unsigned reference candidate can be signed.');
|
|
}
|
|
if (typeof signer?.keyId !== 'string' || !/^[a-z0-9][a-z0-9-]{2,79}$/u.test(signer.keyId) || signer.privateKey === undefined) {
|
|
throw new Error('An external Ed25519 signing identity is required.');
|
|
}
|
|
const payload = {
|
|
...candidate,
|
|
evidenceKind: 'signed-physical-reference',
|
|
gatingReferenceResult: true,
|
|
};
|
|
const bytes = encodedPayload(payload);
|
|
const resultDigest = createHash('sha256').update(bytes).digest('hex');
|
|
const signature = sign(null, bytes, signer.privateKey).toString('base64url');
|
|
return {
|
|
...payload,
|
|
referenceProof: {
|
|
schema: 'portfolio-reference-result-proof/v1',
|
|
algorithm: 'Ed25519',
|
|
encoding: 'base64url-nopad',
|
|
keyId: signer.keyId,
|
|
resultDigest,
|
|
signature,
|
|
},
|
|
};
|
|
};
|
|
|
|
export const verifySignedReferenceRun = (run, policy) => {
|
|
if (policy?.schema !== 'portfolio-reference-trust-policy/v1'
|
|
|| typeof policy.keyId !== 'string' || typeof policy.publicKeyPem !== 'string') {
|
|
throw new Error('An explicit trusted reference public-key policy is required.');
|
|
}
|
|
const proof = run?.referenceProof;
|
|
if (run?.schema !== 'drive-trace.v1' || run.evidenceKind !== 'signed-physical-reference'
|
|
|| run.gatingReferenceResult !== true || proof?.schema !== 'portfolio-reference-result-proof/v1'
|
|
|| proof.algorithm !== 'Ed25519' || proof.encoding !== 'base64url-nopad' || proof.keyId !== policy.keyId
|
|
|| !/^[a-f0-9]{64}$/u.test(proof.resultDigest ?? '') || !/^[A-Za-z0-9_-]+$/u.test(proof.signature ?? '')) {
|
|
throw new Error('Reference result signature metadata is invalid.');
|
|
}
|
|
const payload = signingPayload(run);
|
|
const bytes = encodedPayload(payload);
|
|
const digest = createHash('sha256').update(bytes).digest('hex');
|
|
if (digest !== proof.resultDigest) throw new Error('Reference result digest mismatch.');
|
|
let publicKey;
|
|
try { publicKey = createPublicKey(policy.publicKeyPem); } catch { throw new Error('Trusted reference public key is invalid.'); }
|
|
if (publicKey.asymmetricKeyType !== 'ed25519'
|
|
|| !verify(null, bytes, publicKey, Buffer.from(proof.signature, 'base64url'))) {
|
|
throw new Error('Reference result signature is not trusted.');
|
|
}
|
|
return payload;
|
|
};
|