fix: harden provider and promotion evidence
This commit is contained in:
@@ -1,74 +1,59 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
provenanceProviderAttestationSchema,
|
||||
vulnerabilityProviderReportSchema,
|
||||
validateProviderEvidence,
|
||||
type ExpectedPromotionContext,
|
||||
type ProviderTrust,
|
||||
} from "./provider-evidence.ts";
|
||||
import {
|
||||
verifyReleaseCandidate,
|
||||
type ReleaseCandidateManifest,
|
||||
} from "./release-candidate.ts";
|
||||
import { readBoundedRegularFile } from "./ci-artifact-validator.ts";
|
||||
import { verifyCiCandidateArchive } from "./ci-candidate-archive.ts";
|
||||
|
||||
export async function validateProviderUpload(input: Readonly<{
|
||||
kind: "vulnerability" | "provenance";
|
||||
verifiedManifest: ReleaseCandidateManifest;
|
||||
archiveSha256: string;
|
||||
candidateRoot: string;
|
||||
archivePath: string;
|
||||
expectedArchiveSha256: string;
|
||||
reportPath: string;
|
||||
workspaceRoot?: string;
|
||||
expectedDistSha256: string;
|
||||
capturedReport: Buffer;
|
||||
expectedContext: ExpectedPromotionContext;
|
||||
trust: ProviderTrust;
|
||||
nowEpochMs?: () => number;
|
||||
}>): Promise<unknown> {
|
||||
if (!/^[a-f0-9]{64}$/u.test(input.expectedDistSha256)) {
|
||||
throw new TypeError("expected candidate dist SHA-256 is invalid");
|
||||
if (
|
||||
input.expectedContext.candidate.archiveSha256 !== input.archiveSha256 ||
|
||||
input.expectedContext.candidate.bundleSha256 !== input.verifiedManifest.bundleSha256 ||
|
||||
input.expectedContext.candidate.distSha256 !== input.verifiedManifest.distSha256 ||
|
||||
input.expectedContext.candidate.lockfileSha256 !== input.verifiedManifest.lockfileSha256
|
||||
) {
|
||||
throw new Error("provider supervisor expected candidate context mismatch");
|
||||
}
|
||||
const archive = await verifyCiCandidateArchive({
|
||||
archivePath: input.archivePath,
|
||||
expectedSha256: input.expectedArchiveSha256,
|
||||
});
|
||||
const manifest = archive.manifest;
|
||||
if (manifest.distSha256 !== input.expectedDistSha256) {
|
||||
throw new Error("provider input candidate dist digest mismatch");
|
||||
}
|
||||
const verifiedCandidate = await verifyReleaseCandidate(manifest, input.candidateRoot);
|
||||
const verifiedCandidate = await verifyReleaseCandidate(
|
||||
input.verifiedManifest,
|
||||
input.candidateRoot,
|
||||
);
|
||||
if (verifiedCandidate.failures.length > 0) {
|
||||
throw new Error(
|
||||
`provider input candidate root changed: ${verifiedCandidate.failures.join("; ")}`,
|
||||
);
|
||||
}
|
||||
const reportAbsolute = path.resolve(input.reportPath);
|
||||
const reportRoot = path.resolve(input.workspaceRoot ?? process.cwd());
|
||||
const reportRelative = path.relative(reportRoot, reportAbsolute).replaceAll(path.sep, "/");
|
||||
const report = JSON.parse(
|
||||
new TextDecoder("utf-8", { fatal: true }).decode(
|
||||
await readBoundedRegularFile({
|
||||
root: reportRoot,
|
||||
relativePath: reportRelative,
|
||||
maxBytes: 8_388_608,
|
||||
}),
|
||||
),
|
||||
) as unknown;
|
||||
if (input.kind === "vulnerability") {
|
||||
const parsed = vulnerabilityProviderReportSchema.parse(report);
|
||||
const lockfile = await readBoundedRegularFile({
|
||||
root: input.candidateRoot,
|
||||
relativePath: "pnpm-lock.yaml",
|
||||
maxBytes: 67_108_864,
|
||||
});
|
||||
const lockfileSha256 = createHash("sha256").update(lockfile).digest("hex");
|
||||
if (
|
||||
parsed.scannedDistSha256 !== manifest.distSha256 ||
|
||||
parsed.scannedLockfileSha256 !== manifest.lockfileSha256 ||
|
||||
lockfileSha256 !== manifest.lockfileSha256
|
||||
) {
|
||||
throw new Error("vulnerability provider evidence candidate digest mismatch");
|
||||
}
|
||||
return parsed;
|
||||
let report: unknown;
|
||||
try {
|
||||
report = JSON.parse(
|
||||
new TextDecoder("utf-8", { fatal: true }).decode(input.capturedReport),
|
||||
) as unknown;
|
||||
} catch {
|
||||
throw new TypeError("provider output is not canonical UTF-8 JSON");
|
||||
}
|
||||
const parsed = provenanceProviderAttestationSchema.parse(report);
|
||||
if (parsed.subject.digest.sha256 !== manifest.distSha256) {
|
||||
throw new Error("provenance provider evidence candidate digest mismatch");
|
||||
const evaluated = validateProviderEvidence({
|
||||
kind: input.kind,
|
||||
value: report,
|
||||
expected: input.expectedContext,
|
||||
trust: input.trust,
|
||||
nowEpochMs: input.nowEpochMs,
|
||||
});
|
||||
if (evaluated.status !== "PASS" || !evaluated.evidence) {
|
||||
throw new Error(
|
||||
`provider evidence context validation failed: ${evaluated.failures.join("; ")}`,
|
||||
);
|
||||
}
|
||||
return parsed;
|
||||
return evaluated.evidence;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user