chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,494 @@
|
||||
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<typeof secretScanAttestationSchema>;
|
||||
}>;
|
||||
|
||||
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<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
Reference in New Issue
Block a user