fix: bind final release authority and expiry
This commit is contained in:
@@ -674,6 +674,9 @@ async function activateUnlocked(input) {
|
||||
) {
|
||||
throw new Error("Protected authority changed before pointer publication.");
|
||||
}
|
||||
const publicationNow = nowFrom(input.testClock);
|
||||
if (Date.parse(publicationNow) >= Date.parse(receipt.expiresAt))
|
||||
throw new Error("Release expired before pointer publication.");
|
||||
const nextPath = join(root, "current.next");
|
||||
const nextInfo = await lstat(nextPath).catch((error) =>
|
||||
error?.code === "ENOENT" ? null : Promise.reject(error),
|
||||
@@ -697,7 +700,7 @@ async function activateUnlocked(input) {
|
||||
finalState,
|
||||
tx,
|
||||
receipt,
|
||||
activationNow,
|
||||
publicationNow,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,12 @@ const reportNames = [
|
||||
const allowedLicenses = new Set([
|
||||
'0BSD', 'Apache-2.0', 'BlueOak-1.0.0', 'BSD-2-Clause', 'BSD-3-Clause', 'CC0-1.0', 'ISC', 'MIT', 'Python-2.0', 'Unlicense',
|
||||
]);
|
||||
const productionAuthorityRoot = '/etc/portfolio-release-authority';
|
||||
const productionVerifyFields = ['projectRoot', 'evidenceDir'];
|
||||
const testVerifyFields = [
|
||||
'projectRoot', 'evidenceDir', 'toolTrustPolicyPath', 'expectedCommitSha',
|
||||
'requireTrackedTriage', 'allowedLicenses', 'now', 'today',
|
||||
];
|
||||
|
||||
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
||||
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
@@ -31,6 +37,31 @@ async function safeRead(path, label) {
|
||||
return readFile(path);
|
||||
}
|
||||
|
||||
async function assertAuthorityDirectory(path, label) {
|
||||
const stats = await lstat(path);
|
||||
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
|
||||
if (!stats.isDirectory() || stats.isSymbolicLink() || uid === null || stats.uid !== uid || (stats.mode & 0o022) !== 0) {
|
||||
throw new Error(`${label} must be an activation-service-owned, non-writable regular directory.`);
|
||||
}
|
||||
}
|
||||
|
||||
async function readProductionAuthority() {
|
||||
const controlPath = resolve(productionAuthorityRoot, '.control');
|
||||
await assertAuthorityDirectory(productionAuthorityRoot, 'Supply-chain authority root');
|
||||
await assertAuthorityDirectory(controlPath, 'Supply-chain authority control directory');
|
||||
const trustPolicyPath = resolve(controlPath, 'execution-trust.json');
|
||||
const authorityPath = resolve(controlPath, 'supply-chain-authority.json');
|
||||
const trustPolicyBytes = await safeRead(trustPolicyPath, 'Execution evidence trust policy');
|
||||
const authority = parseJson(await safeRead(authorityPath, 'Supply-chain authority binding'), 'Supply-chain authority binding');
|
||||
if (!exactKeys(authority, ['schema', 'executionTrustPolicyDigest']) ||
|
||||
authority.schema !== 'portfolio-supply-chain-authority/v1' ||
|
||||
!sha256Pattern.test(authority.executionTrustPolicyDigest) ||
|
||||
authority.executionTrustPolicyDigest !== sha256(trustPolicyBytes)) {
|
||||
throw new Error('Supply-chain authority binding does not match the protected execution trust policy.');
|
||||
}
|
||||
return { trustPolicyPath };
|
||||
}
|
||||
|
||||
function parseJson(bytes, label) {
|
||||
try { return JSON.parse(bytes.toString('utf8')); } catch { throw new Error(`${label} is not valid JSON.`); }
|
||||
}
|
||||
@@ -182,7 +213,7 @@ async function verifyExecutionSecurity(envelope, trustPath, now) {
|
||||
if (!verify(null, canonicalValueBytes(payload), publicKey, Buffer.from(signature.value, 'base64url'))) {
|
||||
throw new Error('Execution security evidence signature verification failed.');
|
||||
}
|
||||
return { imageCount: roles.length, nativeToolCount: nativeNames.size };
|
||||
return { imageCount: roles.length, nativeToolCount: nativeNames.size, commitSha: repository.commitSha };
|
||||
}
|
||||
|
||||
function assertTracked(projectRoot, triagePath) {
|
||||
@@ -195,10 +226,31 @@ function assertTracked(projectRoot, triagePath) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifySupplyChainEvidence(options) {
|
||||
function assertVerifyOptions(options, fields, label, exact = false) {
|
||||
if (!isObject(options) || (exact ? !exactKeys(options, fields) :
|
||||
Object.keys(options).some((key) => !fields.includes(key)) ||
|
||||
!['projectRoot', 'evidenceDir'].every((key) => Object.hasOwn(options, key)))) {
|
||||
throw new Error(`${label} contains unknown fields or is incomplete.`);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveGitHead(projectRoot) {
|
||||
let output;
|
||||
try {
|
||||
output = execFileSync('git', ['rev-parse', '--verify', 'HEAD'], { cwd: projectRoot, encoding: 'utf8' });
|
||||
} catch {
|
||||
throw new Error('Current Git HEAD could not be independently resolved.');
|
||||
}
|
||||
const commitSha = output.trim();
|
||||
if (!/^[a-f0-9]{40}$/u.test(commitSha)) throw new Error('Current Git HEAD is not a valid commit SHA.');
|
||||
return commitSha;
|
||||
}
|
||||
|
||||
async function verifySupplyChainEvidenceInternal(options) {
|
||||
const projectRoot = resolve(options.projectRoot);
|
||||
const evidenceDir = resolve(options.evidenceDir);
|
||||
const today = options.today ?? new Date().toISOString().slice(0, 10);
|
||||
const now = options.now ?? new Date().toISOString();
|
||||
const today = options.today ?? now.slice(0, 10);
|
||||
if (!datePattern.test(today)) throw new Error('Trusted supply-chain date must be YYYY-MM-DD.');
|
||||
const packageJsonBytes = await safeRead(resolve(projectRoot, 'package.json'), 'package.json');
|
||||
const packageLockBytes = await safeRead(resolve(projectRoot, 'package-lock.json'), 'package-lock.json');
|
||||
@@ -237,12 +289,32 @@ export async function verifySupplyChainEvidence(options) {
|
||||
const licenseCount = verifyLicenses(reports['licenses.json'], options.allowedLicenses ?? allowedLicenses);
|
||||
const componentCount = verifySbom(reports['sbom.cdx.json'], packageJson);
|
||||
if (typeof options.toolTrustPolicyPath !== 'string') throw new Error('Execution evidence trust policy is required.');
|
||||
const execution = await verifyExecutionSecurity(reports['execution-security.json'], options.toolTrustPolicyPath, options.now ?? new Date().toISOString());
|
||||
if (typeof options.expectedCommitSha !== 'string' || !/^[a-f0-9]{40}$/u.test(options.expectedCommitSha)) throw new Error('Expected repository commit SHA is invalid.');
|
||||
const execution = await verifyExecutionSecurity(reports['execution-security.json'], options.toolTrustPolicyPath, now);
|
||||
if (execution.commitSha !== options.expectedCommitSha) throw new Error('Signed repository commit does not match the expected current Git HEAD.');
|
||||
return { schema: 'portfolio-supply-chain-verification/v1', critical, high, medium: mediumFindings.length, licenseCount, componentCount,
|
||||
executionImages: execution.imageCount, nativeTools: execution.nativeToolCount,
|
||||
packageLockSha256: manifest.packageLockSha256, evidenceManifestSha256: sha256(await safeRead(resolve(evidenceDir, 'evidence-manifest.json'), 'Supply-chain evidence manifest')) };
|
||||
}
|
||||
|
||||
export async function verifySupplyChainEvidence(options) {
|
||||
assertVerifyOptions(options, productionVerifyFields, 'Production supply-chain request', true);
|
||||
const authority = await readProductionAuthority();
|
||||
const trustedNow = new Date().toISOString();
|
||||
return verifySupplyChainEvidenceInternal({
|
||||
...options,
|
||||
toolTrustPolicyPath: authority.trustPolicyPath,
|
||||
now: trustedNow,
|
||||
today: trustedNow.slice(0, 10),
|
||||
expectedCommitSha: resolveGitHead(resolve(options.projectRoot)),
|
||||
});
|
||||
}
|
||||
|
||||
export async function verifySupplyChainEvidenceForTest(options) {
|
||||
assertVerifyOptions(options, testVerifyFields, 'Test supply-chain request');
|
||||
return verifySupplyChainEvidenceInternal(options);
|
||||
}
|
||||
|
||||
function runTool(command, args, cwd, options = {}) {
|
||||
const result = spawnSync(command, args, { cwd, encoding: 'utf8', env: { PATH: process.env.PATH ?? '', LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8', NO_COLOR: '1' }, maxBuffer: 64 * 1024 * 1024 });
|
||||
if (result.error || (!options.allowAuditExit && result.status !== 0) || (options.allowAuditExit && ![0, 1].includes(result.status))) {
|
||||
@@ -284,12 +356,16 @@ async function collectEvidence(projectRoot, evidenceDir, triagePath, executionSe
|
||||
|
||||
function parseArguments(args) {
|
||||
const command = args.shift();
|
||||
const values = { alternate: [] };
|
||||
const values = {};
|
||||
for (let index = 0; index < args.length; index += 2) {
|
||||
const key = args[index]; const value = args[index + 1];
|
||||
if (!key?.startsWith('--') || !value || Object.hasOwn(values, key.slice(2))) throw new Error('Supply-chain arguments must be unique complete name/value pairs.');
|
||||
values[key.slice(2)] = value;
|
||||
}
|
||||
const allowed = command === 'collect'
|
||||
? new Set(['project', 'evidence', 'triage', 'execution-security', 'allow-registry-network'])
|
||||
: command === 'verify' ? new Set(['project', 'evidence']) : null;
|
||||
if (allowed === null || Object.keys(values).some((key) => !allowed.has(key))) throw new Error('Supply-chain command contains unknown fields or command.');
|
||||
return { command, values };
|
||||
}
|
||||
|
||||
@@ -303,8 +379,7 @@ async function main() {
|
||||
if (!values['execution-security']) throw new Error('Collection requires --execution-security <signed-evidence>.');
|
||||
await collectEvidence(projectRoot, evidenceDir, resolve(values.triage), resolve(values['execution-security']));
|
||||
} else if (command !== 'verify') throw new Error('Usage: supply-chain-check.mjs <collect|verify> --project <root> --evidence <dir>.');
|
||||
if (!values['tool-trust-policy']) throw new Error('Verification requires --tool-trust-policy <protected-policy>.');
|
||||
process.stdout.write(`${JSON.stringify(await verifySupplyChainEvidence({ projectRoot, evidenceDir, today: values.today, now: values.now, toolTrustPolicyPath: resolve(values['tool-trust-policy']) }))}\n`);
|
||||
process.stdout.write(`${JSON.stringify(await verifySupplyChainEvidence({ projectRoot, evidenceDir }))}\n`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
|
||||
Reference in New Issue
Block a user