95 lines
3.3 KiB
TypeScript
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 } : {};
|
|
}
|