Files
tech-log-frontend/scripts/verify-exact-promotion-bundle.ts
T

95 lines
3.3 KiB
TypeScript

import path from "node:path";
import { readdir } from "node:fs/promises";
import { PROMOTED_FILE_NAMES } from "./contracts/promotion-artifacts.ts";
import { readBoundedRegularFile } from "./lib/ci-artifact-validator.ts";
import { verifyExactPromotionBundle } from "./lib/exact-promotion-bundle.ts";
import { readProviderTrust } from "./lib/provider-trust.ts";
const required = (name: string): string => {
const value = process.env[name];
if (!value) throw new TypeError(`exact promotion verification environment is missing ${name}`);
return value;
};
const rootArgument = process.argv.indexOf("--root");
const bundleRoot = path.resolve(
rootArgument >= 0
? process.argv[rootArgument + 1] ?? ""
: required("PROMOTION_BUNDLE_ROOT"),
);
if (rootArgument >= 0 && !process.argv[rootArgument + 1]) {
throw new TypeError("--root requires a promotion bundle directory");
}
const trustRoot = process.cwd();
const [vulnerabilityTrust, provenanceTrust] = await Promise.all([
readProviderTrust(
trustRoot,
required("VULNERABILITY_PUBLIC_KEY_PATH"),
required("VULNERABILITY_KEY_ID"),
),
readProviderTrust(
trustRoot,
required("PROVENANCE_PUBLIC_KEY_PATH"),
required("PROVENANCE_KEY_ID"),
),
]);
if (!vulnerabilityTrust || !provenanceTrust) {
throw new TypeError("exact promotion verification trust keys are invalid");
}
const names = (await readdir(bundleRoot)).sort(asciiCompare);
if (
JSON.stringify(names) !==
JSON.stringify([...PROMOTED_FILE_NAMES].sort(asciiCompare))
) {
throw new TypeError("promotion bundle directory must contain exactly the canonical five files");
}
const files = Object.fromEntries(
await Promise.all(
PROMOTED_FILE_NAMES.map(async (name) => [
name,
await readBoundedRegularFile({
root: bundleRoot,
relativePath: name,
maxBytes: name === "release-candidate.tar.gz" ? 268_435_456 : 16_777_216,
}),
] as const),
),
);
await verifyExactPromotionBundle(files, {
vulnerabilityTrust,
provenanceTrust,
expected: {
run: {
id: required("EXPECTED_PROMOTION_RUN_ID"),
attempt: requiredPositiveInteger("EXPECTED_PROMOTION_RUN_ATTEMPT"),
},
sourceRevision: required("EXPECTED_PROMOTION_SOURCE_REVISION"),
archiveSha256: required("EXPECTED_PROMOTION_ARCHIVE_SHA256"),
...optionalDigest("sourceSetSha256", "EXPECTED_PROMOTION_SOURCE_SET_SHA256"),
...optionalDigest("bundleSha256", "EXPECTED_PROMOTION_BUNDLE_SHA256"),
...optionalDigest("distSha256", "EXPECTED_PROMOTION_DIST_SHA256"),
...optionalDigest("lockfileSha256", "EXPECTED_PROMOTION_LOCKFILE_SHA256"),
},
});
process.stdout.write("Exact promotion bundle verification: PASS\n");
function asciiCompare(left: string, right: string): number {
return left < right ? -1 : left > right ? 1 : 0;
}
function requiredPositiveInteger(name: string): number {
const value = required(name);
if (!/^[1-9][0-9]*$/u.test(value) || !Number.isSafeInteger(Number(value))) {
throw new TypeError(`${name} must be a positive safe integer`);
}
return Number(value);
}
function optionalDigest(
property: "sourceSetSha256" | "bundleSha256" | "distSha256" | "lockfileSha256",
environmentName: string,
): Readonly<Record<string, string>> {
const value = process.env[environmentName];
return value ? { [property]: value } : {};
}