import { generateKeyPairSync } from "node:crypto"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { EventEmitter } from "node:events"; import { describe, expect, it } from "vitest"; import { evaluatePromotionEvidence, providerPublicKeyFingerprint, validateProviderEvidence, } from "../../scripts/lib/provider-evidence.ts"; import { readProviderTrust } from "../../scripts/lib/provider-trust.ts"; import { superviseProviderEvidence } from "../../scripts/lib/provider-supervisor.ts"; import { runProviderProcess } from "../../scripts/lib/provider-process-runner.ts"; import { runStageVerifiedPromotionCli } from "../../scripts/lib/stage-verified-promotion-cli.ts"; import type { ReleaseCandidateManifest } from "../../scripts/lib/release-candidate.ts"; import { digest, providerExpectedContext, providerUnsigned, signedProviderV2, trust, } from "./security-followup-fixture.ts"; describe("security provider evidence contracts", () => { it("accepts signed provider v2 evidence only for the exact run, source, archive, and nonce", () => { const now = Date.parse("2026-08-02T01:00:00.000Z"); const vulnerabilityKeys = generateKeyPairSync("ed25519"); const provenanceKeys = generateKeyPairSync("ed25519"); const expected = providerExpectedContext(); const vulnerability = signedProviderV2( { source: expected.source, candidate: expected.candidate, schemaVersion: 2, evidenceType: "vulnerability-report", provider: "fixture-vulnerability", issuedAt: "2026-08-02T01:00:00.000Z", expiresAt: "2026-08-02T02:00:00.000Z", run: { ...expected.run, invocationNonce: "1".repeat(64) }, secretScanAttestation: expected.secretScanAttestation, findings: [], }, "vulnerability-key", vulnerabilityKeys.publicKey, vulnerabilityKeys.privateKey, ); const provenance = signedProviderV2( { source: expected.source, candidate: expected.candidate, schemaVersion: 2, evidenceType: "provenance-attestation", provider: "fixture-provenance", signer: "fixture-workload", issuedAt: "2026-08-02T01:00:00.000Z", expiresAt: "2026-08-02T02:00:00.000Z", run: { ...expected.run, invocationNonce: "2".repeat(64) }, subject: { name: "dist", digest: { sha256: expected.candidate.distSha256 } }, }, "provenance-key", provenanceKeys.publicKey, provenanceKeys.privateKey, ); const result = evaluatePromotionEvidence({ expected: { ...expected, vulnerabilityInvocationNonce: "1".repeat(64), provenanceInvocationNonce: "2".repeat(64), }, localStatus: "PASS", vulnerabilityReport: vulnerability, provenanceAttestation: provenance, vulnerabilityTrust: trust("vulnerability-key", vulnerabilityKeys.publicKey), provenanceTrust: trust("provenance-key", provenanceKeys.publicKey), nowEpochMs: () => now, }); expect(result).toEqual({ status: "PASS", vulnerabilityStatus: "PASS", provenanceAttestationStatus: "PASS", failures: [], }); const replayed = evaluatePromotionEvidence({ expected: { ...expected, run: { id: expected.run.id, attempt: 2 }, vulnerabilityInvocationNonce: "1".repeat(64), provenanceInvocationNonce: "2".repeat(64), }, localStatus: "PASS", vulnerabilityReport: vulnerability, provenanceAttestation: provenance, vulnerabilityTrust: trust("vulnerability-key", vulnerabilityKeys.publicKey), provenanceTrust: trust("provenance-key", provenanceKeys.publicKey), nowEpochMs: () => now, }); expect(replayed.status).toBe("FAIL_UNVERIFIED"); expect(replayed.failures).toEqual( expect.arrayContaining([ "vulnerability report run identity mismatch", "provenance attestation run identity mismatch", ]), ); }); it("rejects a signed vulnerability PASS when the captured SARIF attestation differs", () => { const keys = generateKeyPairSync("ed25519"); const expected = providerExpectedContext(); const secretScanAttestation = { status: "PASS" as const, localEvidenceAssessmentSha256: digest("assessment"), sourceSetSha256: expected.source.sourceSetSha256, policySha256: digest("secret policy"), sarifSha256: digest("real sarif"), scanInputSha256: digest("scan input"), }; const report = signedProviderV2( { schemaVersion: 2, evidenceType: "vulnerability-report", provider: "fixture-vulnerability", issuedAt: "2026-08-02T01:00:00.000Z", expiresAt: "2026-08-02T02:00:00.000Z", run: { ...expected.run, invocationNonce: "1".repeat(64) }, source: expected.source, candidate: expected.candidate, secretScanAttestation, findings: [], }, "vulnerability-key", keys.publicKey, keys.privateKey, ); const validated = validateProviderEvidence({ kind: "vulnerability", value: report, expected: { ...expected, vulnerabilityInvocationNonce: "1".repeat(64), provenanceInvocationNonce: "2".repeat(64), secretScanAttestation: { ...secretScanAttestation, sarifSha256: digest("forged empty sarif"), }, }, trust: trust("vulnerability-key", keys.publicKey), nowEpochMs: () => Date.parse("2026-08-02T01:00:00.000Z"), }); expect(validated.status).toBe("FAIL_UNVERIFIED"); expect(validated.failures).toContain( "vulnerability report secret scan attestation mismatch", ); const forged = structuredClone(report); forged.secretScanAttestation.sarifSha256 = digest("forged empty sarif"); const forgedValidation = validateProviderEvidence({ kind: "vulnerability", value: forged, expected: { ...expected, vulnerabilityInvocationNonce: "1".repeat(64), provenanceInvocationNonce: "2".repeat(64), secretScanAttestation: forged.secretScanAttestation, }, trust: trust("vulnerability-key", keys.publicKey), nowEpochMs: () => Date.parse("2026-08-02T01:00:00.000Z"), }); expect(forgedValidation.failures).toContain( "vulnerability report signature verification failed", ); const provenanceKeys = generateKeyPairSync("ed25519"); const provenance = signedProviderV2( { schemaVersion: 2, evidenceType: "provenance-attestation", provider: "fixture-provenance", signer: "fixture-workload", issuedAt: "2026-08-02T01:00:00.000Z", expiresAt: "2026-08-02T02:00:00.000Z", run: { ...expected.run, invocationNonce: "2".repeat(64) }, source: expected.source, candidate: expected.candidate, subject: { name: "dist", digest: { sha256: expected.candidate.distSha256 } }, }, "provenance-key", provenanceKeys.publicKey, provenanceKeys.privateKey, ); const evaluated = evaluatePromotionEvidence({ expected: { ...expected, vulnerabilityInvocationNonce: "1".repeat(64), provenanceInvocationNonce: "2".repeat(64), secretScanAttestation: { ...secretScanAttestation, sarifSha256: digest("forged empty sarif"), }, }, localStatus: "PASS", vulnerabilityReport: report, provenanceAttestation: provenance, vulnerabilityTrust: trust("vulnerability-key", keys.publicKey), provenanceTrust: trust("provenance-key", provenanceKeys.publicKey), nowEpochMs: () => Date.parse("2026-08-02T01:00:00.000Z"), }); expect(evaluated.vulnerabilityStatus).toBe("FAIL_UNVERIFIED"); expect(evaluated.failures).toContain( "vulnerability report secret scan attestation mismatch", ); }); it.each(["vulnerability", "provenance"] as const)( "rejects correctly re-signed %s v2 context/time/replay drift", (kind) => { const now = Date.parse("2026-08-02T01:00:00.000Z"); const vulnerabilityKeys = generateKeyPairSync("ed25519"); const provenanceKeys = generateKeyPairSync("ed25519"); const expected = providerExpectedContext(); const baseVulnerability = providerUnsigned("vulnerability", expected); const baseProvenance = providerUnsigned("provenance", expected); const validVulnerability = signedProviderV2( baseVulnerability, "vulnerability-key", vulnerabilityKeys.publicKey, vulnerabilityKeys.privateKey, ); const validProvenance = signedProviderV2( baseProvenance, "provenance-key", provenanceKeys.publicKey, provenanceKeys.privateKey, ); const rawCases: Array) => Record, RegExp, ]> = [ ["schema v1", (value) => ({ ...value, schemaVersion: 1 }), /missing or invalid/u], [ "evidence type", (value) => ({ ...value, evidenceType: kind === "vulnerability" ? "provenance-attestation" : "vulnerability-report", }), /missing or invalid/u, ], ...(["archiveSha256", "bundleSha256", "distSha256", "lockfileSha256"] as const).map( (field) => [ `candidate ${field}`, (value: Record) => ({ ...value, candidate: { ...value.candidate, [field]: "f".repeat(64) }, ...(kind === "provenance" && field === "distSha256" ? { subject: { name: "dist", digest: { sha256: "f".repeat(64) }, }, } : {}), }), /candidate identity|subject dist/u, ] as const, ), [ "different archive with same dist and lockfile", (value) => ({ ...value, candidate: { ...value.candidate, archiveSha256: "e".repeat(64) }, }), /candidate identity/u, ], [ "source revision", (value) => ({ ...value, source: { ...value.source, revision: "c".repeat(40) } }), /source identity/u, ], [ "source set", (value) => ({ ...value, source: { ...value.source, sourceSetSha256: "c".repeat(64) } }), /source identity/u, ], [ "run id", (value) => ({ ...value, run: { ...value.run, id: "other-run" } }), /run identity/u, ], [ "run attempt replay", (value) => ({ ...value, run: { ...value.run, attempt: 2 } }), /run identity/u, ], [ "different nonce", (value) => ({ ...value, run: { ...value.run, invocationNonce: "3".repeat(64) } }), /invocation nonce/u, ], [ "missing nonce", (value) => { const run = { ...value.run }; delete run.invocationNonce; return { ...value, run }; }, /missing or invalid/u, ], [ "uppercase nonce", (value) => ({ ...value, run: { ...value.run, invocationNonce: "A".repeat(64) } }), /missing or invalid/u, ], [ "short nonce", (value) => ({ ...value, run: { ...value.run, invocationNonce: "1".repeat(62) } }), /missing or invalid/u, ], [ "issued future boundary", (value) => ({ ...value, issuedAt: "2026-08-02T01:05:00.001Z" }), /future skew/u, ], [ "expiry equality", (value) => ({ ...value, expiresAt: "2026-08-02T01:00:00.000Z" }), /expired/u, ], [ "expiry past", (value) => ({ ...value, expiresAt: "2026-08-02T00:59:59.999Z" }), /expired/u, ], [ "zero lifetime", (value) => ({ ...value, issuedAt: "2026-08-02T01:01:00.000Z", expiresAt: "2026-08-02T01:01:00.000Z", }), /not positive/u, ], [ "negative lifetime", (value) => ({ ...value, issuedAt: "2026-08-02T01:02:00.000Z", expiresAt: "2026-08-02T01:01:59.999Z", }), /not positive/u, ], [ "lifetime above two hours", (value) => ({ ...value, issuedAt: "2026-08-02T01:00:00.000Z", expiresAt: "2026-08-02T03:00:00.001Z", }), /exceeds two hours/u, ], [ "wrong fingerprint", (value) => ({ ...value, signature: { ...value.signature, publicKeyFingerprint: `sha256:${"d".repeat(64)}`, }, }), /trust identity/u, ], ]; const cases = rawCases.map(([name, mutate, failure]) => ({ name, mutate, failure, })); for (const testCase of cases) { const base = kind === "vulnerability" ? baseVulnerability : baseProvenance; const mutated = testCase.mutate(structuredClone(base)); const resigned = signedProviderV2( mutated, kind === "vulnerability" ? "vulnerability-key" : "provenance-key", kind === "vulnerability" ? vulnerabilityKeys.publicKey : provenanceKeys.publicKey, kind === "vulnerability" ? vulnerabilityKeys.privateKey : provenanceKeys.privateKey, "signature" in mutated && mutated.signature?.publicKeyFingerprint ? mutated.signature.publicKeyFingerprint : undefined, ); const result = evaluatePromotionEvidence({ expected: { ...expected, vulnerabilityInvocationNonce: "1".repeat(64), provenanceInvocationNonce: "2".repeat(64), }, localStatus: "PASS", vulnerabilityReport: kind === "vulnerability" ? resigned : validVulnerability, provenanceAttestation: kind === "provenance" ? resigned : validProvenance, vulnerabilityTrust: trust("vulnerability-key", vulnerabilityKeys.publicKey), provenanceTrust: trust("provenance-key", provenanceKeys.publicKey), nowEpochMs: () => now, }); expect(result.status, testCase.name).toBe("FAIL_UNVERIFIED"); expect(result.failures.join("\n"), testCase.name).toMatch(testCase.failure); } }, ); it("canonicalizes provider fingerprints from DER SPKI across PEM wrapping and rejects Ed448", async () => { const root = await mkdtemp(path.join(tmpdir(), "provider-fingerprint-")); try { const ed25519 = generateKeyPairSync("ed25519").publicKey; const pem = ed25519.export({ type: "spki", format: "pem" }).toString(); const body = pem.replace(/-----[^-]+-----|\s/gu, ""); const wrapped = (width: number) => `-----BEGIN PUBLIC KEY-----\n${body.match(new RegExp(`.{1,${width}}`, "gu"))!.join("\n")}\n-----END PUBLIC KEY-----\n`; await writeFile(path.join(root, "a.pem"), wrapped(64)); await writeFile(path.join(root, "b.pem"), wrapped(32)); const first = await readProviderTrust(root, "a.pem", "fixture-key"); const second = await readProviderTrust(root, "b.pem", "fixture-key"); expect(first?.publicKeyFingerprint).toBe(providerPublicKeyFingerprint(ed25519)); expect(second?.publicKeyFingerprint).toBe(first?.publicKeyFingerprint); const ed448 = generateKeyPairSync("ed448").publicKey; await writeFile(root + "/ed448.pem", ed448.export({ type: "spki", format: "pem" })); await expect(readProviderTrust(root, "ed448.pem", "fixture-key")).resolves.toBeNull(); expect(() => providerPublicKeyFingerprint(ed448)).toThrow(/must be Ed25519/u); } finally { await rm(root, { recursive: true, force: true }); } }); it("captures the downloaded archive pathname exactly once in the provider supervisor", async () => { const keys = generateKeyPairSync("ed25519"); const expected = providerExpectedContext(); let captureCount = 0; let receivedEnvironment: Readonly> | undefined; const manifest: ReleaseCandidateManifest = { schemaVersion: 1, distSha256: expected.candidate.distSha256, lockfileSha256: expected.candidate.lockfileSha256, bundleSha256: expected.candidate.bundleSha256, files: [{ path: "pnpm-lock.yaml", bytes: 1, sha256: expected.candidate.lockfileSha256 }], }; const result = await superviseProviderEvidence( { kind: "vulnerability", archivePath: "/downloads/candidate.tar.gz", expectedArchiveSha256: expected.candidate.archiveSha256, expectedRun: { id: expected.run.id, attempt: expected.run.attempt, sourceRevision: expected.source.revision, }, trust: trust("vulnerability-key", keys.publicKey), executeProvider: async ({ environment }) => { receivedEnvironment = environment; }, captureReport: async () => Buffer.from("{}\n"), }, { captureArchive: async (input) => { captureCount += 1; expect(input).toEqual({ archivePath: "/downloads/candidate.tar.gz", expectedSha256: expected.candidate.archiveSha256, }); return { bytes: Buffer.from("captured archive"), archiveSha256: expected.candidate.archiveSha256, }; }, withVerifiedCandidate: (async (input: any) => input.verify({ extractionRoot: "/captured/extraction", manifest })) as any, verifyLocalEvidence: async () => ({ status: "PASS", identity: { sourceRevision: expected.source.revision, sourceSetSha256: expected.source.sourceSetSha256, assessmentSha256: digest("assessment"), secretScan: { policySha256: digest("provider secret policy"), sarifSha256: digest("provider secret sarif"), scanInputSha256: digest("provider secret input"), }, }, failures: [], }), validateUpload: (async (input: any) => { expect("archivePath" in input).toBe(false); return { sealed: true }; }) as any, randomBytes: () => Buffer.alloc(32, 0x11), nowEpochMs: () => Date.parse("2026-08-02T01:00:00.000Z"), }, ); expect(captureCount).toBe(1); expect(receivedEnvironment).toEqual( expect.objectContaining({ PROVIDER_EVIDENCE_SCHEMA_VERSION: "2", PROVIDER_INVOCATION_NONCE: "11".repeat(32), PROVIDER_ISSUED_AT: "2026-08-02T01:00:00.000Z", PROVIDER_EXPIRES_AT: "2026-08-02T02:00:00.000Z", CI_RUN_ID: expected.run.id, CI_RUN_ATTEMPT: "1", SOURCE_REVISION: expected.source.revision, CANDIDATE_ARCHIVE_SHA256: expected.candidate.archiveSha256, }), ); expect(result.evidence).toEqual({ sealed: true }); }); it("samples provider freshness after report capture instead of reusing issuance time", async () => { const keys = generateKeyPairSync("ed25519"); const expected = providerExpectedContext(); const manifest: ReleaseCandidateManifest = { schemaVersion: 1, distSha256: expected.candidate.distSha256, lockfileSha256: expected.candidate.lockfileSha256, bundleSha256: expected.candidate.bundleSha256, files: [{ path: "pnpm-lock.yaml", bytes: 1, sha256: expected.candidate.lockfileSha256 }], }; const issuedSample = Date.parse("2026-08-02T01:00:00.000Z"); const validationSample = Date.parse("2026-08-02T02:00:00.001Z"); const samples = [issuedSample, validationSample]; let issuedAt = ""; await expect( superviseProviderEvidence( { kind: "vulnerability", archivePath: "/downloads/candidate.tar.gz", expectedArchiveSha256: expected.candidate.archiveSha256, expectedRun: { id: expected.run.id, attempt: expected.run.attempt, sourceRevision: expected.source.revision, }, trust: trust("vulnerability-key", keys.publicKey), executeProvider: async ({ environment }) => { issuedAt = environment.PROVIDER_ISSUED_AT!; }, captureReport: async () => Buffer.from("{}\n"), }, { captureArchive: async () => ({ bytes: Buffer.from("captured archive"), archiveSha256: expected.candidate.archiveSha256, }), withVerifiedCandidate: (async (input: any) => input.verify({ extractionRoot: "/captured/extraction", manifest })) as any, verifyLocalEvidence: async () => ({ status: "PASS", identity: { sourceRevision: expected.source.revision, sourceSetSha256: expected.source.sourceSetSha256, assessmentSha256: digest("assessment"), secretScan: { policySha256: digest("provider secret policy"), sarifSha256: digest("provider secret sarif"), scanInputSha256: digest("provider secret input"), }, }, failures: [], }), validateUpload: (async (input: any) => { expect(input.nowEpochMs()).toBe(validationSample); throw new Error("provider report expired during execution"); }) as any, randomBytes: () => Buffer.alloc(32, 0x33), nowEpochMs: () => samples.shift()!, }, ), ).rejects.toThrow(/expired during execution/u); expect(issuedAt).toBe("2026-08-02T01:00:00.000Z"); }); it("kills a timed-out provider but settles only after the child closes", async () => { const child = new EventEmitter() as EventEmitter & { kill(signal: NodeJS.Signals): boolean; }; let killedWith: NodeJS.Signals | undefined; child.kill = (signal) => { killedWith = signal; return true; }; let fireTimeout: (() => void) | undefined; let settled = false; const running = runProviderProcess( { executable: "/usr/bin/bwrap", arguments: [], environment: {}, timeoutMs: 1 }, { spawnChild: () => child as any, setTimer: (callback) => { fireTimeout = callback; return 1 as any; }, clearTimer: () => undefined, }, ).finally(() => { settled = true; }); fireTimeout?.(); await Promise.resolve(); expect(killedWith).toBe("SIGKILL"); expect(settled).toBe(false); child.emit("close", null, "SIGKILL"); await expect(running).rejects.toThrow(/timed out/u); expect(settled).toBe(true); }); it("captures process-group kill errors, attempts child fallback, and settles after close", async () => { const child = new EventEmitter() as EventEmitter & { pid: number; kill(signal: NodeJS.Signals): boolean; }; child.pid = 12_346; let fallbackSignal: NodeJS.Signals | undefined; child.kill = (signal) => { fallbackSignal = signal; return true; }; let fireTimeout: (() => void) | undefined; const running = runProviderProcess( { executable: "/usr/bin/bwrap", arguments: [], environment: {}, timeoutMs: 1 }, { spawnChild: () => child as any, setTimer: (callback) => { fireTimeout = callback; return 1 as any; }, clearTimer: () => undefined, killProcessGroup: () => { throw Object.assign(new Error("group kill denied"), { code: "EPERM" }); }, }, ); expect(() => fireTimeout?.()).not.toThrow(); expect(fallbackSignal).toBe("SIGKILL"); child.emit("close", null, "SIGKILL"); await expect(running).rejects.toThrow(/timed out.*kill failed.*close/u); }); it.each(["open failure", "partial write failure"])( "cleans finalized staging from memory when GITHUB_OUTPUT has a %s", async (failureKind) => { const finalized = { stagingRoot: "/runner/promotion-run-1-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", cleanupToken: "promotion-run-1-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", runnerTempIdentity: { dev: 10, ino: 20 }, stagingIdentity: { dev: 30, ino: 40 }, files: [], } as const; let cleanupInput: unknown; let appendCalls = 0; const environment = { CANDIDATE_ARCHIVE_PATH: "candidate.tar.gz", CANDIDATE_ARCHIVE_SHA256: "a".repeat(64), VULNERABILITY_REPORT_PATH: "vulnerability.json", PROVENANCE_ATTESTATION_PATH: "provenance.json", VULNERABILITY_PUBLIC_KEY_PATH: "vulnerability.pem", VULNERABILITY_KEY_ID: "vulnerability-key", PROVENANCE_PUBLIC_KEY_PATH: "provenance.pem", PROVENANCE_KEY_ID: "provenance-key", CI_RUN_ID: "run", CI_RUN_ATTEMPT: "1", VITE_COMMIT_SHA: "b".repeat(40), VULNERABILITY_INVOCATION_NONCE: "c".repeat(64), PROVENANCE_INVOCATION_NONCE: "d".repeat(64), RUNNER_TEMP: "/runner", GITHUB_OUTPUT: "/runner/github-output", }; await expect( runStageVerifiedPromotionCli(environment, { cwd: () => "/workspace", finalize: async () => finalized as any, appendOutput: async () => { appendCalls += 1; if (failureKind === "partial write failure") { // The output sink accepted an unspecified prefix before rejecting. } throw new Error(failureKind); }, cleanup: async (input) => { cleanupInput = input; }, writeStdout: () => undefined, }), ).rejects.toThrow(new RegExp(failureKind, "u")); expect(appendCalls).toBe(1); expect(cleanupInput).toEqual({ runnerTempRoot: "/runner", stagingRoot: finalized.stagingRoot, cleanupToken: finalized.cleanupToken, runnerTempIdentity: finalized.runnerTempIdentity, stagingIdentity: finalized.stagingIdentity, }); }, ); });