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
+56 -30
View File
@@ -1,6 +1,6 @@
import { spawn } from "node:child_process";
import { constants } from "node:fs";
import { access, lstat, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { access, appendFile, lstat, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
@@ -8,7 +8,9 @@ import {
provenanceProviderAttestationSchema,
vulnerabilityProviderReportSchema,
} from "./lib/provider-evidence.ts";
import { validateProviderUpload } from "./lib/provider-upload-validator.ts";
import { readBoundedRegularFile } from "./lib/ci-artifact-validator.ts";
import { readProviderTrust } from "./lib/promotion-verifier.ts";
import { superviseProviderEvidence } from "./lib/provider-supervisor.ts";
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
import {
assertSafePublishLeaf,
@@ -29,22 +31,37 @@ const reportPath =
? process.env.VULNERABILITY_REPORT_PATH
: process.env.PROVENANCE_ATTESTATION_PATH;
const sealedPath = process.env.VALIDATED_PROVIDER_REPORT_PATH;
const candidateLockfile = process.env.CANDIDATE_LOCKFILE_PATH;
const archivePath = process.env.CANDIDATE_ARCHIVE_PATH;
const archiveSha256 = process.env.CANDIDATE_ARCHIVE_SHA256;
const candidateDistSha256 = process.env.CANDIDATE_DIST_SHA256;
const publicKeyPath = kind === "vulnerability"
? process.env.VULNERABILITY_PUBLIC_KEY_PATH
: process.env.PROVENANCE_PUBLIC_KEY_PATH;
const keyId = kind === "vulnerability"
? process.env.VULNERABILITY_KEY_ID
: process.env.PROVENANCE_KEY_ID;
const runId = process.env.GITEA_RUN_ID ?? process.env.GITHUB_RUN_ID ?? process.env.CI_RUN_ID;
const runAttemptSource = process.env.GITEA_RUN_ATTEMPT ??
process.env.GITHUB_RUN_ATTEMPT ?? process.env.CI_RUN_ATTEMPT;
const sourceRevision = process.env.EXPECTED_SOURCE_REVISION ?? process.env.VITE_COMMIT_SHA;
if (
!command ||
!reportPath ||
!sealedPath ||
!candidateLockfile ||
!archivePath ||
!archiveSha256 ||
!candidateDistSha256
!publicKeyPath ||
!keyId ||
!runId ||
!runAttemptSource ||
!sourceRevision
) {
process.stderr.write("Provider supervisor environment is incomplete\n");
process.exit(2);
}
const runAttempt = Number(runAttemptSource);
if (!Number.isInteger(runAttempt) || runAttempt < 1 || runAttempt > 1_000) {
throw new TypeError("provider supervisor run attempt is invalid");
}
const workspaceRoot = process.cwd();
const reportAbsolute = path.resolve(reportPath);
@@ -62,22 +79,30 @@ await prepareMissingProviderOutput(workspaceRoot, sealedAbsolute, sealedPath, "s
await access("/usr/bin/bwrap", constants.X_OK).catch(() => {
throw new Error("provider sandbox unavailable: /usr/bin/bwrap is required");
});
const childEnvironment = createProviderEnvironment(kind, reportPath, {
candidateLockfile,
archivePath,
archiveSha256,
candidateDistSha256,
});
await runProviderInSandbox(command, childEnvironment, rawDirectory, workspaceRoot);
const parsed = await validateProviderUpload({
const trust = await readProviderTrust(workspaceRoot, publicKeyPath, keyId);
if (!trust) throw new TypeError("provider supervisor trust key is invalid");
const supervised = await superviseProviderEvidence({
kind,
candidateRoot: path.dirname(path.resolve(candidateLockfile)),
archivePath,
expectedArchiveSha256: archiveSha256,
reportPath,
workspaceRoot,
expectedDistSha256: candidateDistSha256,
expectedRun: { id: runId, attempt: runAttempt, sourceRevision },
trust,
executeProvider: async ({ candidateRoot, environment }) => {
const childEnvironment = createProviderEnvironment(kind, reportPath, environment);
await runProviderInSandbox(
command,
childEnvironment,
rawDirectory,
workspaceRoot,
candidateRoot,
);
},
captureReport: () =>
readBoundedRegularFile({
root: workspaceRoot,
relativePath: path.relative(workspaceRoot, reportAbsolute).replaceAll(path.sep, "/"),
maxBytes: 8_388_608,
}),
});
await assertSafePublishLeaf(sealedAbsolute, sealedPath);
await writeValidatedJsonArtifact({
@@ -86,19 +111,21 @@ await writeValidatedJsonArtifact({
kind === "vulnerability"
? vulnerabilityProviderReportSchema
: provenanceProviderAttestationSchema,
value: parsed,
value: supervised.evidence,
});
if (process.env.GITHUB_OUTPUT) {
await appendFile(
process.env.GITHUB_OUTPUT,
`invocation_nonce=${supervised.invocationNonce}\n`,
"utf8",
);
}
process.stdout.write(`${kind} provider supervised validation: PASS\n`);
function createProviderEnvironment(
providerKind: "vulnerability" | "provenance",
rawReportPath: string,
candidate: Readonly<{
candidateLockfile: string;
archivePath: string;
archiveSha256: string;
candidateDistSha256: string;
}>,
bindings: Readonly<Record<string, string>>,
): NodeJS.ProcessEnv {
const environment: NodeJS.ProcessEnv = {
PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
@@ -107,10 +134,7 @@ function createProviderEnvironment(
CI: "true",
GITHUB_ENV: "/tmp/github-env",
GITHUB_PATH: "/tmp/github-path",
CANDIDATE_LOCKFILE_PATH: candidate.candidateLockfile,
CANDIDATE_ARCHIVE_PATH: candidate.archivePath,
CANDIDATE_ARCHIVE_SHA256: candidate.archiveSha256,
CANDIDATE_DIST_SHA256: candidate.candidateDistSha256,
...bindings,
...(providerKind === "vulnerability"
? { VULNERABILITY_REPORT_PATH: rawReportPath }
: { PROVENANCE_ATTESTATION_PATH: rawReportPath }),
@@ -132,6 +156,7 @@ async function runProviderInSandbox(
environment: NodeJS.ProcessEnv,
rawDirectory: string,
workspaceRoot: string,
candidateRoot: string,
): Promise<void> {
const scratch = await mkdtemp(path.join(tmpdir(), "ci-provider-sandbox-"));
try {
@@ -177,6 +202,7 @@ async function runProviderInSandbox(
}
arguments_.push(
"--bind", rawDirectory, rawDirectory,
"--ro-bind", candidateRoot, "/candidate",
"--chdir", workspaceRoot,
"/bin/sh", "-eu", "-c", command,
);