import { createHash, createPublicKey, randomUUID } from "node:crypto"; import { constants } from "node:fs"; import { lstat, mkdtemp, open, rename, rm } from "node:fs/promises"; import path from "node:path"; import { evaluatePromotionEvidence, providerVerificationArtifactSchema, provenanceProviderAttestationSchema, vulnerabilityProviderReportSchema, } from "./provider-evidence.ts"; import { verifyCapturedCiCandidateArchive } from "./ci-candidate-archive.ts"; import { verifyArchivedLocalEvidence } from "./local-release-evidence.ts"; import { verifyReleaseCandidate } from "./release-candidate.ts"; import { readBoundedRegularFile } from "./ci-artifact-validator.ts"; import { assertSafePublishLeaf, ensureSafePublishDirectory, } from "./ci-gate-log.ts"; import { PROMOTED_STAGING_PATHS } from "../contracts/promotion-artifacts.ts"; export { PROMOTED_STAGING_PATHS }; type PromotionSource = Readonly<{ sourcePath: string; destinationName: string; maxBytes: number; validate: (bytes: Buffer) => void; }>; type StagedFile = Readonly<{ destinationName: string; bytes: Buffer; digest: string; }>; export async function stageVerifiedPromotion(input: Readonly<{ repositoryRoot: string; archivePath: string; expectedArchiveSha256: string; vulnerabilityReportPath: string; provenanceAttestationPath: string; vulnerabilityPublicKeyPath: string; vulnerabilityKeyId: string; provenancePublicKeyPath: string; provenanceKeyId: string; }>, dependencies: Readonly<{ verifyLocalEvidence?: typeof verifyArchivedLocalEvidence; afterCapture?: () => Promise; beforePublishRename?: () => Promise; }> = {}): Promise>> { const root = path.resolve(input.repositoryRoot); if (!/^[a-f0-9]{64}$/u.test(input.expectedArchiveSha256)) { throw new TypeError("promotion archive SHA-256 is invalid"); } const sources: PromotionSource[] = [ { sourcePath: input.archivePath, destinationName: "release-candidate.tar.gz", maxBytes: 268_435_456, validate: (bytes) => { if (sha256(bytes) !== input.expectedArchiveSha256) { throw new Error("promotion archive SHA-256 changed before staging"); } }, }, { sourcePath: input.vulnerabilityReportPath, destinationName: "vulnerability-report.json", maxBytes: 16_777_216, validate: (bytes) => vulnerabilityProviderReportSchema.parse(parseJson(bytes)), }, { sourcePath: input.provenanceAttestationPath, destinationName: "provenance-attestation.json", maxBytes: 16_777_216, validate: (bytes) => provenanceProviderAttestationSchema.parse(parseJson(bytes)), }, { sourcePath: "artifacts/security/provider-verification.json", destinationName: "provider-verification.json", maxBytes: 4_194_304, validate: (bytes) => providerVerificationArtifactSchema.parse(parseJson(bytes)), }, { sourcePath: "artifacts/security/promotion-verification.json", destinationName: "promotion-verification.json", maxBytes: 4_194_304, validate: (bytes) => providerVerificationArtifactSchema.parse(parseJson(bytes)), }, ]; const [captured, vulnerabilityPublicKey, provenancePublicKey] = await Promise.all([ Promise.all( sources.map(async (source) => { const relativePath = repositoryRelative(root, source.sourcePath); const bytes = await readBoundedRegularFile({ root, relativePath, maxBytes: source.maxBytes, }); source.validate(bytes); return Object.freeze({ ...source, bytes, digest: sha256(bytes) }); }), ), capture(root, input.vulnerabilityPublicKeyPath, 1_048_576), capture(root, input.provenancePublicKeyPath, 1_048_576), ]); await dependencies.afterCapture?.(); let capturedLocalStatus: "PASS" | "FAIL" = "FAIL"; const archive = await verifyCapturedCiCandidateArchive( captured[0]!.bytes, input.expectedArchiveSha256, { verifyExtracted: async (extractionRoot, manifest) => { const candidate = await verifyReleaseCandidate(manifest, extractionRoot); if (candidate.failures.length > 0) { throw new Error(`captured candidate failed final verification: ${candidate.failures.join(", ")}`); } const local = await (dependencies.verifyLocalEvidence ?? verifyArchivedLocalEvidence)({ repositoryRoot: extractionRoot, candidate: manifest, }); if (local.status !== "PASS" || local.failures.length > 0) { throw new Error(`captured local evidence failed final verification: ${local.failures.join(", ")}`); } capturedLocalStatus = local.status; }, }, ); const vulnerability = vulnerabilityProviderReportSchema.parse(parseJson(captured[1]!.bytes)); const provenance = provenanceProviderAttestationSchema.parse(parseJson(captured[2]!.bytes)); const reevaluated = evaluatePromotionEvidence({ candidate: archive.manifest, currentDistSha256: archive.manifest.distSha256, localStatus: capturedLocalStatus, vulnerabilityReport: vulnerability, provenanceAttestation: provenance, vulnerabilityTrust: { keyId: input.vulnerabilityKeyId, publicKey: createPublicKey( new TextDecoder("utf-8", { fatal: true }).decode(vulnerabilityPublicKey), ), }, provenanceTrust: { keyId: input.provenanceKeyId, publicKey: createPublicKey( new TextDecoder("utf-8", { fatal: true }).decode(provenancePublicKey), ), }, }); if (reevaluated.status !== "PASS" || reevaluated.failures.length > 0) { throw new Error(`captured provider evidence failed trusted revalidation: ${reevaluated.failures.join(", ")}`); } const expectedBindings = { candidateArchiveSha256: captured[0]!.digest, vulnerabilityReportSha256: captured[1]!.digest, provenanceAttestationSha256: captured[2]!.digest, }; for (const [index, expectedArtifactType] of [ [3, "provider-verification"], [4, "promotion-verification"], ] as const) { const verification = providerVerificationArtifactSchema.parse(parseJson(captured[index]!.bytes)); if (verification.artifactType !== expectedArtifactType) { throw new Error( `${captured[index]!.destinationName} artifactType role mismatch: expected ${expectedArtifactType}`, ); } if ( verification.status !== reevaluated.status || verification.vulnerabilityStatus !== reevaluated.vulnerabilityStatus || verification.provenanceAttestationStatus !== reevaluated.provenanceAttestationStatus || verification.failures.length > 0 ) { throw new Error(`${captured[index]!.destinationName} status disagrees with trusted revalidation`); } if (verification.lockfileSha256 !== archive.manifest.lockfileSha256) { throw new Error(`${captured[index]!.destinationName} lockfileSha256 digest mismatch`); } if (verification.distSha256 !== archive.manifest.distSha256) { throw new Error(`${captured[index]!.destinationName} distSha256 digest mismatch`); } for (const [binding, expectedDigest] of Object.entries(expectedBindings) as ReadonlyArray< readonly [keyof typeof expectedBindings, string] >) { if (verification[binding] !== expectedDigest) { throw new Error(`${captured[index]!.destinationName} ${binding} digest mismatch`); } } } const stagedFiles: readonly StagedFile[] = captured; const releaseRoot = path.join(root, ".release"); const releaseIdentity = await ensureSafePublishDirectory(root, releaseRoot); const stagingRoot = path.join(releaseRoot, "promoted-staging"); await assertSafePublishLeaf(stagingRoot, ".release/promoted-staging"); if (await exists(stagingRoot)) throw new Error("promotion staging target already exists"); const temporary = await mkdtemp(path.join(root, `.promoted-staging.${randomUUID()}.`)); let ownsTemporary = true; try { for (const source of stagedFiles) { const handle = await open( path.join(temporary, source.destinationName), constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600, ); try { await handle.writeFile(source.bytes); await handle.sync(); } finally { await handle.close(); } } await syncDirectory(temporary); await dependencies.beforePublishRename?.(); const currentReleaseIdentity = await ensureSafePublishDirectory(root, releaseRoot); if ( releaseIdentity.dev <= 0 || releaseIdentity.ino <= 0 || currentReleaseIdentity.dev !== releaseIdentity.dev || currentReleaseIdentity.ino !== releaseIdentity.ino ) { throw new Error("promotion staging parent identity changed"); } await assertSafePublishLeaf(stagingRoot, ".release/promoted-staging"); if (await exists(stagingRoot)) throw new Error("promotion staging target already exists"); await rename(temporary, stagingRoot); ownsTemporary = false; await syncDirectory(releaseRoot); } finally { if (ownsTemporary) await rm(temporary, { recursive: true, force: true }); } return Object.freeze( stagedFiles.map(({ destinationName, digest }) => Object.freeze({ path: `.release/promoted-staging/${destinationName}`, sha256: digest }), ), ); } async function capture(root: string, configuredPath: string, maxBytes: number): Promise { const absolute = path.resolve(root, configuredPath); const relative = path.relative(root, absolute); const outside = relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative); return readBoundedRegularFile({ root: outside ? path.dirname(absolute) : root, relativePath: outside ? path.basename(absolute) : relative.replaceAll(path.sep, "/"), maxBytes, }); } function parseJson(bytes: Buffer): unknown { return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) as unknown; } function repositoryRelative(root: string, configuredPath: string): string { const absolute = path.resolve(root, configuredPath); const relative = path.relative(root, absolute); if (relative === "" || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) { throw new TypeError(`promotion source escapes repository: ${configuredPath}`); } return relative.replaceAll(path.sep, "/"); } function sha256(bytes: Buffer): string { return createHash("sha256").update(bytes).digest("hex"); } async function exists(target: string): Promise { try { await lstat(target); return true; } catch (error) { if (hasErrorCode(error, "ENOENT")) return false; throw error; } } async function syncDirectory(directory: string): Promise { const handle = await open(directory, constants.O_RDONLY); try { try { await handle.sync(); } catch (error) { if (!hasErrorCode(error, "EINVAL") && !hasErrorCode(error, "ENOTSUP")) throw error; } } finally { await handle.close(); } } function hasErrorCode(error: unknown, code: string): boolean { return Boolean(error && typeof error === "object" && "code" in error && error.code === code); }