import { createHash, createPublicKey, randomBytes as cryptoRandomBytes, } from "node:crypto"; import { constants } from "node:fs"; import { lstat, mkdir, open, readdir, rm, rmdir, stat, } from "node:fs/promises"; import path from "node:path"; import { PROMOTED_FILE_NAMES, type PromotedFileName, } from "../contracts/promotion-artifacts.ts"; import { evaluatePromotionEvidence, assertDistinctProviderTrust, providerPublicKeyFingerprint, providerVerificationArtifactSchema, PROMOTION_VERIFIER_ID, PROMOTION_VERIFIER_VERSION, provenanceProviderAttestationSchema, trustPolicySha256, vulnerabilityProviderReportSchema, type ProviderTrust, } from "./provider-evidence.ts"; import { verifyExactPromotionBundle } from "./exact-promotion-bundle.ts"; import { captureCiCandidateArchive, withVerifiedCapturedCandidate, } from "./ci-candidate-archive.ts"; import { verifyArchivedLocalEvidence } from "./local-release-evidence.ts"; import { readBoundedRegularFile } from "./ci-artifact-validator.ts"; export type StagedFile = Readonly<{ name: PromotedFileName; bytes: Buffer; sha256: string; }>; export type FinalizedPromotion = Readonly<{ stagingRoot: string; cleanupToken: string; runnerTempIdentity: Readonly<{ dev: number; ino: number }>; stagingIdentity: Readonly<{ dev: number; ino: number }>; files: readonly Readonly<{ name: PromotedFileName; sha256: string }>[]; }>; export async function finalizeVerifiedPromotion(input: Readonly<{ repositoryRoot: string; archivePath: string; expectedArchiveSha256: string; vulnerabilityReportPath: string; provenanceAttestationPath: string; vulnerabilityPublicKeyPath: string; vulnerabilityKeyId: string; provenancePublicKeyPath: string; provenanceKeyId: string; expectedRun: Readonly<{ id: string; attempt: number; sourceRevision: string }>; vulnerabilityInvocationNonce: string; provenanceInvocationNonce: string; runnerTempRoot: string; }>, dependencies: Readonly<{ captureArchive?: typeof captureCiCandidateArchive; nowEpochMs?: () => number; randomBytes?: (bytes: number) => Buffer; afterCapture?: () => Promise; beforePublish?: () => Promise; afterStagingWrite?: () => Promise; afterFileWrite?: (name: PromotedFileName) => Promise; beforeSeal?: () => Promise; afterMkdirBeforeOpen?: (stagingRoot: string) => Promise; }> = {}): Promise { const root = path.resolve(input.repositoryRoot); const capturedArchive = await (dependencies.captureArchive ?? captureCiCandidateArchive)({ archivePath: input.archivePath, expectedSha256: input.expectedArchiveSha256, }); const [vulnerabilityBytes, provenanceBytes, vulnerabilityKeyBytes, provenanceKeyBytes] = await Promise.all([ capture(root, input.vulnerabilityReportPath, 16_777_216), capture(root, input.provenanceAttestationPath, 16_777_216), capture(root, input.vulnerabilityPublicKeyPath, 1_048_576), capture(root, input.provenancePublicKeyPath, 1_048_576), ]); await dependencies.afterCapture?.(); const vulnerabilityTrust = capturedTrust( input.vulnerabilityKeyId, vulnerabilityKeyBytes, ); const provenanceTrust = capturedTrust( input.provenanceKeyId, provenanceKeyBytes, ); assertDistinctProviderTrust({ vulnerabilityTrust, provenanceTrust }); const vulnerabilityReport = vulnerabilityProviderReportSchema.parse( parseJson(vulnerabilityBytes), ); const provenanceAttestation = provenanceProviderAttestationSchema.parse( parseJson(provenanceBytes), ); const nowEpochMs = dependencies.nowEpochMs ?? Date.now; const generated = await withVerifiedCapturedCandidate({ captured: capturedArchive, verify: async ({ extractionRoot, manifest }) => { const local = await verifyArchivedLocalEvidence({ extractionRoot, expectedManifest: manifest, }); if (local.status !== "PASS" || !local.identity) { throw new Error( `captured local evidence failed final verification: ${local.failures.join(", ")}`, ); } if (local.identity.sourceRevision !== input.expectedRun.sourceRevision) { throw new Error("captured source revision differs from expected promotion revision"); } const expected = { run: { id: input.expectedRun.id, attempt: input.expectedRun.attempt }, source: { revision: local.identity.sourceRevision, sourceSetSha256: local.identity.sourceSetSha256, }, candidate: { archiveSha256: capturedArchive.archiveSha256, bundleSha256: manifest.bundleSha256, distSha256: manifest.distSha256, lockfileSha256: manifest.lockfileSha256, }, secretScanAttestation: { status: "PASS" as const, localEvidenceAssessmentSha256: local.identity.assessmentSha256, sourceSetSha256: local.identity.sourceSetSha256, policySha256: local.identity.secretScan.policySha256, sarifSha256: local.identity.secretScan.sarifSha256, scanInputSha256: local.identity.secretScan.scanInputSha256, }, vulnerabilityInvocationNonce: input.vulnerabilityInvocationNonce, provenanceInvocationNonce: input.provenanceInvocationNonce, } as const; const reevaluated = evaluatePromotionEvidence({ expected, localStatus: local.status, vulnerabilityReport, provenanceAttestation, vulnerabilityTrust, provenanceTrust, nowEpochMs, }); if (reevaluated.status !== "PASS") { throw new Error( `captured provider evidence failed trusted revalidation: ${reevaluated.failures.join(", ")}`, ); } const providerEvidence = { vulnerabilityReportSha256: sha256(vulnerabilityBytes), provenanceAttestationSha256: sha256(provenanceBytes), vulnerabilityInvocationNonce: input.vulnerabilityInvocationNonce, provenanceInvocationNonce: input.provenanceInvocationNonce, vulnerabilityKeyId: vulnerabilityTrust.keyId, vulnerabilityKeyFingerprint: vulnerabilityTrust.publicKeyFingerprint, provenanceKeyId: provenanceTrust.keyId, provenanceKeyFingerprint: provenanceTrust.publicKeyFingerprint, secretScanAttestation: expected.secretScanAttestation, } as const; const trustDigest = trustPolicySha256({ vulnerabilityTrust, provenanceTrust }); const verifiedAt = new Date(nowEpochMs()).toISOString(); const common = { schemaVersion: 3 as const, verifiedAt, status: "PASS" as const, verifier: { id: PROMOTION_VERIFIER_ID, version: PROMOTION_VERIFIER_VERSION, }, run: expected.run, source: expected.source, candidate: expected.candidate, providerEvidence, trustPolicySha256: trustDigest, failures: [] as const, }; const providerRecord = providerVerificationArtifactSchema.parse({ ...common, artifactType: "provider-verification", vulnerabilityStatus: reevaluated.vulnerabilityStatus, provenanceAttestationStatus: reevaluated.provenanceAttestationStatus, }); const providerRecordBytes = canonicalJsonBytes(providerRecord); const promotionRecord = providerVerificationArtifactSchema.parse({ ...common, artifactType: "promotion-verification", localEvidenceStatus: local.status, localEvidenceAssessmentSha256: local.identity.assessmentSha256, providerVerificationSha256: sha256(providerRecordBytes), }); return Object.freeze({ providerRecordBytes, promotionRecordBytes: canonicalJsonBytes(promotionRecord), exactExpected: Object.freeze({ run: expected.run, sourceRevision: expected.source.revision, sourceSetSha256: expected.source.sourceSetSha256, archiveSha256: expected.candidate.archiveSha256, bundleSha256: expected.candidate.bundleSha256, distSha256: expected.candidate.distSha256, lockfileSha256: expected.candidate.lockfileSha256, }), }); }, }); const stagedFiles: readonly StagedFile[] = Object.freeze([ staged("release-candidate.tar.gz", capturedArchive.bytes), staged("vulnerability-report.json", vulnerabilityBytes), staged("provenance-attestation.json", provenanceBytes), staged("provider-verification.json", generated.providerRecordBytes), staged("promotion-verification.json", generated.promotionRecordBytes), ]); if ( JSON.stringify(stagedFiles.map(({ name }) => name)) !== JSON.stringify(PROMOTED_FILE_NAMES) ) { throw new Error("promotion exact-five canonical file order drift"); } await dependencies.beforePublish?.(); await verifyExactPromotionBundle( Object.fromEntries(stagedFiles.map(({ name, bytes }) => [name, bytes])), { vulnerabilityTrust, provenanceTrust, expected: generated.exactExpected, nowEpochMs, }, ); return publishPrivatePromotionStaging( input.runnerTempRoot, input.expectedRun, stagedFiles, dependencies.randomBytes ?? cryptoRandomBytes, dependencies.afterStagingWrite, dependencies.afterFileWrite, async (capturedFiles) => { await verifyExactPromotionBundle( capturedFiles, { vulnerabilityTrust, provenanceTrust, expected: generated.exactExpected, nowEpochMs, }, ); }, dependencies.afterMkdirBeforeOpen, dependencies.beforeSeal, ); } export const stageVerifiedPromotion = finalizeVerifiedPromotion; export async function cleanupFinalizedPromotion(input: Readonly<{ runnerTempRoot: string; stagingRoot: string; cleanupToken: string; runnerTempIdentity: Readonly<{ dev: number; ino: number }>; stagingIdentity: Readonly<{ dev: number; ino: number }>; }>, dependencies: Readonly<{ beforeRemove?: () => Promise; }> = {}): Promise { const parent = path.resolve(input.runnerTempRoot); const expected = path.join(parent, input.cleanupToken); if ( !/^[A-Za-z0-9._-]+-[a-f0-9]{32}$/u.test(input.cleanupToken) || path.resolve(input.stagingRoot) !== expected || !Number.isSafeInteger(input.runnerTempIdentity.dev) || input.runnerTempIdentity.dev <= 0 || !Number.isSafeInteger(input.runnerTempIdentity.ino) || input.runnerTempIdentity.ino <= 0 || !Number.isSafeInteger(input.stagingIdentity.dev) || input.stagingIdentity.dev <= 0 || !Number.isSafeInteger(input.stagingIdentity.ino) || input.stagingIdentity.ino <= 0 ) { throw new TypeError("promotion cleanup root/token mismatch"); } const parentHandle = await open( parent, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW, ); try { const openedParent = await parentHandle.stat(); assertRunnerTempIdentity(openedParent, input.runnerTempIdentity); const descriptorRoot = `/proc/self/fd/${parentHandle.fd}`; const descriptorMetadata = await stat(descriptorRoot); if (!descriptorMetadata.isDirectory()) { throw new Error("descriptor-relative cleanup is unavailable"); } const descriptorExpected = path.join(descriptorRoot, input.cleanupToken); let metadata; try { metadata = await lstat(descriptorExpected); } catch (error) { if (hasErrorCode(error, "ENOENT")) return; throw error; } if (metadata.isSymbolicLink() || !metadata.isDirectory()) { throw new TypeError("promotion cleanup leaf is unsafe"); } assertStagingIdentity(metadata, input.stagingIdentity); const stagingHandle = await open( descriptorExpected, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW, ); try { const stagingDescriptorRoot = `/proc/self/fd/${stagingHandle.fd}`; assertStagingIdentity(await stat(stagingDescriptorRoot), input.stagingIdentity); const names = (await readdir(stagingDescriptorRoot)).sort(asciiCompare); if ( JSON.stringify(names) !== JSON.stringify([...PROMOTED_FILE_NAMES].sort(asciiCompare)) ) { throw new Error("promotion cleanup leaf does not contain the exact five files"); } await dependencies.beforeRemove?.(); const visibleParent = await lstat(parent); assertRunnerTempIdentity(visibleParent, input.runnerTempIdentity); for (const name of PROMOTED_FILE_NAMES) { await rm(path.join(stagingDescriptorRoot, name), { force: false }); } assertStagingIdentity(await stat(stagingDescriptorRoot), input.stagingIdentity); assertStagingIdentity(await lstat(descriptorExpected), input.stagingIdentity); await rmdir(descriptorExpected); } finally { await stagingHandle.close(); } const afterParent = await lstat(parent); assertRunnerTempIdentity(afterParent, input.runnerTempIdentity); } finally { await parentHandle.close(); } } export async function publishPrivatePromotionStaging( runnerTempRoot: string, run: Readonly<{ id: string; attempt: number }>, files: readonly StagedFile[], randomBytes: (bytes: number) => Buffer, afterStagingWrite?: () => Promise, afterFileWrite?: (name: PromotedFileName) => Promise, sealStagedFiles?: (files: Readonly>) => Promise, afterMkdirBeforeOpen?: (stagingRoot: string) => Promise, beforeSeal?: () => Promise, ): Promise { if ( JSON.stringify(files.map(({ name }) => name)) !== JSON.stringify(PROMOTED_FILE_NAMES) || files.some( ({ bytes, sha256: digest }) => !Buffer.isBuffer(bytes) || !/^[a-f0-9]{64}$/u.test(digest) || sha256(bytes) !== digest, ) ) { throw new TypeError("private promotion staging requires the canonical exact-five bytes"); } const parentPath = path.resolve(runnerTempRoot); const before = await lstat(parentPath); if (!before.isDirectory() || before.isSymbolicLink()) { throw new TypeError("runner temporary root must be a real directory"); } const parentHandle = await open( parentPath, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW, ); const tokenBytes = randomBytes(16); if (tokenBytes.byteLength !== 16) { await parentHandle.close(); throw new TypeError("promotion staging nonce must contain exactly 128 random bits"); } const safeRun = run.id.replaceAll(/[^A-Za-z0-9._-]/gu, "_").slice(0, 64) || "run"; const cleanupToken = `promotion-${safeRun}-${run.attempt}-${tokenBytes.toString("hex")}`; const descriptorRoot = `/proc/self/fd/${parentHandle.fd}`; const descriptorStaging = path.join(descriptorRoot, cleanupToken); const visibleStaging = path.join(parentPath, cleanupToken); let ownsStaging = false; let stagingHandle: Awaited> | undefined; let createdStagingIdentity: Readonly<{ dev: number; ino: number }> | undefined; let stagingIdentity: Readonly<{ dev: number; ino: number }> | undefined; let openedIdentityVerified = false; const cleanup = async (primaryFailure?: unknown): Promise => { const cleanupFailures: unknown[] = []; const attemptCleanup = async (operation: () => Promise): Promise => { try { await operation(); } catch (error) { cleanupFailures.push(error); } }; if (ownsStaging && openedIdentityVerified && stagingHandle && stagingIdentity) { const ownedIdentity = stagingIdentity; const stagingDescriptorRoot = `/proc/self/fd/${stagingHandle.fd}`; const removals = await Promise.allSettled( files.map(({ name }) => rm(path.join(stagingDescriptorRoot, name), { force: true })), ); cleanupFailures.push( ...removals.flatMap((result) => result.status === "rejected" ? [result.reason] : [], ), ); await attemptCleanup(async () => { let visible; try { visible = await lstat(descriptorStaging); } catch (error) { if (hasErrorCode(error, "ENOENT")) return; throw error; } if ( visible.isDirectory() && !visible.isSymbolicLink() && visible.dev === ownedIdentity.dev && visible.ino === ownedIdentity.ino ) { await rmdir(descriptorStaging); } }); } if (stagingHandle) { const ownedHandle = stagingHandle; await attemptCleanup(async () => ownedHandle.close()); } await attemptCleanup(async () => parentHandle.close()); if (cleanupFailures.length > 0) { throw new AggregateError( primaryFailure === undefined ? cleanupFailures : [primaryFailure, ...cleanupFailures], primaryFailure instanceof Error ? `${primaryFailure.message}; promotion staging cleanup also failed` : "promotion staging cleanup failed", { cause: cleanupFailures.at(-1) }, ); } }; let finalizedPromotion: FinalizedPromotion; try { const procMetadata = await stat(descriptorRoot); if (!procMetadata.isDirectory()) throw new Error("descriptor-relative staging is unavailable"); await mkdir(descriptorStaging, { mode: 0o700 }); ownsStaging = true; const createdStaging = await lstat(descriptorStaging); if (!createdStaging.isDirectory() || createdStaging.isSymbolicLink()) { throw new Error("created promotion staging leaf is unsafe"); } createdStagingIdentity = Object.freeze({ dev: createdStaging.dev, ino: createdStaging.ino, }); await afterMkdirBeforeOpen?.(visibleStaging); const openedHandle = await open( descriptorStaging, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW, ); let openedStaging; try { openedStaging = await openedHandle.stat(); if (!openedStaging.isDirectory()) { throw new Error("promotion staging descriptor is not a directory"); } if ( openedStaging.dev !== createdStagingIdentity.dev || openedStaging.ino !== createdStagingIdentity.ino ) { throw new Error("promotion staging leaf identity changed between mkdir and open"); } openedIdentityVerified = true; } catch (error) { try { await openedHandle.close(); } catch (closeError) { throw new AggregateError( [error, closeError], error instanceof Error ? `${error.message}; rejected staging descriptor close also failed` : "rejected staging descriptor and close both failed", { cause: closeError }, ); } throw error; } stagingHandle = openedHandle; await stagingHandle.chmod(0o700); stagingIdentity = Object.freeze({ dev: openedStaging.dev, ino: openedStaging.ino }); const stagingDescriptorRoot = `/proc/self/fd/${stagingHandle.fd}`; assertStagingIdentity(await stat(stagingDescriptorRoot), stagingIdentity); for (const file of files) { const handle = await open( path.join(stagingDescriptorRoot, file.name), constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o400, ); try { await handle.chmod(0o400); await handle.writeFile(file.bytes); await handle.sync(); } finally { await handle.close(); } await afterFileWrite?.(file.name); } await syncHandle(stagingHandle); await syncHandle(parentHandle); await afterStagingWrite?.(); await beforeSeal?.(); const capturedFiles = await captureStagedFiles(stagingHandle, files); await sealStagedFiles?.(capturedFiles); const after = await lstat(parentPath); if ( after.dev !== before.dev || after.ino !== before.ino || after.isSymbolicLink() || !after.isDirectory() ) { throw new Error("runner temporary parent identity changed during staging"); } const visible = await lstat(visibleStaging); if (!visible.isDirectory() || visible.isSymbolicLink()) { throw new Error("promotion staging visibility identity mismatch"); } assertStagingIdentity(visible, stagingIdentity); ownsStaging = false; finalizedPromotion = Object.freeze({ stagingRoot: visibleStaging, cleanupToken, runnerTempIdentity: Object.freeze({ dev: before.dev, ino: before.ino }), stagingIdentity, files: Object.freeze( files.map(({ name, sha256: digest }) => Object.freeze({ name, sha256: digest })), ), }); } catch (error) { await cleanup(error); throw error; } await cleanup(); return finalizedPromotion; } async function captureStagedFiles( stagingHandle: Awaited>, declaredFiles: readonly StagedFile[], ): Promise>> { const descriptorRoot = `/proc/self/fd/${stagingHandle.fd}`; const names = (await readdir(descriptorRoot)).sort(asciiCompare); if ( JSON.stringify(names) !== JSON.stringify([...PROMOTED_FILE_NAMES].sort(asciiCompare)) ) { throw new Error("staged promotion seal requires exactly the canonical five files"); } const declared = new Map(declaredFiles.map((file) => [file.name, file] as const)); const captured = {} as Record; for (const name of PROMOTED_FILE_NAMES) { const expected = declared.get(name)!; const handle = await open( path.join(descriptorRoot, name), constants.O_RDONLY | constants.O_NOFOLLOW, ); try { const before = await handle.stat(); const maxBytes = name === "release-candidate.tar.gz" ? 268_435_456 : 16_777_216; if ( !before.isFile() || before.nlink !== 1 || (before.mode & 0o777) !== 0o400 || before.size <= 0 || before.size > maxBytes ) { throw new Error( `staged promotion file must be regular, single-link, bounded, and mode 0400: ${name}`, ); } const bytes = await handle.readFile(); const after = await handle.stat(); if ( after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size || after.nlink !== 1 || (after.mode & 0o777) !== 0o400 || bytes.byteLength !== before.size ) { throw new Error(`staged promotion file inode or size changed during seal: ${name}`); } if (sha256(bytes) !== expected.sha256) { throw new Error(`staged promotion file digest mismatch during seal: ${name}`); } captured[name] = bytes; } finally { await handle.close(); } } return Object.freeze(captured); } function asciiCompare(left: string, right: string): number { return left < right ? -1 : left > right ? 1 : 0; } function assertStagingIdentity( metadata: Readonly<{ dev: number; ino: number; isDirectory: () => boolean; isSymbolicLink?: () => boolean; }>, expected: Readonly<{ dev: number; ino: number }>, ): void { if ( metadata.dev !== expected.dev || metadata.ino !== expected.ino || !metadata.isDirectory() || metadata.isSymbolicLink?.() ) { throw new Error("promotion staging leaf identity changed"); } } function assertRunnerTempIdentity( metadata: Readonly<{ dev: number; ino: number; isDirectory: () => boolean; isSymbolicLink?: () => boolean }>, expected: Readonly<{ dev: number; ino: number }>, ): void { if ( metadata.dev !== expected.dev || metadata.ino !== expected.ino || !metadata.isDirectory() || metadata.isSymbolicLink?.() ) { throw new Error("runner temporary parent identity changed during cleanup"); } } function capturedTrust(keyId: string, bytes: Buffer): ProviderTrust { const publicKey = createPublicKey( new TextDecoder("utf-8", { fatal: true }).decode(bytes), ); return Object.freeze({ keyId, publicKey, publicKeyFingerprint: providerPublicKeyFingerprint(publicKey), }); } 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 staged(name: PromotedFileName, bytes: Buffer): StagedFile { return Object.freeze({ name, bytes, sha256: sha256(bytes) }); } function canonicalJsonBytes(value: unknown): Buffer { return Buffer.from(`${JSON.stringify(value, null, 2)}\n`, "utf8"); } function parseJson(bytes: Buffer): unknown { try { return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) as unknown; } catch { throw new TypeError("captured provider evidence is not valid UTF-8 JSON"); } } function sha256(bytes: Buffer): string { return createHash("sha256").update(bytes).digest("hex"); } async function syncHandle(handle: Awaited>): Promise { try { await handle.sync(); } catch (error) { if (!hasErrorCode(error, "EINVAL") && !hasErrorCode(error, "ENOTSUP")) throw error; } } function hasErrorCode(error: unknown, code: string): boolean { return Boolean(error && typeof error === "object" && "code" in error && error.code === code); }