Files
clean-architecture-frontend…/scripts/lib/provider-evidence.ts
T

207 lines
6.0 KiB
TypeScript

import { verify, type KeyObject } from "node:crypto";
import { z } from "zod";
import { canonicalizeSupplyChainValue } from "./supply-chain.ts";
const sha256 = z.string().regex(/^[a-f0-9]{64}$/u);
const nonEmptyString = z.string().trim().min(1);
const signatureSchema = z
.object({
algorithm: z.literal("Ed25519"),
keyId: nonEmptyString,
value: z.string().regex(/^[A-Za-z0-9+/]+={0,2}$/u),
})
.strict();
export const vulnerabilityProviderReportSchema = z
.object({
schemaVersion: z.literal(1),
provider: nonEmptyString,
generatedAt: z.iso.datetime(),
scannedLockfileSha256: sha256,
scannedDistSha256: sha256,
findings: z.array(z.record(z.string(), z.json())),
signature: signatureSchema,
})
.strict();
export const provenanceProviderAttestationSchema = z
.object({
schemaVersion: z.literal(1),
provider: nonEmptyString,
signer: nonEmptyString,
generatedAt: z.iso.datetime(),
subject: z
.object({
name: z.literal("dist"),
digest: z.object({ sha256 }).strict(),
})
.strict(),
signature: signatureSchema,
})
.strict();
export const providerVerificationArtifactSchema = z
.object({
schemaVersion: z.literal(1),
status: z.enum(["PASS", "FAIL_UNVERIFIED"]),
vulnerabilityStatus: z.enum(["PASS", "FAIL_UNVERIFIED"]),
provenanceAttestationStatus: z.enum(["PASS", "FAIL_UNVERIFIED"]),
lockfileSha256: sha256,
distSha256: sha256,
failures: z.array(z.string()),
})
.strict();
export type ProviderTrust = Readonly<{
keyId: string;
publicKey: KeyObject;
}>;
export type PromotionEvidenceResult = Readonly<{
status: "PASS" | "FAIL_UNVERIFIED";
vulnerabilityStatus: "PASS" | "FAIL_UNVERIFIED";
provenanceAttestationStatus: "PASS" | "FAIL_UNVERIFIED";
failures: readonly string[];
}>;
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 evaluatePromotionEvidence(input: Readonly<{
candidate: Readonly<{ distSha256: string; lockfileSha256: string }>;
currentDistSha256: string;
localStatus: unknown;
vulnerabilityReport: unknown;
provenanceAttestation: unknown;
vulnerabilityTrust: ProviderTrust | null;
provenanceTrust: ProviderTrust | null;
}>): PromotionEvidenceResult {
const failures: string[] = [];
let vulnerabilityStatus: "PASS" | "FAIL_UNVERIFIED" = "FAIL_UNVERIFIED";
let provenanceAttestationStatus: "PASS" | "FAIL_UNVERIFIED" =
"FAIL_UNVERIFIED";
if (input.localStatus !== "PASS") {
failures.push("local supply-chain evidence is not PASS");
}
if (input.currentDistSha256 !== input.candidate.distSha256) {
failures.push("candidate dist bytes changed after immutable build");
}
const vulnerability = vulnerabilityProviderReportSchema.safeParse(
input.vulnerabilityReport,
);
if (!vulnerability.success) {
failures.push("external vulnerability provider report is missing or invalid");
} else {
if (
vulnerability.data.scannedLockfileSha256 !==
input.candidate.lockfileSha256
) {
failures.push("vulnerability report lockfile digest mismatch");
}
if (
vulnerability.data.scannedDistSha256 !== input.candidate.distSha256
) {
failures.push("vulnerability report dist digest mismatch");
}
if (vulnerability.data.findings.length > 0) {
failures.push("vulnerability report contains findings");
}
const signaturePassed = signatureMatches(
vulnerability.data,
input.vulnerabilityTrust,
);
if (!signaturePassed) {
failures.push("vulnerability report signature verification failed");
}
if (
vulnerability.data.scannedLockfileSha256 ===
input.candidate.lockfileSha256 &&
vulnerability.data.scannedDistSha256 === input.candidate.distSha256 &&
vulnerability.data.findings.length === 0 &&
input.currentDistSha256 === input.candidate.distSha256 &&
input.localStatus === "PASS" &&
signaturePassed
) {
vulnerabilityStatus = "PASS";
}
}
const provenance = provenanceProviderAttestationSchema.safeParse(
input.provenanceAttestation,
);
if (!provenance.success) {
failures.push("external signed provenance attestation is missing or invalid");
} else {
if (provenance.data.subject.digest.sha256 !== input.candidate.distSha256) {
failures.push("provenance attestation dist digest mismatch");
}
const signaturePassed = signatureMatches(
provenance.data,
input.provenanceTrust,
);
if (!signaturePassed) {
failures.push("provenance attestation signature verification failed");
}
if (
provenance.data.subject.digest.sha256 === input.candidate.distSha256 &&
input.currentDistSha256 === input.candidate.distSha256 &&
input.localStatus === "PASS" &&
signaturePassed
) {
provenanceAttestationStatus = "PASS";
}
}
return Object.freeze({
status:
failures.length === 0 &&
vulnerabilityStatus === "PASS" &&
provenanceAttestationStatus === "PASS"
? "PASS"
: "FAIL_UNVERIFIED",
vulnerabilityStatus,
provenanceAttestationStatus,
failures: Object.freeze(failures),
});
}
function signatureMatches(
evidence: z.infer<
| typeof vulnerabilityProviderReportSchema
| typeof provenanceProviderAttestationSchema
>,
trust: ProviderTrust | null,
): boolean {
if (
!trust ||
evidence.signature.keyId !== trust.keyId ||
trust.publicKey.asymmetricKeyType !== "ed25519"
) {
return false;
}
try {
return verify(
null,
providerEvidenceSignaturePayload(evidence),
trust.publicKey,
Buffer.from(evidence.signature.value, "base64"),
);
} catch {
return false;
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}