import { createHash } from "node:crypto"; import { chmod, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile, } from "node:fs/promises"; import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; import { byteDigest, canonicalJsonDigest, canonicalValueBytes, isSha256Hex, parseCanonicalJson, } from "../../../src/security/canonical-digest.ts"; import { validatePublicPath } from "../../../src/security/production-input-root.ts"; import { validateReleaseManifest } from "../../../src/security/release-manifest.ts"; import { verifySignedReleaseEnvelope as auditedReleaseVerifier } from "../../release/sign-release.mjs"; const encoder = new TextEncoder(); const reservedPrefix = ".release/"; const safeToken = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u; const timestamp = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/u; export function exactObject(value, fields, label) { if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`); const actual = Object.keys(value).sort(); const expected = [...fields].sort(); if ( actual.length !== expected.length || actual.some((field, index) => field !== expected[index]) ) { throw new Error(`${label} contains missing or unknown fields.`); } return value; } export function assertToken(value, label) { if (typeof value !== "string" || !safeToken.test(value)) throw new Error(`${label} is invalid.`); return value; } export function assertTimestamp(value, label) { if ( typeof value !== "string" || !timestamp.test(value) || Number.isNaN(Date.parse(value)) ) throw new Error(`${label} is invalid.`); return value; } export function assertDigest(value, label) { if (!isSha256Hex(value)) throw new Error(`${label} is invalid.`); return value; } export async function readCanonicalFile(path, label) { const info = await lstat(path); if (!info.isFile() || info.isSymbolicLink()) throw new Error(`${label} must be a regular file.`); return parseCanonicalJson(await readFile(path)); } export function validateProtectedState(value) { const state = exactObject( value, [ "latestLedgerHeadHash", "latestLedgerRevision", "currentManifestDigest", "publisherFence", "releaseTrustPolicyDigest", ], "protected state", ); assertDigest(state.latestLedgerHeadHash, "protected ledger head"); assertDigest( state.releaseTrustPolicyDigest, "protected release trust policy", ); if ( !Number.isSafeInteger(state.latestLedgerRevision) || state.latestLedgerRevision < 0 || !Number.isSafeInteger(state.publisherFence) || state.publisherFence < 0 || (state.currentManifestDigest !== null && !isSha256Hex(state.currentManifestDigest)) ) throw new Error("Protected state is invalid."); return state; } export function validateAdmission(value, protectedState, now) { const admission = exactObject( value, [ "schema", "manifestDigest", "manifestPayload", "expiresAt", "predecessorManifestDigest", "latestLedgerHeadHash", "latestLedgerRevision", "expectedFence", "signatureVerified", ], "verified release admission", ); if ( admission.schema !== "portfolio-verified-release-admission/v1" || admission.signatureVerified !== true ) { throw new Error("Release signature was not verified."); } assertDigest(admission.manifestDigest, "manifest digest"); assertTimestamp(now, "trusted time"); assertTimestamp(admission.expiresAt, "admission expiry"); if (Date.parse(now) >= Date.parse(admission.expiresAt)) throw new Error("Release admission is expired."); if (admission.predecessorManifestDigest !== null) assertDigest( admission.predecessorManifestDigest, "predecessor manifest digest", ); const state = validateProtectedState(protectedState); if ( admission.latestLedgerHeadHash !== state.latestLedgerHeadHash || admission.latestLedgerRevision !== state.latestLedgerRevision ) { throw new Error( "Release admission is stale relative to the protected ledger.", ); } if ( admission.predecessorManifestDigest !== state.currentManifestDigest || admission.expectedFence !== state.publisherFence + 1 ) { throw new Error("Release admission predecessor or fence is stale."); } const manifest = validateReleaseManifest(admission.manifestPayload, { now }); if ( manifest.expiresAt !== admission.expiresAt || manifest.currentManifestDigest !== admission.predecessorManifestDigest || manifest.predecessorManifestDigest !== admission.predecessorManifestDigest ) throw new Error("Admission does not bind the embedded release manifest."); return { admission, manifest }; } async function verifySignedReleaseAt({ signedManifestPath, trustPolicyPath, verifier, protectedState, now, }) { const [signedRelease, trustPolicy] = await Promise.all([ readCanonicalFile(signedManifestPath, "signed release"), readCanonicalFile(trustPolicyPath, "trust policy"), ]); const state = validateProtectedState(protectedState); if ( canonicalJsonDigest("portfolio-trust-policy/v1", trustPolicy) !== state.releaseTrustPolicyDigest ) { throw new Error( "Release trust-policy identity does not match protected deployment authority state.", ); } if (typeof verifier !== "function") throw new Error("A release signature verifier is required."); const result = await verifier({ signedRelease, trustPolicy, now, protectedState, }); const verified = validateAdmission(result, protectedState, now); return { ...verified, signedRelease, signedReleaseBytes: canonicalValueBytes(signedRelease), }; } export async function verifySignedRelease(input) { exactObject( input, ["signedManifestPath", "authorityRoot", "protectedState"], "production release verification request", ); if ( typeof input.authorityRoot !== "string" || !isAbsolute(input.authorityRoot) ) throw new Error("Protected authority root is invalid."); const now = new Date().toISOString(); const signedRelease = await readCanonicalFile( input.signedManifestPath, "signed release", ); const trustPolicyPath = join( input.authorityRoot, ".control", "release-trust.json", ); const trustPolicy = await readCanonicalFile( trustPolicyPath, "protected release trust policy", ); const state = validateProtectedState(input.protectedState); if ( canonicalJsonDigest("portfolio-trust-policy/v1", trustPolicy) !== state.releaseTrustPolicyDigest ) { throw new Error( "Release trust-policy identity does not match protected deployment authority state.", ); } const result = auditedReleaseVerifier({ signedRelease, trustPolicy, protectedState: input.protectedState, }); const verified = validateAdmission(result, input.protectedState, now); return { ...verified, signedRelease, signedReleaseBytes: canonicalValueBytes(signedRelease), }; } export async function verifySignedReleaseForTest(input) { exactObject( input, [ "signedManifestPath", "trustPolicyPath", "verifySignedReleaseEnvelope", "protectedState", "now", ], "test-only release verification request", ); return verifySignedReleaseAt({ signedManifestPath: input.signedManifestPath, trustPolicyPath: input.trustPolicyPath, verifier: input.verifySignedReleaseEnvelope, protectedState: input.protectedState, now: input.now, }); } export async function enumerateRegularFiles(root) { const rootPath = await realpath(root); const output = []; async function visit(directory, prefix = "") { const entries = await readdir(directory, { withFileTypes: true }); entries.sort((left, right) => Buffer.compare(Buffer.from(left.name), Buffer.from(right.name)), ); for (const entry of entries) { const path = join(directory, entry.name); const publicPath = prefix === "" ? entry.name : `${prefix}/${entry.name}`; if (entry.isSymbolicLink()) throw new Error( `Symlink is forbidden in release closure: ${publicPath}`, ); if (entry.isDirectory()) await visit(path, publicPath); else if (entry.isFile()) output.push({ path, publicPath }); else throw new Error( `Non-regular release input is forbidden: ${publicPath}`, ); } } await visit(rootPath); return output; } function splitTarPath(path) { const bytes = encoder.encode(path); if (bytes.length <= 100) return { name: path, prefix: "" }; const segments = path.split("/"); for (let index = segments.length - 1; index > 0; index -= 1) { const prefix = segments.slice(0, index).join("/"); const name = segments.slice(index).join("/"); if ( encoder.encode(prefix).length <= 155 && encoder.encode(name).length <= 100 ) return { name, prefix }; } throw new Error(`Release path is too long for deterministic ustar: ${path}`); } function writeString(target, offset, length, value) { const bytes = encoder.encode(value); if (bytes.length > length) throw new Error("Tar header value exceeds its field."); target.set(bytes, offset); } function writeOctal(target, offset, length, value) { const text = value.toString(8).padStart(length - 1, "0"); if (text.length !== length - 1) throw new Error("Tar numeric field overflow."); writeString(target, offset, length, `${text}\0`); } function tarHeader(path, size) { const header = new Uint8Array(512); const { name, prefix } = splitTarPath(path); writeString(header, 0, 100, name); writeOctal(header, 100, 8, 0o444); writeOctal(header, 108, 8, 0); writeOctal(header, 116, 8, 0); writeOctal(header, 124, 12, size); writeOctal(header, 136, 12, 0); header.fill(0x20, 148, 156); writeString(header, 156, 1, "0"); writeString(header, 257, 6, "ustar\0"); writeString(header, 263, 2, "00"); writeString(header, 345, 155, prefix); const checksum = header.reduce((sum, byte) => sum + byte, 0); const checksumText = checksum.toString(8).padStart(6, "0"); writeString(header, 148, 8, `${checksumText}\0 `); return header; } export function createDeterministicTar(files) { const ordered = [...files].sort((left, right) => Buffer.compare(Buffer.from(left.path), Buffer.from(right.path)), ); const seen = new Set(); const chunks = []; for (const file of ordered) { validatePublicPath(file.path); if (seen.has(file.path)) throw new Error(`Duplicate release archive path: ${file.path}`); seen.add(file.path); const bytes = new Uint8Array(file.bytes); chunks.push(tarHeader(file.path, bytes.length), bytes); const remainder = bytes.length % 512; if (remainder !== 0) chunks.push(new Uint8Array(512 - remainder)); } chunks.push(new Uint8Array(1024)); return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))); } function readString(bytes, start, length) { const end = bytes.indexOf(0, start); return new TextDecoder("utf-8", { fatal: true }).decode( bytes.subarray( start, end === -1 || end > start + length ? start + length : end, ), ); } function readOctal(bytes, start, length, label) { const text = readString(bytes, start, length).trim(); if (!/^[0-7]+$/u.test(text)) throw new Error(`Invalid tar ${label}.`); const value = Number.parseInt(text, 8); if (!Number.isSafeInteger(value) || value < 0) throw new Error(`Invalid tar ${label}.`); return value; } export function parseDeterministicTar(source) { const bytes = new Uint8Array(source); const files = []; const seen = new Set(); let offset = 0; let zeroBlocks = 0; while (offset + 512 <= bytes.length) { const header = bytes.subarray(offset, offset + 512); offset += 512; if (header.every((byte) => byte === 0)) { zeroBlocks += 1; if (zeroBlocks === 2) break; continue; } if (zeroBlocks !== 0) throw new Error("Malformed tar end marker."); const expectedChecksum = readOctal(header, 148, 8, "checksum"); const checksumHeader = new Uint8Array(header); checksumHeader.fill(0x20, 148, 156); if ( checksumHeader.reduce((sum, byte) => sum + byte, 0) !== expectedChecksum ) throw new Error("Tar checksum mismatch."); if ( readString(header, 257, 6) !== "ustar" || String.fromCharCode(header[156] ?? 0) !== "0" ) throw new Error("Only deterministic regular ustar entries are accepted."); const name = readString(header, 0, 100); const prefix = readString(header, 345, 155); const path = prefix === "" ? name : `${prefix}/${name}`; validatePublicPath(path); if (seen.has(path)) throw new Error(`Duplicate archive entry: ${path}`); seen.add(path); const size = readOctal(header, 124, 12, "size"); if (offset + size > bytes.length) throw new Error("Truncated tar entry."); files.push({ path, bytes: bytes.slice(offset, offset + size) }); offset += Math.ceil(size / 512) * 512; } if (zeroBlocks !== 2 || offset !== bytes.length) throw new Error("Tar archive has trailing or missing data."); return files; } export function packageIndexFor({ admission, manifest, signedBytes, sbomBytes, }) { const artifacts = [...manifest.artifacts].sort((left, right) => Buffer.compare(Buffer.from(left.path), Buffer.from(right.path)), ); return { schema: "portfolio-release-package/v1", buildId: manifest.buildId, releaseId: `${manifest.buildId}-${admission.manifestDigest}`, manifestDigest: admission.manifestDigest, predecessorManifestDigest: admission.predecessorManifestDigest, expiresAt: admission.expiresAt, expectedFence: admission.expectedFence, latestLedgerHeadHash: admission.latestLedgerHeadHash, latestLedgerRevision: admission.latestLedgerRevision, signedManifest: { path: ".release/signed-release-manifest.json", sha256: byteDigest(signedBytes), bytes: signedBytes.length, }, sbom: { path: ".release/sbom.cdx.json", sha256: byteDigest(sbomBytes), bytes: sbomBytes.length, }, artifacts, }; } export function validatePackageFiles(files) { const byPath = new Map(files.map((file) => [file.path, file])); const indexFile = byPath.get(".release/package-index.json"); if (indexFile === undefined) throw new Error("Release package index is missing."); const index = exactObject( parseCanonicalJson(indexFile.bytes), [ "schema", "buildId", "releaseId", "manifestDigest", "predecessorManifestDigest", "expiresAt", "expectedFence", "latestLedgerHeadHash", "latestLedgerRevision", "signedManifest", "sbom", "artifacts", ], "release package index", ); if (index.schema !== "portfolio-release-package/v1") throw new Error("Release package schema is invalid."); assertToken(index.buildId, "package buildId"); assertToken(index.releaseId, "package releaseId"); assertDigest(index.manifestDigest, "package manifest digest"); assertTimestamp(index.expiresAt, "package expiry"); if (!Number.isSafeInteger(index.expectedFence) || index.expectedFence < 1) throw new Error("Package fence is invalid."); const metadata = [index.signedManifest, index.sbom]; for (const record of metadata) { exactObject(record, ["path", "sha256", "bytes"], "package metadata record"); const file = byPath.get(record.path); if ( file === undefined || byteDigest(file.bytes) !== record.sha256 || file.bytes.length !== record.bytes ) throw new Error("Package metadata hash or size mismatch."); } if (!Array.isArray(index.artifacts) || index.artifacts.length === 0) throw new Error("Package artifact closure is empty."); const expectedPaths = new Set([ ".release/package-index.json", ".release/signed-release-manifest.json", ".release/sbom.cdx.json", ]); for (const artifact of index.artifacts) { exactObject( artifact, ["path", "sha256", "bytes", "encoding"], "package artifact", ); if ( typeof artifact.path !== "string" || artifact.path.startsWith(reservedPrefix) ) throw new Error("Package artifact uses a reserved path."); const file = byPath.get(artifact.path); if ( file === undefined || byteDigest(file.bytes) !== artifact.sha256 || file.bytes.length !== artifact.bytes ) throw new Error(`Artifact hash or size mismatch: ${artifact.path}`); expectedPaths.add(artifact.path); } if ( expectedPaths.size !== byPath.size || [...byPath.keys()].some((path) => !expectedPaths.has(path)) ) throw new Error( "Release package files do not exactly match the signed closure.", ); return { index, byPath }; } export async function writeArchiveFiles(files, targetRoot) { await mkdir(targetRoot, { recursive: false, mode: 0o700 }); const root = resolve(targetRoot); for (const file of files) { const destination = resolve(root, file.path); if ( relative(root, destination).startsWith(`..${sep}`) || destination === root ) throw new Error("Archive entry escapes staging root."); await mkdir(dirname(destination), { recursive: true, mode: 0o700 }); await writeFile(destination, file.bytes, { flag: "wx", mode: 0o600 }); } } export async function makeImmutable(root, uid, gid) { if ( !Number.isSafeInteger(uid) || uid < 0 || !Number.isSafeInteger(gid) || gid < 0 ) throw new Error("Release owner UID/GID is invalid."); async function visit(path) { const info = await lstat(path); if (info.isSymbolicLink()) throw new Error("Symlink found in immutable release."); if (info.uid !== uid || info.gid !== gid) throw new Error(`Release ownership mismatch at ${path}.`); if (info.isDirectory()) { for (const entry of await readdir(path)) await visit(join(path, entry)); await chmod(path, 0o555); } else if (info.isFile()) await chmod(path, 0o444); else throw new Error("Immutable release contains a non-file entry."); } await visit(root); } export async function assertReleaseRoot(rootPath) { if (typeof rootPath !== "string" || !isAbsolute(rootPath)) throw new Error("Release root must be an explicit absolute path."); const normalized = resolve(rootPath); if (normalized === "/" || normalized.split("/").filter(Boolean).length < 2) throw new Error("Broad release root is forbidden."); const info = await lstat(normalized); if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("Release root must be a real dedicated directory."); const marker = await readFile( join(normalized, ".portfolio-release-root"), "utf8", ).catch((error) => { if (error?.code === "ENOENT") throw new Error("Release root sentinel is missing or invalid."); throw error; }); if (marker !== "portfolio-release-root/v1\n") throw new Error("Release root sentinel is missing or invalid."); return normalized; } export async function atomicCanonicalWrite(path, value, mode = 0o600) { const temporary = `${path}.next-${process.pid}`; const handle = await open(temporary, "wx", mode); try { await handle.writeFile(canonicalValueBytes(value)); await handle.sync(); } finally { await handle.close(); } await rename(temporary, path); } export async function safeRemoveStage(root, stageName) { assertToken(stageName, "stage name"); const staging = join(root, "staging"); const target = join(staging, stageName); const rel = relative(staging, target); if (rel.startsWith("..") || isAbsolute(rel)) throw new Error("Stage cleanup escaped its root."); await rm(target, { recursive: true, force: true }); } export function digestBytes(bytes) { return createHash("sha256").update(bytes).digest("hex"); }