import { generateKeyPairSync, sign, verify } from 'node:crypto'; import { chmod, 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; verifierModulePath: string; verifySignedReleaseEnvelope(input: { signedRelease: any; trustPolicy: any; now: string; protectedState: any }): any; protectedState: { latestLedgerHeadHash: string; latestLedgerRevision: number; currentManifestDigest: string | null; publisherFence: number; }; 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 { const distDir = join(root, 'dist'); await mkdir(join(distDir, 'assets'), { recursive: true }); await writeFile(join(distDir, 'index.html'), 'Portfolio\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'); await writeFile(trustPolicyPath, canonicalValueBytes({ keyId: 'release-test-key', issuerEpoch: 7, scope: 'portfolio-release-signing/v1', publicKeyPem, })); const verifierModulePath = join(root, 'real-release-verifier.mjs'); await writeFile(verifierModulePath, ` import { createHash, verify } from 'node:crypto'; function canonical(value) { if (value === null || typeof value !== 'object') return JSON.stringify(value); if (Array.isArray(value)) return '[' + value.map(canonical).join(',') + ']'; return '{' + Object.keys(value).sort().map((key) => JSON.stringify(key) + ':' + canonical(value[key])).join(',') + '}'; } function digest(schema, value) { return createHash('sha256').update(canonical({ schema, value })).digest('hex'); } function fail(message) { throw new Error(message); } export function verifySignedReleaseEnvelope({ signedRelease, trustPolicy, now, protectedState }) { if (signedRelease?.schema !== 'portfolio-signed-release-manifest/v1') fail('bad envelope schema'); if (signedRelease.signature?.algorithm !== 'Ed25519' || signedRelease.signature?.encoding !== 'base64url-nopad') fail('bad signature metadata'); if (signedRelease.signature?.keyId !== trustPolicy.keyId || signedRelease.signature?.issuerEpoch !== trustPolicy.issuerEpoch || signedRelease.signature?.scope !== trustPolicy.scope) fail('untrusted signer'); const signature = Buffer.from(signedRelease.signature.value, 'base64url'); if (signature.length !== 64 || !verify(null, Buffer.from(canonical(signedRelease.payload)), trustPolicy.publicKeyPem, signature)) fail('invalid release signature'); const manifestDigest = digest('portfolio-candidate-release-manifest/v1', signedRelease.payload); if (manifestDigest !== signedRelease.payloadDigest) fail('candidate digest mismatch'); const candidate = signedRelease.payload; if (candidate.latestLedgerHeadHash !== protectedState.latestLedgerHeadHash || candidate.latestLedgerRevision !== protectedState.latestLedgerRevision) fail('stale ledger'); if (candidate.releaseManifest.currentManifestDigest !== protectedState.currentManifestDigest) fail('stale predecessor'); if (Date.parse(now) >= Date.parse(candidate.releaseManifest.expiresAt)) fail('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: protectedState.publisherFence + 1, signatureVerified: true, }; } `); await chmod(verifierModulePath, 0o500); const protectedState = { latestLedgerHeadHash: digest('9'), latestLedgerRevision: 11, currentManifestDigest: null, publisherFence: 0, }; 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'), 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, verifierModulePath, verifySignedReleaseEnvelope, protectedState, manifestDigest: initial.manifestDigest, buildId: 'release-a', expiresAt: '2026-09-03T00:00:00Z', signFor, }; } export async function initializeReleaseRoot(root: string, state?: Partial): Promise { await mkdir(join(root, 'control'), { recursive: true }); 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, activeTransaction: null, })); }