feat: verify canonical portfolio release provenance

This commit is contained in:
2026-09-02 09:41:01 +03:00
parent 1cec658baf
commit 672ba80d9b
12 changed files with 3536 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
#!/usr/bin/env node
import { createPublicKey } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';
import { computeApprovalClosure } from '../../src/security/approval-closure.ts';
import { canonicalValueBytes, isSha256Hex, parseCanonicalJson } from '../../src/security/canonical-digest.ts';
import { computeProductionInputRoot } from '../../src/security/production-input-root.ts';
const expectedRequestFields = [
'schema', 'now', 'latestLedgerHeadHash', 'latestLedgerRevision', 'trustedKeys', 'contentRecords', 'assetRecords', 'inputDigests',
].sort();
const expectedKeyFields = ['keyId', 'issuerEpoch', 'scope', 'authorizedRoles', 'publicKeyPem'].sort();
const expectedInputs = ['PortfolioContent', 'PreparedAssetSet', 'PublicAssetManifest', 'SiteConfig'];
function exactObject(value, fields, label) {
if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must be an object.`);
if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(fields)) throw new Error(`${label} contains missing or unknown fields.`);
return value;
}
function requestPath(args) {
if (args.length !== 2 || args[0] !== '--request' || args[1].length === 0) throw new Error('Usage: verify-approvals.mjs --request <canonical-request.json>');
return args[1];
}
export function verifyApprovalRequest(source) {
const request = exactObject(parseCanonicalJson(source), expectedRequestFields, 'request');
if (request.schema !== 'portfolio-approval-verification-request/v1') throw new Error('Request schema is invalid.');
if (!Array.isArray(request.trustedKeys) || !Array.isArray(request.contentRecords) || !Array.isArray(request.assetRecords)) throw new Error('Approval record lists are invalid.');
const trustedKeys = request.trustedKeys.map((candidate) => {
const key = exactObject(candidate, expectedKeyFields, 'trusted key');
if (key.scope !== 'portfolio-approval-decision/v1' || typeof key.keyId !== 'string' ||
!Number.isSafeInteger(key.issuerEpoch) || !Array.isArray(key.authorizedRoles) ||
key.authorizedRoles.some((role) => typeof role !== 'string') || typeof key.publicKeyPem !== 'string') {
throw new Error('Trusted approval key is invalid.');
}
return {
keyId: key.keyId, issuerEpoch: key.issuerEpoch, scope: key.scope,
authorizedRoles: key.authorizedRoles, publicKey: createPublicKey(key.publicKeyPem),
};
});
const inputDigests = exactObject(request.inputDigests, expectedInputs, 'inputDigests');
if (Object.values(inputDigests).some((digest) => !isSha256Hex(digest))) throw new Error('Input digest is invalid.');
const closure = computeApprovalClosure({
contentRecords: request.contentRecords,
assetRecords: request.assetRecords,
trust: {
now: request.now,
latestLedgerHeadHash: request.latestLedgerHeadHash,
latestLedgerRevision: request.latestLedgerRevision,
trustedKeys,
},
});
const productionInputRoot = computeProductionInputRoot({
portfolioContentDigest: inputDigests.PortfolioContent,
publicAssetManifestDigest: inputDigests.PublicAssetManifest,
preparedAssetSetDigest: inputDigests.PreparedAssetSet,
siteConfigDigest: inputDigests.SiteConfig,
});
return {
schema: 'portfolio-approval-verification-result/v1',
approvedRecordRoot: closure.approvedRecordRoot,
approvedAssetRoot: closure.approvedAssetRoot,
approvalProofSetDigest: closure.approvalProofSetDigest,
productionInputRoot,
latestLedgerHeadHash: request.latestLedgerHeadHash,
latestLedgerRevision: request.latestLedgerRevision,
earliestApprovalExpiry: closure.earliestApprovalExpiry,
};
}
async function main() {
const path = requestPath(process.argv.slice(2));
const result = verifyApprovalRequest(await readFile(path));
process.stdout.write(Buffer.from(canonicalValueBytes(result)));
process.stdout.write('\n');
}
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch((error) => {
const message = error instanceof Error ? error.message : 'Unknown verification failure.';
process.stderr.write(`Approval verification failed: ${message}\n`);
process.exitCode = 1;
});
}