import { createHash, verify, type KeyObject } from "node:crypto"; import { z } from "zod"; import { canonicalizeSupplyChainValue, supplyChainDigest, } from "./supply-chain.ts"; export const PROVIDER_FUTURE_SKEW_MS = 5 * 60 * 1_000; export const PROVIDER_MAX_LIFETIME_MS = 2 * 60 * 60 * 1_000; export const PROMOTION_VERIFIER_ID = "clean-architecture-frontend-template/promotion-verifier"; export const PROMOTION_VERIFIER_VERSION = "3"; const sha256 = z.string().regex(/^[a-f0-9]{64}$/u); const fingerprint = z.string().regex(/^sha256:[a-f0-9]{64}$/u); const revision = z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u); const nonce = z.string().regex(/^[a-f0-9]{64}$/u); const nonEmptyString = z.string().min(1); const timestamp = z .string() .regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u) .refine((value) => new Date(value).toISOString() === value); const runSchema = z .object({ id: z.string().min(1).max(128), attempt: z.int().min(1).max(1_000) }) .strict(); const sourceSchema = z .object({ revision, sourceSetSha256: sha256 }) .strict(); const candidateSchema = z .object({ archiveSha256: sha256, bundleSha256: sha256, distSha256: sha256, lockfileSha256: sha256, }) .strict(); const providerRunSchema = runSchema.extend({ invocationNonce: nonce }).strict(); export const secretScanAttestationSchema = z .object({ status: z.literal("PASS"), localEvidenceAssessmentSha256: sha256, sourceSetSha256: sha256, policySha256: sha256, sarifSha256: sha256, scanInputSha256: sha256, }) .strict(); const signatureSchema = z .object({ algorithm: z.literal("Ed25519"), keyId: nonEmptyString, publicKeyFingerprint: fingerprint, value: z.string().regex(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u), }) .strict(); const providerCommon = { schemaVersion: z.literal(2), provider: nonEmptyString, issuedAt: timestamp, expiresAt: timestamp, run: providerRunSchema, source: sourceSchema, candidate: candidateSchema, signature: signatureSchema, } as const; export const vulnerabilityProviderReportSchema = z .object({ ...providerCommon, evidenceType: z.literal("vulnerability-report"), secretScanAttestation: secretScanAttestationSchema, findings: z.array(z.record(z.string(), z.json())), }) .strict(); export const provenanceProviderAttestationSchema = z .object({ ...providerCommon, evidenceType: z.literal("provenance-attestation"), signer: nonEmptyString, subject: z .object({ name: z.literal("dist"), digest: z.object({ sha256 }).strict() }) .strict(), }) .strict(); const verificationCommon = { schemaVersion: z.literal(3), verifiedAt: timestamp, status: z.enum(["PASS", "FAIL_UNVERIFIED"]), verifier: z .object({ id: nonEmptyString, version: nonEmptyString }) .strict(), run: runSchema, source: sourceSchema, candidate: candidateSchema, providerEvidence: z .object({ vulnerabilityReportSha256: sha256, provenanceAttestationSha256: sha256, vulnerabilityInvocationNonce: nonce, provenanceInvocationNonce: nonce, vulnerabilityKeyId: nonEmptyString, vulnerabilityKeyFingerprint: fingerprint, provenanceKeyId: nonEmptyString, provenanceKeyFingerprint: fingerprint, secretScanAttestation: secretScanAttestationSchema, }) .strict(), trustPolicySha256: sha256, failures: z.array(z.string()), } as const; const providerVerificationV3Schema = z .object({ ...verificationCommon, artifactType: z.literal("provider-verification"), vulnerabilityStatus: z.enum(["PASS", "FAIL_UNVERIFIED"]), provenanceAttestationStatus: z.enum(["PASS", "FAIL_UNVERIFIED"]), }) .strict(); const promotionVerificationV3Schema = z .object({ ...verificationCommon, artifactType: z.literal("promotion-verification"), localEvidenceStatus: z.enum(["PASS", "FAIL"]), localEvidenceAssessmentSha256: sha256, providerVerificationSha256: sha256, }) .strict(); export const providerVerificationArtifactSchema = z .discriminatedUnion("artifactType", [ providerVerificationV3Schema, promotionVerificationV3Schema, ]) .superRefine((record, context) => { const subordinatePass = record.artifactType === "provider-verification" ? record.vulnerabilityStatus === "PASS" && record.provenanceAttestationStatus === "PASS" : record.localEvidenceStatus === "PASS"; const coherentPass = subordinatePass && record.failures.length === 0; if ((record.status === "PASS") !== coherentPass) { context.addIssue({ code: "custom", path: ["status"], message: "verification PASS must agree with subordinate statuses and failures", }); } if (record.status === "FAIL_UNVERIFIED" && record.failures.length === 0) { context.addIssue({ code: "custom", path: ["failures"], message: "failed verification requires a failure diagnostic", }); } }); export type ProviderVerificationArtifactType = z.infer< typeof providerVerificationArtifactSchema >["artifactType"]; export type ProviderTrust = Readonly<{ keyId: string; publicKey: KeyObject; publicKeyFingerprint: string; }>; export type ExpectedPromotionContext = Readonly<{ run: Readonly<{ id: string; attempt: number }>; source: Readonly<{ revision: string; sourceSetSha256: string }>; candidate: Readonly<{ archiveSha256: string; bundleSha256: string; distSha256: string; lockfileSha256: string; }>; vulnerabilityInvocationNonce: string; provenanceInvocationNonce: string; secretScanAttestation: z.infer; }>; export type PromotionEvidenceResult = Readonly<{ status: "PASS" | "FAIL_UNVERIFIED"; vulnerabilityStatus: "PASS" | "FAIL_UNVERIFIED"; provenanceAttestationStatus: "PASS" | "FAIL_UNVERIFIED"; failures: readonly string[]; }>; export function validateProviderEvidence(input: Readonly<{ kind: "vulnerability" | "provenance"; value: unknown; expected: ExpectedPromotionContext; trust: ProviderTrust | null; nowEpochMs?: () => number; }>): Readonly<{ evidence: unknown | null; status: "PASS" | "FAIL_UNVERIFIED"; failures: readonly string[]; }> { const failures: string[] = []; const now = (input.nowEpochMs ?? Date.now)(); if (input.kind === "vulnerability") { const parsed = vulnerabilityProviderReportSchema.safeParse(input.value); if (!parsed.success) { return Object.freeze({ evidence: null, status: "FAIL_UNVERIFIED", failures: Object.freeze([ "external vulnerability provider report is missing or invalid", ]), }); } validateCommonContext( "vulnerability report", parsed.data, input.expected, input.expected.vulnerabilityInvocationNonce, input.trust, now, failures, ); if ( JSON.stringify(parsed.data.secretScanAttestation) !== JSON.stringify(input.expected.secretScanAttestation) ) { failures.push("vulnerability report secret scan attestation mismatch"); } if (parsed.data.findings.length > 0) { failures.push("vulnerability report contains findings"); } return Object.freeze({ evidence: parsed.data, status: failures.length === 0 ? "PASS" : "FAIL_UNVERIFIED", failures: Object.freeze(failures), }); } const parsed = provenanceProviderAttestationSchema.safeParse(input.value); if (!parsed.success) { return Object.freeze({ evidence: null, status: "FAIL_UNVERIFIED", failures: Object.freeze([ "external signed provenance attestation is missing or invalid", ]), }); } validateCommonContext( "provenance attestation", parsed.data, input.expected, input.expected.provenanceInvocationNonce, input.trust, now, failures, ); if (parsed.data.subject.digest.sha256 !== input.expected.candidate.distSha256) { failures.push("provenance attestation subject dist digest mismatch"); } return Object.freeze({ evidence: parsed.data, status: failures.length === 0 ? "PASS" : "FAIL_UNVERIFIED", failures: Object.freeze(failures), }); } export function providerEvidenceSignaturePayload(value: unknown): Buffer { if (!isRecord(value)) return Buffer.from("null", "utf8"); const { signature: _signature, ...payload } = value; return Buffer.from(JSON.stringify(canonicalizeSupplyChainValue(payload)), "utf8"); } export function providerPublicKeyFingerprint(publicKey: KeyObject): string { if (publicKey.asymmetricKeyType !== "ed25519") { throw new TypeError("provider trust key must be Ed25519"); } return `sha256:${createHash("sha256") .update(publicKey.export({ type: "spki", format: "der" })) .digest("hex")}`; } export function createTrustPolicy(input: Readonly<{ vulnerabilityTrust: ProviderTrust; provenanceTrust: ProviderTrust; }>) { assertDistinctProviderTrust(input); return Object.freeze({ algorithm: "Ed25519" as const, vulnerability: Object.freeze({ keyId: input.vulnerabilityTrust.keyId, publicKeyFingerprint: input.vulnerabilityTrust.publicKeyFingerprint, }), provenance: Object.freeze({ keyId: input.provenanceTrust.keyId, publicKeyFingerprint: input.provenanceTrust.publicKeyFingerprint, }), issuedAtFutureSkewMs: PROVIDER_FUTURE_SKEW_MS, maximumLifetimeMs: PROVIDER_MAX_LIFETIME_MS, }); } export function assertDistinctProviderTrust(input: Readonly<{ vulnerabilityTrust: ProviderTrust; provenanceTrust: ProviderTrust; }>): void { const vulnerabilityFingerprint = providerPublicKeyFingerprint( input.vulnerabilityTrust.publicKey, ); const provenanceFingerprint = providerPublicKeyFingerprint( input.provenanceTrust.publicKey, ); if ( input.vulnerabilityTrust.keyId === input.provenanceTrust.keyId || vulnerabilityFingerprint === provenanceFingerprint || input.vulnerabilityTrust.publicKeyFingerprint === input.provenanceTrust.publicKeyFingerprint ) { throw new TypeError("provider trust roles require distinct key identities and DER-SPKI fingerprints"); } } export function trustPolicySha256(input: Readonly<{ vulnerabilityTrust: ProviderTrust; provenanceTrust: ProviderTrust; }>): string { return supplyChainDigest(createTrustPolicy(input)); } export function evaluatePromotionEvidence(input: Readonly<{ expected: ExpectedPromotionContext; localStatus: unknown; vulnerabilityReport: unknown; provenanceAttestation: unknown; vulnerabilityTrust: ProviderTrust | null; provenanceTrust: ProviderTrust | null; nowEpochMs?: () => number; }>): PromotionEvidenceResult { const failures: string[] = []; if (input.localStatus !== "PASS") { failures.push("local supply-chain evidence is not PASS"); } const now = (input.nowEpochMs ?? Date.now)(); let vulnerabilityStatus: "PASS" | "FAIL_UNVERIFIED" = "FAIL_UNVERIFIED"; let provenanceAttestationStatus: "PASS" | "FAIL_UNVERIFIED" = "FAIL_UNVERIFIED"; const vulnerability = vulnerabilityProviderReportSchema.safeParse( input.vulnerabilityReport, ); if (!vulnerability.success) { failures.push("external vulnerability provider report is missing or invalid"); } else { const before = failures.length; validateCommonContext( "vulnerability report", vulnerability.data, input.expected, input.expected.vulnerabilityInvocationNonce, input.vulnerabilityTrust, now, failures, ); if ( JSON.stringify(vulnerability.data.secretScanAttestation) !== JSON.stringify(input.expected.secretScanAttestation) ) { failures.push("vulnerability report secret scan attestation mismatch"); } if (vulnerability.data.findings.length > 0) { failures.push("vulnerability report contains findings"); } if (failures.length === before && input.localStatus === "PASS") { vulnerabilityStatus = "PASS"; } } const provenance = provenanceProviderAttestationSchema.safeParse( input.provenanceAttestation, ); if (!provenance.success) { failures.push("external signed provenance attestation is missing or invalid"); } else { const before = failures.length; validateCommonContext( "provenance attestation", provenance.data, input.expected, input.expected.provenanceInvocationNonce, input.provenanceTrust, now, failures, ); if (provenance.data.subject.digest.sha256 !== input.expected.candidate.distSha256) { failures.push("provenance attestation subject dist digest mismatch"); } if (failures.length === before && input.localStatus === "PASS") { provenanceAttestationStatus = "PASS"; } } return Object.freeze({ status: failures.length === 0 && vulnerabilityStatus === "PASS" && provenanceAttestationStatus === "PASS" ? "PASS" : "FAIL_UNVERIFIED", vulnerabilityStatus, provenanceAttestationStatus, failures: Object.freeze(failures), }); } function validateCommonContext( label: "vulnerability report" | "provenance attestation", evidence: z.infer< | typeof vulnerabilityProviderReportSchema | typeof provenanceProviderAttestationSchema >, expected: ExpectedPromotionContext, expectedNonce: string, trust: ProviderTrust | null, now: number, failures: string[], ): void { if ( evidence.run.id !== expected.run.id || evidence.run.attempt !== expected.run.attempt ) { failures.push(`${label} run identity mismatch`); } if (evidence.run.invocationNonce !== expectedNonce) { failures.push(`${label} invocation nonce mismatch`); } if ( evidence.source.revision !== expected.source.revision || evidence.source.sourceSetSha256 !== expected.source.sourceSetSha256 ) { failures.push(`${label} source identity mismatch`); } if (JSON.stringify(evidence.candidate) !== JSON.stringify(expected.candidate)) { failures.push(`${label} candidate identity mismatch`); } validateEvidenceTime(label, evidence.issuedAt, evidence.expiresAt, now, failures); if ( !trust || evidence.signature.keyId !== trust.keyId || evidence.signature.publicKeyFingerprint !== trust.publicKeyFingerprint ) { failures.push(`${label} trust identity mismatch`); return; } try { if ( providerPublicKeyFingerprint(trust.publicKey) !== trust.publicKeyFingerprint || !verify( null, providerEvidenceSignaturePayload(evidence), trust.publicKey, Buffer.from(evidence.signature.value, "base64"), ) ) { failures.push(`${label} signature verification failed`); } } catch { failures.push(`${label} signature verification failed`); } } function validateEvidenceTime( label: string, issuedAt: string, expiresAt: string, now: number, failures: string[], ): void { const issued = Date.parse(issuedAt); const expires = Date.parse(expiresAt); if (issued > now + PROVIDER_FUTURE_SKEW_MS) { failures.push(`${label} issuedAt exceeds allowed future skew`); } if (expires <= now) failures.push(`${label} is expired`); if (expires <= issued) failures.push(`${label} validity window is not positive`); if (expires - issued > PROVIDER_MAX_LIFETIME_MS) { failures.push(`${label} validity window exceeds two hours`); } } function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); }