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
+51 -36
View File
@@ -10,7 +10,7 @@ import {
RELEASE_CANDIDATE_MANIFEST_PATH,
} from "../lib/release-candidate.ts";
import { validatePackageScriptGraph } from "../lib/package-script-graph.ts";
import { PROMOTED_STAGING_PATHS } from "./promotion-artifacts.ts";
import { PROMOTED_UPLOAD_PATHS } from "./promotion-artifacts.ts";
const ciActionRegistrationSchema = z
.object({
@@ -332,7 +332,11 @@ const extractStep = z
})
.strict();
const providerStep = z
.object({ kind: z.literal("run-provider"), provider: z.enum(["vulnerability", "provenance"]) })
.object({
kind: z.literal("run-provider"),
provider: z.enum(["vulnerability", "provenance"]),
stepId: id,
})
.strict();
const validateProviderStep = z
.object({
@@ -340,7 +344,12 @@ const validateProviderStep = z
provider: z.enum(["vulnerability", "provenance"]),
})
.strict();
const promotionStep = z.object({ kind: z.literal("verify-promotion") }).strict();
const promotionStep = z
.object({ kind: z.literal("verify-promotion"), stepId: id })
.strict();
const cleanupPromotionStep = z
.object({ kind: z.literal("cleanup-promotion"), finalizerStepId: id })
.strict();
const jobStepSchema = z.discriminatedUnion("kind", [
checkoutStep,
@@ -356,6 +365,7 @@ const jobStepSchema = z.discriminatedUnion("kind", [
providerStep,
validateProviderStep,
promotionStep,
cleanupPromotionStep,
]);
const jobSchema = z
@@ -756,9 +766,9 @@ function validateContractSemantics(
merge_gate: ["checkout", "setup-node", "frozen-install", "browser-install", "run-gate", "upload"],
release_gate: ["checkout", "setup-node", "frozen-install", "browser-install", "run-gate", "upload"],
immutable_build: ["checkout", "setup-node", "frozen-install", "run-gate", "archive-candidate", "upload"],
vulnerability_provider: ["checkout", "setup-node", "frozen-install", "download", "extract", "run-provider", "validate-provider-evidence", "upload"],
provenance_provider: ["checkout", "setup-node", "frozen-install", "download", "extract", "run-provider", "validate-provider-evidence", "upload"],
promotion: ["checkout", "setup-node", "frozen-install", "download", "download", "download", "extract", "verify-promotion", "upload"],
vulnerability_provider: ["checkout", "setup-node", "frozen-install", "download", "run-provider", "validate-provider-evidence", "upload"],
provenance_provider: ["checkout", "setup-node", "frozen-install", "download", "run-provider", "validate-provider-evidence", "upload"],
promotion: ["checkout", "setup-node", "frozen-install", "download", "download", "download", "verify-promotion", "upload", "cleanup-promotion"],
production_gate: ["checkout", "setup-node", "frozen-install", "run-gate", "upload"],
field_gate: ["checkout", "setup-node", "frozen-install", "run-gate", "upload"],
documentation_gate: ["checkout", "setup-node", "frozen-install", "run-gate", "upload"],
@@ -776,8 +786,11 @@ function validateContractSemantics(
vulnerability_provider: [
{ name: "CANDIDATE_ARCHIVE_SHA256", value: "${{ needs.immutable_build.outputs.archive_sha256 }}" },
{ name: "CANDIDATE_ARCHIVE_PATH", value: ".release/vulnerability-candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz" },
{ name: "CANDIDATE_DIST_SHA256", value: "${{ needs.immutable_build.outputs.dist_sha256 }}" },
{ name: "CANDIDATE_LOCKFILE_PATH", value: ".release/verified-vulnerability/pnpm-lock.yaml" },
{ name: "CI_RUN_ID", value: "${{ gitea.run_id }}" },
{ name: "CI_RUN_ATTEMPT", value: "${{ gitea.run_attempt }}" },
{ name: "EXPECTED_SOURCE_REVISION", value: "${{ gitea.sha }}" },
{ name: "VULNERABILITY_PUBLIC_KEY_PATH", value: "${{ vars.VULNERABILITY_PUBLIC_KEY_PATH }}" },
{ name: "VULNERABILITY_KEY_ID", value: "${{ vars.VULNERABILITY_KEY_ID }}" },
{ name: "VULNERABILITY_PROVIDER_COMMAND", value: "${{ vars.VULNERABILITY_PROVIDER_COMMAND }}" },
{ name: "VULNERABILITY_REPORT_PATH", value: "provider-evidence/untrusted/vulnerability-report.json" },
{ name: "VALIDATED_PROVIDER_REPORT_PATH", value: "provider-evidence/vulnerability-report.json" },
@@ -785,8 +798,11 @@ function validateContractSemantics(
provenance_provider: [
{ name: "CANDIDATE_ARCHIVE_SHA256", value: "${{ needs.immutable_build.outputs.archive_sha256 }}" },
{ name: "CANDIDATE_ARCHIVE_PATH", value: ".release/provenance-candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz" },
{ name: "CANDIDATE_DIST_SHA256", value: "${{ needs.immutable_build.outputs.dist_sha256 }}" },
{ name: "CANDIDATE_LOCKFILE_PATH", value: ".release/verified-provenance/pnpm-lock.yaml" },
{ name: "CI_RUN_ID", value: "${{ gitea.run_id }}" },
{ name: "CI_RUN_ATTEMPT", value: "${{ gitea.run_attempt }}" },
{ name: "EXPECTED_SOURCE_REVISION", value: "${{ gitea.sha }}" },
{ name: "PROVENANCE_PUBLIC_KEY_PATH", value: "${{ vars.PROVENANCE_PUBLIC_KEY_PATH }}" },
{ name: "PROVENANCE_KEY_ID", value: "${{ vars.PROVENANCE_KEY_ID }}" },
{ name: "PROVENANCE_PROVIDER_COMMAND", value: "${{ vars.PROVENANCE_PROVIDER_COMMAND }}" },
{ name: "PROVENANCE_ATTESTATION_PATH", value: "provider-evidence/untrusted/provenance-attestation.json" },
{ name: "VALIDATED_PROVIDER_REPORT_PATH", value: "provider-evidence/provenance-attestation.json" },
@@ -794,13 +810,16 @@ function validateContractSemantics(
promotion: [
{ name: "CANDIDATE_ARCHIVE_SHA256", value: "${{ needs.immutable_build.outputs.archive_sha256 }}" },
{ name: "CANDIDATE_ARCHIVE_PATH", value: ".release/candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz" },
{ name: "CANDIDATE_ROOT", value: "${{ gitea.workspace }}/.release/verified-candidate" },
{ name: "CI_RUN_ID", value: "${{ gitea.run_id }}" },
{ name: "CI_RUN_ATTEMPT", value: "${{ gitea.run_attempt }}" },
{ name: "VULNERABILITY_REPORT_PATH", value: "${{ gitea.workspace }}/.release/vulnerability/vulnerability-report.json" },
{ name: "PROVENANCE_ATTESTATION_PATH", value: "${{ gitea.workspace }}/.release/provenance/provenance-attestation.json" },
{ name: "VULNERABILITY_PUBLIC_KEY_PATH", value: "${{ vars.VULNERABILITY_PUBLIC_KEY_PATH }}" },
{ name: "VULNERABILITY_KEY_ID", value: "${{ vars.VULNERABILITY_KEY_ID }}" },
{ name: "PROVENANCE_PUBLIC_KEY_PATH", value: "${{ vars.PROVENANCE_PUBLIC_KEY_PATH }}" },
{ name: "PROVENANCE_KEY_ID", value: "${{ vars.PROVENANCE_KEY_ID }}" },
{ name: "VULNERABILITY_INVOCATION_NONCE", value: "${{ needs.vulnerability_provider.outputs.invocation_nonce }}" },
{ name: "PROVENANCE_INVOCATION_NONCE", value: "${{ needs.provenance_provider.outputs.invocation_nonce }}" },
],
production_gate: [],
field_gate: [
@@ -875,12 +894,14 @@ function validateContractSemantics(
if (upload?.kind === "upload" && upload.always) {
issue("promotion upload must not use always");
}
const cleanupIndex = order.indexOf("cleanup-promotion");
if (
order.indexOf("extract") < order.lastIndexOf("download") ||
order.indexOf("verify-promotion") < order.indexOf("extract") ||
order.indexOf("upload") < order.indexOf("verify-promotion")
order.includes("extract") ||
verificationIndex < order.lastIndexOf("download") ||
uploadIndex < verificationIndex ||
cleanupIndex !== uploadIndex + 1
) {
issue("promotion formula order must download, verify, then upload");
issue("promotion formula order must download, finalize, upload, then cleanup without extraction");
}
}
const immutable = contract.jobs.find(({ id }) => id === "immutable_build");
@@ -905,7 +926,7 @@ function validateContractSemantics(
const promotionUpload = promotion?.steps.find(
(step) => step.kind === "upload" && step.transferId === "promoted-release",
);
if (!promotionUpload || promotionUpload.kind !== "upload" || JSON.stringify(promotionUpload.paths) !== JSON.stringify(PROMOTED_STAGING_PATHS)) {
if (!promotionUpload || promotionUpload.kind !== "upload" || JSON.stringify(promotionUpload.paths) !== JSON.stringify(PROMOTED_UPLOAD_PATHS)) {
issue("promotion upload bundle must contain the exact five typed paths");
}
@@ -934,10 +955,9 @@ function validateCanonicalStepFields(
const providerExpectations = {
vulnerability_provider: {
provider: "vulnerability",
stepId: "supervise_vulnerability",
downloadPath: ".release/vulnerability-candidate",
archivePath: ".release/vulnerability-candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz",
targetRoot: ".release/verified-vulnerability",
lockfilePath: ".release/verified-vulnerability/pnpm-lock.yaml",
rawPath: "provider-evidence/untrusted/vulnerability-report.json",
rawName: "VULNERABILITY_REPORT_PATH",
sealedPath: "provider-evidence/vulnerability-report.json",
@@ -945,10 +965,9 @@ function validateCanonicalStepFields(
},
provenance_provider: {
provider: "provenance",
stepId: "supervise_provenance",
downloadPath: ".release/provenance-candidate",
archivePath: ".release/provenance-candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz",
targetRoot: ".release/verified-provenance",
lockfilePath: ".release/verified-provenance/pnpm-lock.yaml",
rawPath: "provider-evidence/untrusted/provenance-attestation.json",
rawName: "PROVENANCE_ATTESTATION_PATH",
sealedPath: "provider-evidence/provenance-attestation.json",
@@ -959,7 +978,6 @@ function validateCanonicalStepFields(
const job = contract.jobs.find(({ id }) => id === jobId);
const environment = new Map(job?.environment.map(({ name, value }) => [name, value]));
const download = job?.steps.find(({ kind }) => kind === "download");
const extract = job?.steps.find(({ kind }) => kind === "extract");
const runProvider = job?.steps.find(({ kind }) => kind === "run-provider");
const validateProvider = job?.steps.find(({ kind }) => kind === "validate-provider-evidence");
const upload = job?.steps.find(
@@ -967,11 +985,9 @@ function validateCanonicalStepFields(
);
if (
!download || download.kind !== "download" || download.transferId !== "release-candidate" || download.path !== expected.downloadPath ||
!extract || extract.kind !== "extract" || extract.archivePath !== expected.archivePath || extract.targetRoot !== expected.targetRoot ||
!runProvider || runProvider.kind !== "run-provider" || runProvider.provider !== expected.provider ||
!runProvider || runProvider.kind !== "run-provider" || runProvider.provider !== expected.provider || runProvider.stepId !== expected.stepId ||
!validateProvider || validateProvider.kind !== "validate-provider-evidence" || validateProvider.provider !== expected.provider ||
environment.get("CANDIDATE_ARCHIVE_PATH") !== expected.archivePath ||
environment.get("CANDIDATE_LOCKFILE_PATH") !== expected.lockfilePath ||
environment.get(expected.rawName) !== expected.rawPath ||
environment.get("VALIDATED_PROVIDER_REPORT_PATH") !== expected.sealedPath ||
!upload || upload.kind !== "upload" || JSON.stringify(upload.paths) !== JSON.stringify([expected.sealedPath])
@@ -987,15 +1003,18 @@ function validateCanonicalStepFields(
{ kind: "download", transferId: "vulnerability-provider-evidence", path: ".release/vulnerability" },
{ kind: "download", transferId: "provenance-provider-evidence", path: ".release/provenance" },
];
const promotionExtract = promotion?.steps.find(({ kind }) => kind === "extract");
const promotionFinalizer = promotion?.steps.find(({ kind }) => kind === "verify-promotion");
const promotionCleanup = promotion?.steps.find(({ kind }) => kind === "cleanup-promotion");
if (
JSON.stringify(promotionDownloads) !== JSON.stringify(expectedDownloads) ||
!promotionExtract ||
promotionExtract.kind !== "extract" ||
promotionExtract.archivePath !== ".release/candidate/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz" ||
promotionExtract.targetRoot !== ".release/verified-candidate"
!promotionFinalizer ||
promotionFinalizer.kind !== "verify-promotion" ||
promotionFinalizer.stepId !== "finalize" ||
!promotionCleanup ||
promotionCleanup.kind !== "cleanup-promotion" ||
promotionCleanup.finalizerStepId !== "finalize"
) {
issue("promotion download and extraction fields must remain linked");
issue("promotion download fields and finalizer/cleanup step identities must remain linked");
}
}
@@ -1008,7 +1027,7 @@ function validateJobStepKinds(
"gate-single": new Set(["checkout", "setup-node", "frozen-install", "run-gate", "upload"]),
immutable: new Set(["checkout", "setup-node", "frozen-install", "run-gate", "archive-candidate", "upload"]),
provider: new Set(["checkout", "setup-node", "frozen-install", "download", "validate-candidate-archive", "extract", "run-provider", "validate-provider-evidence", "upload"]),
promotion: new Set(["checkout", "setup-node", "frozen-install", "download", "validate-candidate-archive", "extract", "verify-promotion", "upload"]),
promotion: new Set(["checkout", "setup-node", "frozen-install", "download", "verify-promotion", "upload", "cleanup-promotion"]),
};
for (const step of job.steps) {
if (!allowed[job.kind].has(step.kind)) {
@@ -1016,16 +1035,12 @@ function validateJobStepKinds(
}
}
const kinds = job.steps.map(({ kind }) => kind);
const extractIndex = kinds.indexOf("extract");
if ((job.kind === "provider" || job.kind === "promotion") && extractIndex < 0) {
issue(`verified extraction step is missing: ${job.id}`);
}
if (job.kind === "provider") {
const providerIndex = kinds.indexOf("run-provider");
const validateProviderIndex = kinds.indexOf("validate-provider-evidence");
const uploadIndex = kinds.indexOf("upload");
if (
providerIndex < extractIndex ||
providerIndex < kinds.lastIndexOf("download") ||
validateProviderIndex < providerIndex ||
uploadIndex < validateProviderIndex
) {