import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import path from "node:path"; import { PROMOTED_FILE_NAMES, type PromotedFileName, } from "../contracts/promotion-artifacts.ts"; import { PROMOTION_VERIFIER_ID, PROMOTION_VERIFIER_VERSION, assertDistinctProviderTrust, evaluatePromotionEvidence, providerVerificationArtifactSchema, provenanceProviderAttestationSchema, vulnerabilityProviderReportSchema, trustPolicySha256, type ProviderTrust, } from "./provider-evidence.ts"; import { verifyCapturedCiCandidateArchive } from "./ci-candidate-archive.ts"; import { LOCAL_EVIDENCE_ASSESSMENT_PATH } from "./release-candidate.ts"; import { verifyArchivedLocalEvidence } from "./local-release-evidence.ts"; export type ExactPromotionBundle = Readonly< Partial> >; export type ExactPromotionExpectedContext = Readonly<{ run: Readonly<{ id: string; attempt: number }>; sourceRevision: string; archiveSha256: string; sourceSetSha256?: string; bundleSha256?: string; distSha256?: string; lockfileSha256?: string; }>; export async function verifyExactPromotionBundle( files: ExactPromotionBundle, options: Readonly<{ vulnerabilityTrust: ProviderTrust; provenanceTrust: ProviderTrust; expected: ExactPromotionExpectedContext; nowEpochMs?: () => number; }>, ): Promise> { assertDistinctProviderTrust(options); assertExternalExpectedContext(options.expected); const names = Object.keys(files).sort(asciiCompare); const expectedNames = [...PROMOTED_FILE_NAMES].sort(asciiCompare); if (JSON.stringify(names) !== JSON.stringify(expectedNames)) { throw new Error("promotion bundle must contain the exact five canonical files"); } for (const name of PROMOTED_FILE_NAMES) { if (!Buffer.isBuffer(files[name])) { throw new TypeError(`promotion bundle file is missing or not captured bytes: ${name}`); } } const archiveBytes = files["release-candidate.tar.gz"]!; const vulnerabilityBytes = files["vulnerability-report.json"]!; const provenanceBytes = files["provenance-attestation.json"]!; const providerBytes = files["provider-verification.json"]!; const promotionBytes = files["promotion-verification.json"]!; const vulnerability = vulnerabilityProviderReportSchema.parse(parseJson( vulnerabilityBytes, "vulnerability report", )); const provenance = provenanceProviderAttestationSchema.parse(parseJson( provenanceBytes, "provenance attestation", )); const provider = providerVerificationArtifactSchema.parse(parseJson( providerBytes, "provider verification", )); const promotion = providerVerificationArtifactSchema.parse(parseJson( promotionBytes, "promotion verification", )); if ( provider.artifactType !== "provider-verification" || promotion.artifactType !== "promotion-verification" ) { throw new Error("promotion verification artifact role mismatch"); } for (const [label, record] of [ ["provider", provider], ["promotion", promotion], ] as const) { if ( record.verifier.id !== PROMOTION_VERIFIER_ID || record.verifier.version !== PROMOTION_VERIFIER_VERSION ) { throw new Error(`${label} verification literal verifier identity mismatch`); } if (record.status !== "PASS" || record.failures.length !== 0) { throw new Error(`${label} verification must be PASS without failures`); } } if ( provider.vulnerabilityStatus !== "PASS" || provider.provenanceAttestationStatus !== "PASS" ) { throw new Error("provider verification subordinate statuses must both be PASS"); } if (promotion.localEvidenceStatus !== "PASS") { throw new Error("promotion local evidence subordinate status must be PASS"); } assertEqual("shared verifiedAt", provider.verifiedAt, promotion.verifiedAt); assertEqual("shared run", provider.run, promotion.run); assertEqual("shared source", provider.source, promotion.source); assertEqual("shared candidate", provider.candidate, promotion.candidate); assertEqual( "shared provider evidence", provider.providerEvidence, promotion.providerEvidence, ); assertEqual( "shared trust policy", provider.trustPolicySha256, promotion.trustPolicySha256, ); assertEqual("external expected run", provider.run, options.expected.run); assertEqual( "external expected source revision", provider.source.revision, options.expected.sourceRevision, ); assertEqual( "external expected archive digest", provider.candidate.archiveSha256, options.expected.archiveSha256, ); for (const [label, actual, expected] of [ ["source set", provider.source.sourceSetSha256, options.expected.sourceSetSha256], ["bundle", provider.candidate.bundleSha256, options.expected.bundleSha256], ["dist", provider.candidate.distSha256, options.expected.distSha256], ["lockfile", provider.candidate.lockfileSha256, options.expected.lockfileSha256], ] as const) { if (expected !== undefined) { assertEqual(`external expected ${label} digest`, actual, expected); } } const anchoredTrustPolicySha256 = trustPolicySha256(options); if (provider.trustPolicySha256 !== anchoredTrustPolicySha256) { throw new Error("verification trust policy does not match anchored provider keys"); } if (promotion.providerVerificationSha256 !== sha256(providerBytes)) { throw new Error("promotion provider verification byte hash mismatch"); } if ( provider.candidate.archiveSha256 !== sha256(archiveBytes) || provider.providerEvidence.vulnerabilityReportSha256 !== sha256(vulnerabilityBytes) || provider.providerEvidence.provenanceAttestationSha256 !== sha256(provenanceBytes) ) { if (provider.candidate.archiveSha256 !== sha256(archiveBytes)) { throw new Error("candidate archive actual digest mismatch"); } if ( provider.providerEvidence.vulnerabilityReportSha256 !== sha256(vulnerabilityBytes) ) { throw new Error("vulnerability report actual digest mismatch"); } throw new Error("provenance attestation actual digest mismatch"); } for (const [label, evidence, nonce, keyId, fingerprint] of [ [ "vulnerability", vulnerability, provider.providerEvidence.vulnerabilityInvocationNonce, provider.providerEvidence.vulnerabilityKeyId, provider.providerEvidence.vulnerabilityKeyFingerprint, ], [ "provenance", provenance, provider.providerEvidence.provenanceInvocationNonce, provider.providerEvidence.provenanceKeyId, provider.providerEvidence.provenanceKeyFingerprint, ], ] as const) { assertEqual(`${label} run`, { id: evidence.run.id, attempt: evidence.run.attempt }, provider.run); assertEqual(`${label} source`, evidence.source, provider.source); assertEqual(`${label} candidate`, evidence.candidate, provider.candidate); if ( evidence.run.invocationNonce !== nonce || evidence.signature.keyId !== keyId || evidence.signature.publicKeyFingerprint !== fingerprint ) { throw new Error(`${label} provider evidence nonce or trust role mismatch`); } } if (provenance.subject.digest.sha256 !== provider.candidate.distSha256) { throw new Error("provenance subject dist digest mismatch"); } if (vulnerability.findings.length !== 0) { throw new Error("vulnerability report is not PASS"); } assertEqual( "signed secret scan attestation", vulnerability.secretScanAttestation, provider.providerEvidence.secretScanAttestation, ); let assessmentSha256: string | null = null; const localIdentityHolder: { current: null | Readonly<{ sourceRevision: string; sourceSetSha256: string; assessmentSha256: string; secretScan: Readonly<{ policySha256: string; sarifSha256: string; scanInputSha256: string; }>; }>; } = { current: null }; await verifyCapturedCiCandidateArchive( archiveBytes, provider.candidate.archiveSha256, { verifyExtracted: async (extractionRoot, manifest) => { assertEqual("archive candidate", { bundleSha256: manifest.bundleSha256, distSha256: manifest.distSha256, lockfileSha256: manifest.lockfileSha256, }, { bundleSha256: provider.candidate.bundleSha256, distSha256: provider.candidate.distSha256, lockfileSha256: provider.candidate.lockfileSha256, }); assessmentSha256 = sha256( await readFile(path.join(extractionRoot, LOCAL_EVIDENCE_ASSESSMENT_PATH)), ); const local = await verifyArchivedLocalEvidence({ extractionRoot, expectedManifest: manifest, }); if (local.status !== "PASS" || !local.identity) { throw new Error( `exact-five archived local verification is not PASS: ${local.failures.join(", ")}`, ); } localIdentityHolder.current = local.identity; }, }, ); if (assessmentSha256 !== promotion.localEvidenceAssessmentSha256) { throw new Error("promotion local evidence assessment actual digest mismatch"); } if ( !localIdentityHolder.current || localIdentityHolder.current.sourceRevision !== provider.source.revision || localIdentityHolder.current.sourceSetSha256 !== provider.source.sourceSetSha256 || localIdentityHolder.current.assessmentSha256 !== promotion.localEvidenceAssessmentSha256 ) { throw new Error("exact-five archived local identity mismatch"); } assertEqual("archived secret scan attestation", { status: "PASS", localEvidenceAssessmentSha256: localIdentityHolder.current.assessmentSha256, sourceSetSha256: localIdentityHolder.current.sourceSetSha256, policySha256: localIdentityHolder.current.secretScan.policySha256, sarifSha256: localIdentityHolder.current.secretScan.sarifSha256, scanInputSha256: localIdentityHolder.current.secretScan.scanInputSha256, }, vulnerability.secretScanAttestation); const reevaluated = evaluatePromotionEvidence({ expected: { run: provider.run, source: provider.source, candidate: provider.candidate, vulnerabilityInvocationNonce: provider.providerEvidence.vulnerabilityInvocationNonce, provenanceInvocationNonce: provider.providerEvidence.provenanceInvocationNonce, secretScanAttestation: provider.providerEvidence.secretScanAttestation, }, localStatus: "PASS", vulnerabilityReport: vulnerability, provenanceAttestation: provenance, vulnerabilityTrust: options.vulnerabilityTrust, provenanceTrust: options.provenanceTrust, nowEpochMs: options.nowEpochMs, }); if ( reevaluated.status !== "PASS" || reevaluated.vulnerabilityStatus !== "PASS" || reevaluated.provenanceAttestationStatus !== "PASS" ) { throw new Error( `exact-five provider signature/freshness revalidation is not PASS: ${reevaluated.failures.join(", ")}`, ); } return Object.freeze({ status: "PASS" as const }); } function assertExternalExpectedContext( expected: ExactPromotionExpectedContext, ): void { if ( !expected || typeof expected.run?.id !== "string" || expected.run.id.length === 0 || !Number.isSafeInteger(expected.run.attempt) || expected.run.attempt < 1 || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(expected.sourceRevision) || !isSha256(expected.archiveSha256) ) { throw new TypeError("external expected promotion context is invalid or incomplete"); } for (const digest of [ expected.sourceSetSha256, expected.bundleSha256, expected.distSha256, expected.lockfileSha256, ]) { if (digest !== undefined && !isSha256(digest)) { throw new TypeError("external optional expected promotion digest is invalid"); } } } function isSha256(value: unknown): value is string { return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value); } function parseJson(bytes: Buffer, label: string): unknown { try { return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) as unknown; } catch { throw new TypeError(`${label} is not strict UTF-8 JSON`); } } function assertEqual(label: string, left: unknown, right: unknown): void { if (JSON.stringify(left) !== JSON.stringify(right)) { throw new Error(`${label} mismatch`); } } function sha256(bytes: Buffer): string { return createHash("sha256").update(bytes).digest("hex"); } function asciiCompare(left: string, right: string): number { return left < right ? -1 : left > right ? 1 : 0; }