fix: harden provider and promotion evidence

This commit is contained in:
DongHyeonka
2026-08-02 16:28:24 +09:00
parent 42ffb79997
commit 30ceac23c1
29 changed files with 3961 additions and 1076 deletions
+40 -55
View File
@@ -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;
}