Files
Portfolio/tests/integration/release-test-fixture.ts

304 lines
9.5 KiB
TypeScript

import { generateKeyPairSync, sign, verify } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import {
canonicalJsonDigest,
canonicalValueBytes,
byteDigest,
} from "../../src/security/canonical-digest.ts";
import { computeProductionInputRoot } from "../../src/security/production-input-root.ts";
const digest = (character: string) => character.repeat(64);
export interface ReleaseFixture {
distDir: string;
signedManifestPath: string;
sbomPath: string;
trustPolicyPath: string;
trustPolicy: Record<string, unknown>;
verifySignedReleaseEnvelope(input: {
signedRelease: any;
trustPolicy: any;
now: string;
protectedState: any;
}): any;
protectedState: {
latestLedgerHeadHash: string;
latestLedgerRevision: number;
currentManifestDigest: string | null;
publisherFence: number;
releaseTrustPolicyDigest: string;
};
manifestDigest: string;
buildId: string;
expiresAt: string;
signFor(input: {
buildId: string;
distDir: string;
predecessorManifestDigest: string | null;
publisherFence: number;
issuedAt?: string;
expiresAt?: string;
outputPath: string;
}): Promise<{ manifestDigest: string; signedManifestPath: string }>;
}
export async function createReleaseFixture(
root: string,
): Promise<ReleaseFixture> {
const distDir = join(root, "dist");
await mkdir(join(distDir, "assets"), { recursive: true });
await writeFile(
join(distDir, "index.html"),
"<!doctype html><title>Portfolio</title>\n",
);
await writeFile(
join(distDir, "assets/app.js"),
'console.log("portfolio");\n',
);
const sbomPath = join(root, "sbom.cdx.json");
await writeFile(sbomPath, '{"bomFormat":"CycloneDX","specVersion":"1.6"}\n');
const keys = generateKeyPairSync("ed25519");
const publicKeyPem = keys.publicKey
.export({ type: "spki", format: "pem" })
.toString();
const trustPolicyPath = join(root, "release-trust.json");
const trustPolicy = {
keyId: "release-test-key",
issuerEpoch: 7,
scope: "portfolio-release-signing/v1",
publicKeyPem,
};
await writeFile(trustPolicyPath, canonicalValueBytes(trustPolicy));
const protectedState = {
latestLedgerHeadHash: digest("9"),
latestLedgerRevision: 11,
currentManifestDigest: null,
publisherFence: 0,
releaseTrustPolicyDigest: canonicalJsonDigest(
"portfolio-trust-policy/v1",
trustPolicy,
),
};
const verifySignedReleaseEnvelope = ({
signedRelease,
trustPolicy,
now,
protectedState: state,
}: {
signedRelease: any;
trustPolicy: any;
now: string;
protectedState: any;
}) => {
if (signedRelease?.schema !== "portfolio-signed-release-manifest/v1")
throw new Error("bad envelope schema");
if (
signedRelease.signature?.keyId !== trustPolicy.keyId ||
signedRelease.signature?.issuerEpoch !== trustPolicy.issuerEpoch ||
signedRelease.signature?.scope !== trustPolicy.scope
)
throw new Error("untrusted signer");
const signature = Buffer.from(signedRelease.signature.value, "base64url");
if (
signature.length !== 64 ||
!verify(
null,
canonicalValueBytes(signedRelease.payload),
trustPolicy.publicKeyPem,
signature,
)
)
throw new Error("invalid release signature");
const manifestDigest = canonicalJsonDigest(
"portfolio-candidate-release-manifest/v1",
signedRelease.payload,
);
if (manifestDigest !== signedRelease.payloadDigest)
throw new Error("candidate digest mismatch");
const candidate = signedRelease.payload;
if (
candidate.latestLedgerHeadHash !== state.latestLedgerHeadHash ||
candidate.latestLedgerRevision !== state.latestLedgerRevision
)
throw new Error("stale ledger");
if (
candidate.releaseManifest.currentManifestDigest !==
state.currentManifestDigest
)
throw new Error("stale predecessor");
if (Date.parse(now) >= Date.parse(candidate.releaseManifest.expiresAt))
throw new Error("expired release");
return {
schema: "portfolio-verified-release-admission/v1",
manifestDigest,
manifestPayload: candidate.releaseManifest,
expiresAt: candidate.releaseManifest.expiresAt,
predecessorManifestDigest:
candidate.releaseManifest.currentManifestDigest,
latestLedgerHeadHash: candidate.latestLedgerHeadHash,
latestLedgerRevision: candidate.latestLedgerRevision,
expectedFence: state.publisherFence + 1,
signatureVerified: true,
};
};
const signFor = async (input: {
buildId: string;
distDir: string;
predecessorManifestDigest: string | null;
publisherFence: number;
issuedAt?: string;
expiresAt?: string;
outputPath: string;
}) => {
const artifactPaths = ["assets/app.js", "index.html"];
const artifacts = await Promise.all(
artifactPaths.map(async (path) => {
const bytes = await readFile(join(input.distDir, path));
return {
path,
sha256: byteDigest(bytes),
bytes: bytes.length,
encoding: "identity" as const,
};
}),
);
const inputDigests = {
PortfolioContent: digest("a"),
PublicAssetManifest: digest("b"),
PreparedAssetSet: digest("c"),
SiteConfig: digest("d"),
};
const issuedAt = input.issuedAt ?? "2026-09-02T00:00:00Z";
const expiresAt = input.expiresAt ?? "2026-09-03T00:00:00Z";
const releaseManifest = {
schema: "portfolio-release-manifest/v1" as const,
launchMode: "private-source-initial-launch" as const,
buildId: input.buildId,
inputDigests,
productionInputRoot: computeProductionInputRoot({
portfolioContentDigest: inputDigests.PortfolioContent,
publicAssetManifestDigest: inputDigests.PublicAssetManifest,
preparedAssetSetDigest: inputDigests.PreparedAssetSet,
siteConfigDigest: inputDigests.SiteConfig,
}),
artifacts,
deployFiles: artifactPaths,
sourceCommit: digest("e"),
sourceTree: digest("f"),
approvedRecordRoot: digest("1"),
approvedAssetRoot: digest("2"),
approvalProofSetDigest: digest("3"),
approvalProjectionAttestationDigest: digest("4"),
scanEvidenceDigest: digest("5"),
policyVersions: { approval: "1", privacy: "1", release: "1" },
toolVersions: { node: "22.22.1" },
predecessorManifestDigest: input.predecessorManifestDigest,
currentManifestDigest: input.predecessorManifestDigest,
issuedAt,
expiresAt,
minimumBoundExpiry: expiresAt,
};
const candidate = {
schema: "portfolio-candidate-release-manifest/v1",
releaseManifest,
latestLedgerHeadHash: protectedState.latestLedgerHeadHash,
latestLedgerRevision: protectedState.latestLedgerRevision,
sourceClosureDigest: digest("6"),
publicObjectClosureDigest: digest("b"),
publicRefMap: {},
lfsOids: [],
releaseArchives: [],
buildProvenanceDigest: digest("7"),
privacyPolicyDigest: digest("8"),
toolImageDigests: [digest("0")],
};
const payloadDigest = canonicalJsonDigest(
"portfolio-candidate-release-manifest/v1",
candidate,
);
const signature = sign(
null,
canonicalValueBytes(candidate),
keys.privateKey,
).toString("base64url");
const signedRelease = {
schema: "portfolio-signed-release-manifest/v1",
payload: candidate,
payloadDigest,
signature: {
algorithm: "Ed25519",
encoding: "base64url-nopad",
keyId: "release-test-key",
issuerEpoch: 7,
scope: "portfolio-release-signing/v1",
value: signature,
},
};
await writeFile(input.outputPath, canonicalValueBytes(signedRelease));
return {
manifestDigest: payloadDigest,
signedManifestPath: input.outputPath,
};
};
const signedManifestPath = join(root, "signed-release.json");
const initial = await signFor({
buildId: "release-a",
distDir,
predecessorManifestDigest: null,
publisherFence: 0,
outputPath: signedManifestPath,
});
return {
distDir,
signedManifestPath,
sbomPath,
trustPolicyPath,
trustPolicy,
verifySignedReleaseEnvelope,
protectedState,
manifestDigest: initial.manifestDigest,
buildId: "release-a",
expiresAt: "2026-09-03T00:00:00Z",
signFor,
};
}
export async function initializeReleaseRoot(
root: string,
state?: Partial<ReleaseFixture["protectedState"]>,
trustPolicy?: Record<string, unknown>,
): Promise<void> {
await mkdir(join(root, "control"), { recursive: true });
await mkdir(join(root, ".control"), { recursive: true });
await mkdir(join(root, "inbox"), { recursive: true });
if (trustPolicy !== undefined)
await writeFile(
join(root, ".control", "release-trust.json"),
canonicalValueBytes(trustPolicy),
);
await writeFile(
join(root, ".portfolio-release-root"),
"portfolio-release-root/v1\n",
);
await writeFile(
join(root, "control/state.json"),
canonicalValueBytes({
schema: "portfolio-release-control-state/v1",
currentManifestDigest: state?.currentManifestDigest ?? null,
currentReleaseId: null,
publisherFence: state?.publisherFence ?? 0,
latestLedgerHeadHash: state?.latestLedgerHeadHash ?? digest("9"),
latestLedgerRevision: state?.latestLedgerRevision ?? 11,
releaseTrustPolicyDigest: state?.releaseTrustPolicyDigest ?? digest("8"),
activeTransaction: null,
}),
);
}