Files
clean-architecture-frontend…/scripts/lib/exact-promotion-bundle.ts
T

354 lines
12 KiB
TypeScript

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<Record<PromotedFileName, Buffer>>
>;
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<Readonly<{ status: "PASS" }>> {
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;
}