refactor: adapter 구현중..
This commit is contained in:
@@ -0,0 +1,353 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user