fix: close release provenance and privacy gaps

This commit is contained in:
2026-09-02 11:25:52 +03:00
parent 7da2b72933
commit 79e5d04395
17 changed files with 2271 additions and 704 deletions

View File

@@ -1,9 +1,9 @@
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
const request = JSON.parse(await readFile("/input/request.json", "utf8"));
const [a, b] = request.reports ?? [];
if (
export function matchReports(request) {
const [a, b] = request.reports ?? [];
if (
!a ||
!b ||
a.workerId !== "worker-a" ||
@@ -13,26 +13,27 @@ if (
a.inputDigest !== b.inputDigest ||
a.checkId !== b.checkId ||
a.normalizedOutputDigest !== b.normalizedOutputDigest ||
JSON.stringify(a.normalizedMembers) !== JSON.stringify(b.normalizedMembers) ||
JSON.stringify(a.normalizedMembers) !==
JSON.stringify(b.normalizedMembers) ||
a.verdict !== "pass" ||
b.verdict !== "pass"
)
throw new Error("Independent worker evidence does not agree.");
const rules = request.rules;
if (!Array.isArray(rules) || rules.length === 0)
)
throw new Error("Independent worker evidence does not agree and pass.");
const rules = request.rules;
if (!Array.isArray(rules) || rules.length === 0)
throw new Error("Matcher rules are required.");
const appliedRuleIds = rules
const appliedRuleIds = rules
.map((rule) => rule.id)
.sort((left, right) => Buffer.from(left).compare(Buffer.from(right)));
if (
if (
new Set(appliedRuleIds).size !== appliedRuleIds.length ||
rules.some((rule) => !Array.isArray(rule.deniedMemberDigests))
)
)
throw new Error("Matcher rules are invalid.");
const denied = new Set(rules.flatMap((rule) => rule.deniedMemberDigests));
if (a.normalizedMembers.some((member) => denied.has(member)))
const denied = new Set(rules.flatMap((rule) => rule.deniedMemberDigests));
if (a.normalizedMembers.some((member) => denied.has(member)))
throw new Error("Privacy rule matched normalized evidence.");
const result = {
return {
schema: "portfolio-privacy-matcher-result/v1",
inputDigest: a.inputDigest,
checkId: a.checkId,
@@ -42,8 +43,18 @@ const result = {
appliedRuleIds,
verdict: "pass",
workerImageDigests: [a.imageDigest, b.imageDigest],
};
await writeFile("/output/result.json", JSON.stringify(result), {
};
}
async function main() {
const result = matchReports(
JSON.parse(await readFile("/input/request.json", "utf8")),
);
await writeFile("/output/result.json", JSON.stringify(result), {
flag: "wx",
mode: 0o400,
});
});
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
await main();

View File

@@ -1,33 +1,45 @@
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
const request = JSON.parse(await readFile("/input/request.json", "utf8"));
if (
request.schema !== "portfolio-privacy-worker-request/v1" ||
export function inspectObject(request, bytes) {
if (
request?.schema !== "portfolio-privacy-worker-request/v1" ||
request.workerId !== "worker-a" ||
!/^[a-f0-9]{64}$/u.test(request.inputDigest)
)
!/^[a-f0-9]{64}$/u.test(request.inputDigest) ||
!/^[a-f0-9]{64}$/u.test(request.imageDigest) ||
!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/u.test(request.checkId)
)
throw new Error("Invalid worker request.");
const bytes = await readFile("/input/object");
const actual = createHash("sha256").update(bytes).digest("hex");
if (actual !== request.inputDigest) throw new Error("Input digest mismatch.");
const normalizedMembers = [`bytes:${actual}`];
const normalizedOutputDigest = createHash("sha256")
.update(Buffer.from(JSON.stringify(normalizedMembers)))
.digest("hex");
const output = {
const actual = createHash("sha256").update(bytes).digest("hex");
if (actual !== request.inputDigest) throw new Error("Input digest mismatch.");
const normalizedMembers = [`unsupported:${actual}`];
return {
schema: "portfolio-privacy-worker-report/v1",
workerId: "worker-a",
imageDigest: request.imageDigest,
toolchain: "bytes-js/v1",
toolchain: "scanner-unavailable-js/v1",
inputDigest: actual,
checkId: request.checkId,
normalizedOutputDigest,
normalizedOutputDigest: createHash("sha256")
.update(Buffer.from(JSON.stringify(normalizedMembers)))
.digest("hex"),
normalizedMembers,
verdict: "pass",
findingsDigest: "0".repeat(64),
};
await writeFile("/output/report.json", JSON.stringify(output), {
verdict: "unsupported",
findingsDigest: createHash("sha256")
.update("scanner-unavailable")
.digest("hex"),
};
}
async function main() {
const request = JSON.parse(await readFile("/input/request.json", "utf8"));
const output = inspectObject(request, await readFile("/input/object"));
await writeFile("/output/report.json", JSON.stringify(output), {
flag: "wx",
mode: 0o400,
});
});
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
await main();

View File

@@ -1,20 +1,23 @@
import hashlib
import json
with open('/input/request.json', 'r', encoding='utf-8') as handle:
request = json.load(handle)
if request.get('schema') != 'portfolio-privacy-worker-request/v1' or request.get('workerId') != 'worker-b':
def inspect_object(request, payload):
if request.get('schema') != 'portfolio-privacy-worker-request/v1' or request.get('workerId') != 'worker-b':
raise ValueError('Invalid worker request.')
with open('/input/object', 'rb') as handle:
payload = handle.read()
actual = hashlib.sha256(payload).hexdigest()
if actual != request.get('inputDigest'):
actual = hashlib.sha256(payload).hexdigest()
if actual != request.get('inputDigest'):
raise ValueError('Input digest mismatch.')
members = ['bytes:' + actual]
normalized = json.dumps(members, separators=(',', ':')).encode('utf-8')
output = {'schema': 'portfolio-privacy-worker-report/v1', 'workerId': 'worker-b', 'imageDigest': request['imageDigest'],
'toolchain': 'bytes-python/v1', 'inputDigest': actual, 'checkId': request['checkId'],
members = ['unsupported:' + actual]
normalized = json.dumps(members, separators=(',', ':')).encode('utf-8')
return {'schema': 'portfolio-privacy-worker-report/v1', 'workerId': 'worker-b', 'imageDigest': request['imageDigest'],
'toolchain': 'scanner-unavailable-python/v1', 'inputDigest': actual, 'checkId': request['checkId'],
'normalizedOutputDigest': hashlib.sha256(normalized).hexdigest(), 'normalizedMembers': members,
'verdict': 'pass', 'findingsDigest': '0' * 64}
with open('/output/report.json', 'x', encoding='utf-8') as handle:
json.dump(output, handle, separators=(',', ':'))
'verdict': 'unsupported', 'findingsDigest': hashlib.sha256(b'scanner-unavailable').hexdigest()}
if __name__ == '__main__':
with open('/input/request.json', 'r', encoding='utf-8') as handle:
request = json.load(handle)
with open('/input/object', 'rb') as handle:
payload = handle.read()
with open('/output/report.json', 'x', encoding='utf-8') as handle:
json.dump(inspect_object(request, payload), handle, separators=(',', ':'))

View File

@@ -5,3 +5,17 @@ export interface ArchiveFile {
export function createDeterministicTar(files: readonly ArchiveFile[]): Buffer;
export function parseDeterministicTar(source: Uint8Array): ArchiveFile[];
export function verifySignedRelease(input: {
signedManifestPath: string;
authorityRoot: string;
protectedState: Record<string, unknown>;
}): Promise<Record<string, unknown>>;
export function verifySignedReleaseForTest(input: {
signedManifestPath: string;
trustPolicyPath: string;
verifySignedReleaseEnvelope: (input: Record<string, unknown>) => unknown;
protectedState: Record<string, unknown>;
now: string;
}): Promise<Record<string, unknown>>;

View File

@@ -1,34 +1,62 @@
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 { pathToFileURL } from 'node:url';
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 {
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 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.`);
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])) {
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.`);
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.`);
if (
typeof value !== "string" ||
!timestamp.test(value) ||
Number.isNaN(Date.parse(value))
)
throw new Error(`${label} is invalid.`);
return value;
}
@@ -39,78 +67,222 @@ export function assertDigest(value, label) {
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.`);
if (!info.isFile() || info.isSymbolicLink())
throw new Error(`${label} must be a regular file.`);
return parseCanonicalJson(await readFile(path));
}
export async function loadVerifier(modulePath) {
if (!isAbsolute(modulePath)) throw new Error('Verifier module path must be absolute.');
const info = await lstat(modulePath);
if (!info.isFile() || info.isSymbolicLink()) throw new Error('Verifier module must be a regular file.');
const module = await import(/* @vite-ignore */ pathToFileURL(await realpath(modulePath)).href);
if (typeof module.verifySignedReleaseEnvelope !== 'function') throw new Error('Verifier module does not expose verifySignedReleaseEnvelope.');
return module.verifySignedReleaseEnvelope;
}
export function validateProtectedState(value) {
const state = exactObject(value, ['latestLedgerHeadHash', 'latestLedgerRevision', 'currentManifestDigest', 'publisherFence'], 'protected state');
assertDigest(state.latestLedgerHeadHash, 'protected ledger head');
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.');
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.');
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');
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.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.');
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.');
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 };
}
export async function verifySignedRelease({ signedManifestPath, trustPolicyPath, verifierModulePath, verifySignedReleaseEnvelope, protectedState, now }) {
async function verifySignedReleaseAt({
signedManifestPath,
trustPolicyPath,
verifier,
protectedState,
now,
}) {
const [signedRelease, trustPolicy] = await Promise.all([
readCanonicalFile(signedManifestPath, 'signed release'), readCanonicalFile(trustPolicyPath, 'trust policy'),
readCanonicalFile(signedManifestPath, "signed release"),
readCanonicalFile(trustPolicyPath, "trust policy"),
]);
const verifier = verifySignedReleaseEnvelope ?? await loadVerifier(verifierModulePath);
if (typeof verifier !== 'function') throw new Error('A release signature verifier is required.');
const result = await verifier({ signedRelease, trustPolicy, now, protectedState });
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) };
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 = '') {
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)));
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}`);
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}`);
else
throw new Error(
`Non-regular release input is forbidden: ${publicPath}`,
);
}
}
await visit(rootPath);
@@ -119,25 +291,31 @@ export async function enumerateRegularFiles(root) {
function splitTarPath(path) {
const bytes = encoder.encode(path);
if (bytes.length <= 100) return { name: path, prefix: '' };
const segments = path.split('/');
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 };
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.');
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.');
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`);
}
@@ -151,23 +329,26 @@ function tarHeader(path, size) {
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, 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');
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 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}`);
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);
@@ -180,14 +361,20 @@ export function createDeterministicTar(files) {
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));
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}.`);
if (!Number.isSafeInteger(value) || value < 0)
throw new Error(`Invalid tar ${label}.`);
return value;
}
@@ -205,31 +392,46 @@ export function parseDeterministicTar(source) {
if (zeroBlocks === 2) break;
continue;
}
if (zeroBlocks !== 0) throw new Error('Malformed tar end marker.');
const expectedChecksum = readOctal(header, 148, 8, 'checksum');
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.');
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}`;
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.');
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.');
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)));
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',
schema: "portfolio-release-package/v1",
buildId: manifest.buildId,
releaseId: `${manifest.buildId}-${admission.manifestDigest}`,
manifestDigest: admission.manifestDigest,
@@ -238,42 +440,96 @@ export function packageIndexFor({ admission, manifest, signedBytes, sbomBytes })
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 },
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 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');
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 (
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']);
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.');
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}`);
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.');
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 };
}
@@ -282,44 +538,64 @@ export async function writeArchiveFiles(files, targetRoot) {
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.');
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 });
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.');
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.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.');
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.');
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.');
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.');
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.');
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);
const handle = await open(temporary, "wx", mode);
try {
await handle.writeFile(canonicalValueBytes(value));
await handle.sync();
@@ -330,14 +606,15 @@ export async function atomicCanonicalWrite(path, value, mode = 0o600) {
}
export async function safeRemoveStage(root, stageName) {
assertToken(stageName, 'stage name');
const staging = join(root, 'staging');
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.');
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');
return createHash("sha256").update(bytes).digest("hex");
}

View File

@@ -140,85 +140,68 @@ async function writeOnce(root, digest, bytes) {
}
}
export function reconcileScan({
expectedChecks,
function closureDigest(value, schema, label) {
const closure = object(value, label);
exact(closure, ["schema", "members"], label);
if (closure.schema !== schema || !Array.isArray(closure.members))
throw new Error(`${label} is invalid.`);
return canonicalJsonDigest(schema, closure);
}
export function reconcileScan(input) {
const request = object(input, "reconciliation request");
exact(
request,
[
"sourceClosure",
"publicObjectClosure",
"reports",
"matcherResults",
"candidate",
],
"reconciliation request",
);
const {
sourceClosure,
publicObjectClosure,
reports,
matcherResults,
candidate,
}) {
if (
!Array.isArray(expectedChecks) ||
!Array.isArray(reports) ||
!Array.isArray(matcherResults) ||
expectedChecks.length === 0
)
} = request;
if (!Array.isArray(reports) || !Array.isArray(matcherResults))
throw new Error("Reconciliation input is invalid.");
const ordered = [...expectedChecks].sort((a, b) =>
Buffer.from(`${a.inputDigest}\0${a.checkId}`).compare(
Buffer.from(`${b.inputDigest}\0${b.checkId}`),
),
const sourceClosureDigest = closureDigest(
sourceClosure,
"portfolio-publication-closure/v1",
"source closure",
);
const publicObjectClosureDigest = closureDigest(
publicObjectClosure,
"portfolio-public-object-closure/v1",
"public object closure",
);
if (
!stringsEqual(expectedChecks, ordered) ||
new Set(expectedChecks.map((v) => `${v.inputDigest}\0${v.checkId}`))
.size !== expectedChecks.length
)
throw new Error("Expected check closure must be unique and byte sorted.");
if (
reports.length !== expectedChecks.length * 2 ||
matcherResults.length !== expectedChecks.length
)
throw new Error("Missing or extra privacy evidence.");
const checks = [];
expectedChecks.forEach((expected, index) => {
if (!isSha256Hex(expected.inputDigest) || !safeToken.test(expected.checkId))
throw new Error("Expected check is invalid.");
const a = validateWorker(reports[index * 2], "worker-a");
const b = validateWorker(reports[index * 2 + 1], "worker-b");
const match = validateMatcher(matcherResults[index]);
for (const item of [a, b, match])
if (
item.inputDigest !== expected.inputDigest ||
item.checkId !== expected.checkId
)
candidate?.sourceClosureDigest !== sourceClosureDigest ||
candidate?.publicObjectClosureDigest !== publicObjectClosureDigest
) {
throw new Error(
"Privacy evidence is missing, reordered, or bound to another check.",
"Candidate does not bind the exact canonical source/public closure.",
);
if (a.imageDigest === b.imageDigest || a.toolchain === b.toolchain)
throw new Error("Privacy workers are not heterogeneous.");
if (
a.verdict !== "pass" ||
b.verdict !== "pass" ||
match.verdict !== "pass"
)
throw new Error("Every worker and matcher must independently pass.");
if (
a.normalizedOutputDigest !== b.normalizedOutputDigest ||
a.normalizedOutputDigest !== match.normalizedOutputDigest ||
!stringsEqual(a.normalizedMembers, b.normalizedMembers) ||
!stringsEqual(a.normalizedMembers, match.normalizedMembers) ||
!stringsEqual(match.workerImageDigests, [a.imageDigest, b.imageDigest])
)
throw new Error("Independent normalized evidence disagrees.");
checks.push({
inputDigest: expected.inputDigest,
checkId: expected.checkId,
normalizedOutputDigest: a.normalizedOutputDigest,
normalizedMembers: a.normalizedMembers,
workerReports: [
canonicalJsonDigest("portfolio-privacy-worker-report/v1", a),
canonicalJsonDigest("portfolio-privacy-worker-report/v1", b),
],
matcherResult: canonicalJsonDigest(
"portfolio-privacy-matcher-result/v1",
match,
),
ruleSetDigest: match.ruleSetDigest,
appliedRuleIds: match.appliedRuleIds,
});
});
}
if (publicObjectClosure.members.length !== 0) {
throw new Error(
"Verified privacy scanners are unavailable; every non-empty public object closure fails closed.",
);
}
if (reports.length !== 0 || matcherResults.length !== 0)
throw new Error(
"Empty public object closure cannot carry fabricated scan evidence.",
);
const checks = [];
const evidence = {
schema: "portfolio-scan-evidence/v1",
sourceClosureDigest,
publicObjectClosureDigest,
coverageCount: checks.length,
passedCount: checks.length,
checks,

View File

@@ -107,6 +107,18 @@ export function liveContainerStatus() {
};
}
export function requireLiveContainerValidation(value) {
if (
value?.schema !== "portfolio-live-container-validation/v1" ||
value.status !== "verified"
) {
throw new Error(
"Live privacy scanners are not verified; pending or unsupported scanner state is a hard release failure.",
);
}
return value;
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href

View File

@@ -11,6 +11,11 @@ import {
parseCanonicalJson,
} from "../../src/security/canonical-digest.ts";
import { validateReleaseManifest } from "../../src/security/release-manifest.ts";
import {
APPROVAL_PROJECTION_ATTESTATION_SCHEMA,
APPROVAL_PROJECTION_ATTESTATION_SCOPE,
verifyApprovalProjectionAttestation,
} from "../../src/security/approval-closure.ts";
const rootContract = {
approval: {
@@ -18,7 +23,7 @@ const rootContract = {
recovery: ["freeze-publication", "audit-checkpoint", "fresh-approvals"],
},
projection: {
scope: "portfolio-approval-projection/v1",
scope: APPROVAL_PROJECTION_ATTESTATION_SCOPE,
recovery: ["freeze-publication", "revoke-epoch", "fresh-evidence"],
},
privacyWorkload: {
@@ -62,6 +67,7 @@ const candidateFields = [
"latestLedgerHeadHash",
"latestLedgerRevision",
"sourceClosureDigest",
"publicObjectClosureDigest",
"publicRefMap",
"lfsOids",
"releaseArchives",
@@ -78,21 +84,6 @@ const signatureFields = [
"value",
];
const attestationPayloadFields = {
projection: [
"schema",
"keyId",
"issuerEpoch",
"scope",
"issuedAt",
"expiresAt",
"digest",
"productionInputRoot",
"approvedRecordRoot",
"approvedAssetRoot",
"approvalProofSetDigest",
"latestLedgerHeadHash",
"latestLedgerRevision",
],
privacyWorkload: [
"schema",
"keyId",
@@ -293,6 +284,7 @@ function validateCandidate(value, now) {
throw new Error("Candidate schema is invalid.");
digest(candidate.latestLedgerHeadHash, "latestLedgerHeadHash");
digest(candidate.sourceClosureDigest, "sourceClosureDigest");
digest(candidate.publicObjectClosureDigest, "publicObjectClosureDigest");
digest(candidate.buildProvenanceDigest, "buildProvenanceDigest");
digest(candidate.privacyPolicyDigest, "privacyPolicyDigest");
if (
@@ -354,18 +346,30 @@ function validateEvidence(value, expectedDigest) {
const evidence = object(value, "scan evidence");
exact(
evidence,
["schema", "coverageCount", "passedCount", "checks"],
[
"schema",
"sourceClosureDigest",
"publicObjectClosureDigest",
"coverageCount",
"passedCount",
"checks",
],
"scan evidence",
);
if (
evidence.schema !== "portfolio-scan-evidence/v1" ||
!Number.isSafeInteger(evidence.coverageCount) ||
evidence.coverageCount < 1 ||
evidence.coverageCount < 0 ||
evidence.passedCount !== evidence.coverageCount ||
!Array.isArray(evidence.checks) ||
evidence.checks.length !== evidence.coverageCount
)
throw new Error("Scan evidence coverage is incomplete.");
digest(evidence.sourceClosureDigest, "scan source closure digest");
digest(
evidence.publicObjectClosureDigest,
"scan public object closure digest",
);
const keys = [];
for (const raw of evidence.checks) {
const check = object(raw, "scan evidence check");
@@ -452,6 +456,8 @@ function verifyCandidateAuthority({
trustPolicy,
now,
protectedState,
sourceClosure,
publicObjectClosure,
}) {
if (trustPolicy.publicationFrozen)
throw new Error("Publication is frozen by trust policy.");
@@ -462,6 +468,39 @@ function verifyCandidateAuthority({
now,
);
const state = validateProtectedState(protectedState);
const sourceRecord = object(sourceClosure, "source closure");
exact(sourceRecord, ["schema", "members"], "source closure");
if (
sourceRecord.schema !== "portfolio-publication-closure/v1" ||
!Array.isArray(sourceRecord.members)
)
throw new Error("Source closure is invalid.");
const publicRecord = object(publicObjectClosure, "public object closure");
exact(publicRecord, ["schema", "members"], "public object closure");
if (
publicRecord.schema !== "portfolio-public-object-closure/v1" ||
!Array.isArray(publicRecord.members)
)
throw new Error("Public object closure is invalid.");
const sourceClosureDigest = canonicalJsonDigest(
"portfolio-publication-closure/v1",
sourceRecord,
);
const publicObjectClosureDigest = canonicalJsonDigest(
"portfolio-public-object-closure/v1",
publicRecord,
);
if (
record.sourceClosureDigest !== sourceClosureDigest ||
record.publicObjectClosureDigest !== publicObjectClosureDigest
)
throw new Error(
"Candidate does not bind exact source/public closure bytes.",
);
if (publicRecord.members.length !== 0)
throw new Error(
"Verified scanners are unavailable; non-empty public object closure cannot be signed.",
);
if (
record.latestLedgerHeadHash !== state.latestLedgerHeadHash ||
record.latestLedgerRevision !== state.latestLedgerRevision
@@ -474,7 +513,19 @@ function verifyCandidateAuthority({
throw new Error(
"Candidate predecessor does not equal protected current manifest.",
);
validateEvidence(evidence, releaseManifest.scanEvidenceDigest);
const validatedEvidence = validateEvidence(
evidence,
releaseManifest.scanEvidenceDigest,
);
if (
validatedEvidence.sourceClosureDigest !== sourceClosureDigest ||
validatedEvidence.publicObjectClosureDigest !== publicObjectClosureDigest ||
validatedEvidence.coverageCount !== 0 ||
validatedEvidence.checks.length !== 0
)
throw new Error(
"Scan evidence does not exactly cover the canonical empty public object closure.",
);
const candidateDigest = canonicalJsonDigest(
"portfolio-candidate-release-manifest/v1",
record,
@@ -483,12 +534,20 @@ function verifyCandidateAuthority({
"portfolio-scan-evidence/v1",
evidence,
);
const projection = verifyAttestation(
const projectionRoot = trustPolicy.roots.projection;
const projection = verifyApprovalProjectionAttestation(
attestations.projection,
trustPolicy.roots.projection,
"portfolio-approval-projection-attestation/v1",
"projection",
{
now,
latestLedgerHeadHash: record.latestLedgerHeadHash,
latestLedgerRevision: record.latestLedgerRevision,
trustedKey: {
keyId: projectionRoot.keyId,
issuerEpoch: projectionRoot.issuerEpoch,
scope: APPROVAL_PROJECTION_ATTESTATION_SCOPE,
publicKey: createPublicKey(projectionRoot.publicKeyPem),
},
},
);
const workload = verifyAttestation(
attestations.privacyWorkload,
@@ -516,13 +575,18 @@ function verifyCandidateAuthority({
["approvedRecordRoot", releaseManifest.approvedRecordRoot],
["approvedAssetRoot", releaseManifest.approvedAssetRoot],
["approvalProofSetDigest", releaseManifest.approvalProofSetDigest],
["latestLedgerHeadHash", record.latestLedgerHeadHash],
["latestLedgerRevision", record.latestLedgerRevision],
["liveLedgerHeadHash", record.latestLedgerHeadHash],
["liveLedgerRevision", record.latestLedgerRevision],
]) {
if (projection[field] !== expected)
throw new Error(`Approval projection mismatch: ${field}.`);
}
if (projection.digest !== releaseManifest.approvalProjectionAttestationDigest)
if (
canonicalJsonDigest(
"portfolio-approval-projection-attestation-envelope/v1",
attestations.projection,
) !== releaseManifest.approvalProjectionAttestationDigest
)
throw new Error("Projection attestation digest binding is invalid.");
if (
workload.candidateDigest !== candidateDigest ||
@@ -572,9 +636,9 @@ async function externalPrivateKey(path) {
return key;
}
export async function signReleaseCandidate(input) {
async function signReleaseCandidateAt(input, now) {
const trustPolicy = validateTrustPolicy(input.trustPolicy);
const verified = verifyCandidateAuthority({ ...input, trustPolicy });
const verified = verifyCandidateAuthority({ ...input, trustPolicy, now });
const key = await externalPrivateKey(input.releaseKeyPath);
const releaseRoot = trustPolicy.roots.release;
const bytes = canonicalValueBytes(verified.record);
@@ -605,7 +669,20 @@ export async function signReleaseCandidate(input) {
};
}
export function verifySignedReleaseEnvelope({
export async function signReleaseCandidate(input) {
if (Object.prototype.hasOwnProperty.call(input, "now"))
throw new Error("Production signer does not accept caller-selected now.");
return signReleaseCandidateAt(input, new Date().toISOString());
}
export async function signReleaseCandidateForTest(input) {
if (typeof input?.now !== "string")
throw new Error("Test-only signer requires an explicit clock.");
const { now, ...rest } = input;
return signReleaseCandidateAt(rest, now);
}
function verifySignedReleaseEnvelopeAt({
signedRelease,
trustPolicy: trustValue,
now,
@@ -672,6 +749,21 @@ export function verifySignedReleaseEnvelope({
};
}
export function verifySignedReleaseEnvelope(input) {
if (Object.prototype.hasOwnProperty.call(input, "now"))
throw new Error("Production verifier does not accept caller-selected now.");
return verifySignedReleaseEnvelopeAt({
...input,
now: new Date().toISOString(),
});
}
export function verifySignedReleaseEnvelopeForTest(input) {
if (typeof input?.now !== "string")
throw new Error("Test-only verifier requires an explicit clock.");
return verifySignedReleaseEnvelopeAt(input);
}
async function main() {
const args = process.argv.slice(2);
if (args.length !== 4 || args[0] !== "--request" || args[2] !== "--key")

View File

@@ -1,55 +1,110 @@
#!/usr/bin/env node
import { createPublicKey } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';
import { createPublicKey } from "node:crypto";
import { readFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
import { computeApprovalClosure } from '../../src/security/approval-closure.ts';
import { canonicalValueBytes, isSha256Hex, parseCanonicalJson } from '../../src/security/canonical-digest.ts';
import { computeProductionInputRoot } from '../../src/security/production-input-root.ts';
import {
APPROVAL_PROJECTION_ATTESTATION_SCHEMA,
APPROVAL_PROJECTION_ATTESTATION_SCOPE,
computeApprovalClosure,
} from "../../src/security/approval-closure.ts";
import {
canonicalValueBytes,
isSha256Hex,
parseCanonicalJson,
} from "../../src/security/canonical-digest.ts";
import { computeProductionInputRoot } from "../../src/security/production-input-root.ts";
const expectedRequestFields = [
'schema', 'now', 'latestLedgerHeadHash', 'latestLedgerRevision', 'trustedKeys', 'contentRecords', 'assetRecords', 'inputDigests',
"schema",
"latestLedgerHeadHash",
"latestLedgerRevision",
"trustedKeys",
"ledgerProof",
"contentRecords",
"assetRecords",
"inputDigests",
].sort();
const expectedKeyFields = ['keyId', 'issuerEpoch', 'scope', 'authorizedRoles', 'publicKeyPem'].sort();
const expectedInputs = ['PortfolioContent', 'PreparedAssetSet', 'PublicAssetManifest', 'SiteConfig'];
const expectedKeyFields = [
"keyId",
"issuerEpoch",
"scope",
"authorizedRoles",
"publicKeyPem",
].sort();
const expectedInputs = [
"PortfolioContent",
"PreparedAssetSet",
"PublicAssetManifest",
"SiteConfig",
];
function exactObject(value, fields, label) {
if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must be an object.`);
if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(fields)) throw new Error(`${label} contains missing or unknown fields.`);
if (value === null || typeof value !== "object" || Array.isArray(value))
throw new Error(`${label} must be an object.`);
if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(fields))
throw new Error(`${label} contains missing or unknown fields.`);
return value;
}
function requestPath(args) {
if (args.length !== 2 || args[0] !== '--request' || args[1].length === 0) throw new Error('Usage: verify-approvals.mjs --request <canonical-request.json>');
if (args.length !== 2 || args[0] !== "--request" || args[1].length === 0)
throw new Error(
"Usage: verify-approvals.mjs --request <canonical-request.json>",
);
return args[1];
}
export function verifyApprovalRequest(source) {
const request = exactObject(parseCanonicalJson(source), expectedRequestFields, 'request');
if (request.schema !== 'portfolio-approval-verification-request/v1') throw new Error('Request schema is invalid.');
if (!Array.isArray(request.trustedKeys) || !Array.isArray(request.contentRecords) || !Array.isArray(request.assetRecords)) throw new Error('Approval record lists are invalid.');
function verifyApprovalRequestAt(source, now) {
const request = exactObject(
parseCanonicalJson(source),
expectedRequestFields,
"request",
);
if (request.schema !== "portfolio-approval-verification-request/v1")
throw new Error("Request schema is invalid.");
if (
!Array.isArray(request.trustedKeys) ||
!Array.isArray(request.contentRecords) ||
!Array.isArray(request.assetRecords)
)
throw new Error("Approval record lists are invalid.");
const trustedKeys = request.trustedKeys.map((candidate) => {
const key = exactObject(candidate, expectedKeyFields, 'trusted key');
if (key.scope !== 'portfolio-approval-decision/v1' || typeof key.keyId !== 'string' ||
!Number.isSafeInteger(key.issuerEpoch) || !Array.isArray(key.authorizedRoles) ||
key.authorizedRoles.some((role) => typeof role !== 'string') || typeof key.publicKeyPem !== 'string') {
throw new Error('Trusted approval key is invalid.');
const key = exactObject(candidate, expectedKeyFields, "trusted key");
if (
key.scope !== "portfolio-approval-decision/v1" ||
typeof key.keyId !== "string" ||
!Number.isSafeInteger(key.issuerEpoch) ||
!Array.isArray(key.authorizedRoles) ||
key.authorizedRoles.some((role) => typeof role !== "string") ||
typeof key.publicKeyPem !== "string"
) {
throw new Error("Trusted approval key is invalid.");
}
return {
keyId: key.keyId, issuerEpoch: key.issuerEpoch, scope: key.scope,
authorizedRoles: key.authorizedRoles, publicKey: createPublicKey(key.publicKeyPem),
keyId: key.keyId,
issuerEpoch: key.issuerEpoch,
scope: key.scope,
authorizedRoles: key.authorizedRoles,
publicKey: createPublicKey(key.publicKeyPem),
};
});
const inputDigests = exactObject(request.inputDigests, expectedInputs, 'inputDigests');
if (Object.values(inputDigests).some((digest) => !isSha256Hex(digest))) throw new Error('Input digest is invalid.');
const inputDigests = exactObject(
request.inputDigests,
expectedInputs,
"inputDigests",
);
if (Object.values(inputDigests).some((digest) => !isSha256Hex(digest)))
throw new Error("Input digest is invalid.");
const closure = computeApprovalClosure({
contentRecords: request.contentRecords,
assetRecords: request.assetRecords,
trust: {
now: request.now,
now,
latestLedgerHeadHash: request.latestLedgerHeadHash,
latestLedgerRevision: request.latestLedgerRevision,
trustedKeys,
ledgerProof: request.ledgerProof,
},
});
const productionInputRoot = computeProductionInputRoot({
@@ -59,7 +114,9 @@ export function verifyApprovalRequest(source) {
siteConfigDigest: inputDigests.SiteConfig,
});
return {
schema: 'portfolio-approval-verification-result/v1',
schema: "portfolio-approval-verification-result/v1",
projectionSchema: APPROVAL_PROJECTION_ATTESTATION_SCHEMA,
projectionScope: APPROVAL_PROJECTION_ATTESTATION_SCOPE,
approvedRecordRoot: closure.approvedRecordRoot,
approvedAssetRoot: closure.approvedAssetRoot,
approvalProofSetDigest: closure.approvalProofSetDigest,
@@ -70,16 +127,36 @@ export function verifyApprovalRequest(source) {
};
}
export function verifyApprovalRequest(source) {
return verifyApprovalRequestAt(source, new Date().toISOString());
}
export function verifyApprovalRequestForTest(source, clock) {
if (
clock === null ||
typeof clock !== "object" ||
Object.keys(clock).length !== 1 ||
typeof clock.now !== "string"
) {
throw new Error("Test-only approval clock is invalid.");
}
return verifyApprovalRequestAt(source, clock.now);
}
async function main() {
const path = requestPath(process.argv.slice(2));
const result = verifyApprovalRequest(await readFile(path));
process.stdout.write(Buffer.from(canonicalValueBytes(result)));
process.stdout.write('\n');
process.stdout.write("\n");
}
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
if (
process.argv[1] !== undefined &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
main().catch((error) => {
const message = error instanceof Error ? error.message : 'Unknown verification failure.';
const message =
error instanceof Error ? error.message : "Unknown verification failure.";
process.stderr.write(`Approval verification failed: ${message}\n`);
process.exitCode = 1;
});

View File

@@ -1,11 +1,22 @@
import type { KeyLike } from 'node:crypto';
import { canonicalJsonDigest, isSha256Hex, verifyEd25519Envelope, type Sha256Hex } from './canonical-digest.ts';
import type { KeyLike } from "node:crypto";
import {
canonicalJsonDigest,
isSha256Hex,
verifyEd25519Envelope,
type Sha256Hex,
} from "./canonical-digest.ts";
import {
type ApprovalEnvelope,
type ApprovalTrustPolicy,
verifyApprovalLedger,
verifyApprovalEnvelope,
} from './approval-envelope.ts';
import { computeProductionInputRoot } from './production-input-root.ts';
} from "./approval-envelope.ts";
import { computeProductionInputRoot } from "./production-input-root.ts";
export const APPROVAL_PROJECTION_ATTESTATION_SCHEMA =
"portfolio-approval-projection-attestation/v1" as const;
export const APPROVAL_PROJECTION_ATTESTATION_SCOPE =
APPROVAL_PROJECTION_ATTESTATION_SCHEMA;
export interface ContentApprovalRecord {
recordType: string;
@@ -29,14 +40,19 @@ export interface ApprovalClosure {
}
export interface ApprovalProjectionAttestationPayload {
schema: 'portfolio-approval-projection-attestation/v1';
schema: typeof APPROVAL_PROJECTION_ATTESTATION_SCHEMA;
keyId: string;
issuerEpoch: number;
scope: 'portfolio-approval-projection-attestation/v1';
scope: typeof APPROVAL_PROJECTION_ATTESTATION_SCOPE;
approvedRecordRoot: string;
approvedAssetRoot: string;
approvalProofSetDigest: string;
inputDigests: { PortfolioContent: string; PublicAssetManifest: string; PreparedAssetSet: string; SiteConfig: string };
inputDigests: {
PortfolioContent: string;
PublicAssetManifest: string;
PreparedAssetSet: string;
SiteConfig: string;
};
productionInputRoot: string;
liveLedgerHeadHash: string;
liveLedgerRevision: number;
@@ -47,16 +63,20 @@ export interface ApprovalProjectionAttestationPayload {
}
export interface ApprovalProjectionAttestationEnvelope {
schema: 'portfolio-approval-projection-attestation-envelope/v1';
schema: "portfolio-approval-projection-attestation-envelope/v1";
payload: ApprovalProjectionAttestationPayload;
signature: { algorithm: 'Ed25519'; encoding: 'base64url-nopad'; value: string };
signature: {
algorithm: "Ed25519";
encoding: "base64url-nopad";
value: string;
};
}
const encoder = new TextEncoder();
function utf8TupleCompare(left: string[], right: string[]): number {
for (let field = 0; field < Math.max(left.length, right.length); field += 1) {
const a = encoder.encode(left[field] ?? '');
const b = encoder.encode(right[field] ?? '');
const a = encoder.encode(left[field] ?? "");
const b = encoder.encode(right[field] ?? "");
for (let index = 0; index < Math.min(a.length, b.length); index += 1) {
if (a[index] !== b[index]) return (a[index] ?? 0) - (b[index] ?? 0);
}
@@ -70,51 +90,122 @@ export function computeApprovalClosure(input: {
assetRecords: AssetApprovalRecord[];
trust: ApprovalTrustPolicy;
}): ApprovalClosure {
const latestLedger = verifyApprovalLedger(input.trust);
const ids = new Set<string>();
const expiries: string[] = [];
const contentTuples = input.contentRecords.map((record) => {
const contentTuples = input.contentRecords
.map((record) => {
const typedId = record.publicRecordId;
if (ids.has(typedId)) throw new Error('Duplicate content approval ID.');
if (ids.has(typedId)) throw new Error("Duplicate content approval ID.");
ids.add(typedId);
if (!isSha256Hex(record.approvedPublicRecordHash)) throw new Error('Content record hash is invalid.');
if (!isSha256Hex(record.approvedPublicRecordHash))
throw new Error("Content record hash is invalid.");
const payload = verifyApprovalEnvelope(record.envelope, input.trust);
if (payload.recordClass !== 'content' || payload.recordType !== record.recordType ||
if (
payload.recordClass !== "content" ||
payload.recordType !== record.recordType ||
payload.publicRecordIdOrAssetId !== record.publicRecordId ||
payload.approvedPublicRecordHashOrManifestRecordHash !== record.approvedPublicRecordHash ||
payload.sanitizedOutputHashOrNull !== null) {
throw new Error('Content approval does not bind the exact emitted record.');
payload.approvedPublicRecordHashOrManifestRecordHash !==
record.approvedPublicRecordHash ||
payload.sanitizedOutputHashOrNull !== null
) {
throw new Error(
"Content approval does not bind the exact emitted record.",
);
}
expiries.push(payload.expiresAt);
const envelopeDigest = canonicalJsonDigest('portfolio-approval-envelope-digest/v1', record.envelope);
return [record.recordType, record.publicRecordId, record.approvedPublicRecordHash, envelopeDigest] as const;
}).sort((a, b) => utf8TupleCompare([a[0], a[1]], [b[0], b[1]]));
const envelopeDigest = canonicalJsonDigest(
"portfolio-approval-envelope-digest/v1",
record.envelope,
);
return [
record.recordType,
record.publicRecordId,
record.approvedPublicRecordHash,
envelopeDigest,
] as const;
})
.sort((a, b) => utf8TupleCompare([a[0], a[1]], [b[0], b[1]]));
const assetTuples = input.assetRecords.map((record) => {
const assetTuples = input.assetRecords
.map((record) => {
const typedId = record.assetId;
if (ids.has(typedId)) throw new Error('Duplicate asset approval ID.');
if (ids.has(typedId)) throw new Error("Duplicate asset approval ID.");
ids.add(typedId);
if (!isSha256Hex(record.approvedManifestRecordHash) || !isSha256Hex(record.sanitizedOutputHash)) throw new Error('Asset record hash is invalid.');
if (
!isSha256Hex(record.approvedManifestRecordHash) ||
!isSha256Hex(record.sanitizedOutputHash)
)
throw new Error("Asset record hash is invalid.");
const payload = verifyApprovalEnvelope(record.envelope, input.trust);
if (payload.recordClass !== 'asset' || payload.recordType !== '' ||
if (
payload.recordClass !== "asset" ||
payload.recordType !== "" ||
payload.publicRecordIdOrAssetId !== record.assetId ||
payload.approvedPublicRecordHashOrManifestRecordHash !== record.approvedManifestRecordHash ||
payload.sanitizedOutputHashOrNull !== record.sanitizedOutputHash) {
throw new Error('Asset approval does not bind the exact emitted record and bytes.');
payload.approvedPublicRecordHashOrManifestRecordHash !==
record.approvedManifestRecordHash ||
payload.sanitizedOutputHashOrNull !== record.sanitizedOutputHash
) {
throw new Error(
"Asset approval does not bind the exact emitted record and bytes.",
);
}
expiries.push(payload.expiresAt);
const envelopeDigest = canonicalJsonDigest('portfolio-approval-envelope-digest/v1', record.envelope);
return [record.assetId, record.approvedManifestRecordHash, record.sanitizedOutputHash, envelopeDigest] as const;
}).sort((a, b) => utf8TupleCompare([a[0]], [b[0]]));
const envelopeDigest = canonicalJsonDigest(
"portfolio-approval-envelope-digest/v1",
record.envelope,
);
return [
record.assetId,
record.approvedManifestRecordHash,
record.sanitizedOutputHash,
envelopeDigest,
] as const;
})
.sort((a, b) => utf8TupleCompare([a[0]], [b[0]]));
const proofList = [
...contentTuples.map((tuple) => ['content', tuple[0], tuple[1], tuple[3]] as const),
...assetTuples.map((tuple) => ['asset', '', tuple[0], tuple[3]] as const),
...contentTuples.map(
(tuple) => ["content", tuple[0], tuple[1], tuple[3]] as const,
),
...assetTuples.map((tuple) => ["asset", "", tuple[0], tuple[3]] as const),
].sort((a, b) => utf8TupleCompare([a[0], a[1], a[2]], [b[0], b[1], b[2]]));
const supplied = new Set([
...input.contentRecords.map(
(record) => `content\0${record.recordType}\0${record.publicRecordId}`,
),
...input.assetRecords.map((record) => `asset\0\0${record.assetId}`),
]);
const required = new Set(
[...latestLedger.entries()]
.filter(
([, envelope]) =>
envelope.payload.decision === "approve" &&
Date.parse(input.trust.now) < Date.parse(envelope.payload.expiresAt),
)
.map(([typedId]) => typedId),
);
if (
supplied.size !== required.size ||
[...supplied].some((typedId) => !required.has(typedId))
) {
throw new Error(
"Approval records are not the exact latest authenticated approved ledger projection.",
);
}
return {
approvedRecordRoot: canonicalJsonDigest('portfolio-record-closure/v1', { records: contentTuples }),
approvedAssetRoot: canonicalJsonDigest('portfolio-asset-closure/v1', { assets: assetTuples }),
approvalProofSetDigest: canonicalJsonDigest('portfolio-approval-proof-set/v1', { envelopes: proofList }),
approvedRecordRoot: canonicalJsonDigest("portfolio-record-closure/v1", {
records: contentTuples,
}),
approvedAssetRoot: canonicalJsonDigest("portfolio-asset-closure/v1", {
assets: assetTuples,
}),
approvalProofSetDigest: canonicalJsonDigest(
"portfolio-approval-proof-set/v1",
{ envelopes: proofList },
),
earliestApprovalExpiry: expiries.sort()[0] ?? null,
};
}
@@ -125,43 +216,129 @@ export function verifyApprovalProjectionAttestation(
now: string;
latestLedgerHeadHash: string;
latestLedgerRevision: number;
trustedKey: { keyId: string; issuerEpoch: number; scope: 'portfolio-approval-projection-attestation/v1'; publicKey: KeyLike; revoked?: boolean };
trustedKey: {
keyId: string;
issuerEpoch: number;
scope: typeof APPROVAL_PROJECTION_ATTESTATION_SCOPE;
publicKey: KeyLike;
revoked?: boolean;
};
},
): ApprovalProjectionAttestationPayload {
if (envelopeValue === null || typeof envelopeValue !== 'object' || Array.isArray(envelopeValue)) throw new Error('Projection envelope is invalid.');
if (
envelopeValue === null ||
typeof envelopeValue !== "object" ||
Array.isArray(envelopeValue)
)
throw new Error("Projection envelope is invalid.");
const envelope = envelopeValue as Record<string, unknown>;
if (JSON.stringify(Object.keys(envelope).sort()) !== JSON.stringify(['payload', 'schema', 'signature'])) throw new Error('Projection envelope contains unknown fields.');
if (envelope.schema !== 'portfolio-approval-projection-attestation-envelope/v1') throw new Error('Projection envelope schema is invalid.');
if (envelope.payload === null || typeof envelope.payload !== 'object' || Array.isArray(envelope.payload)) throw new Error('Projection payload is invalid.');
const payload = envelope.payload as unknown as ApprovalProjectionAttestationPayload;
if (
JSON.stringify(Object.keys(envelope).sort()) !==
JSON.stringify(["payload", "schema", "signature"])
)
throw new Error("Projection envelope contains unknown fields.");
if (
envelope.schema !== "portfolio-approval-projection-attestation-envelope/v1"
)
throw new Error("Projection envelope schema is invalid.");
if (
envelope.payload === null ||
typeof envelope.payload !== "object" ||
Array.isArray(envelope.payload)
)
throw new Error("Projection payload is invalid.");
const payload =
envelope.payload as unknown as ApprovalProjectionAttestationPayload;
const payloadFields = [
'schema', 'keyId', 'issuerEpoch', 'scope', 'approvedRecordRoot', 'approvedAssetRoot', 'approvalProofSetDigest',
'inputDigests', 'productionInputRoot', 'liveLedgerHeadHash', 'liveLedgerRevision', 'policyVersion', 'issuedAt',
'expiresAt', 'earliestApprovalExpiry',
"schema",
"keyId",
"issuerEpoch",
"scope",
"approvedRecordRoot",
"approvedAssetRoot",
"approvalProofSetDigest",
"inputDigests",
"productionInputRoot",
"liveLedgerHeadHash",
"liveLedgerRevision",
"policyVersion",
"issuedAt",
"expiresAt",
"earliestApprovalExpiry",
].sort();
if (JSON.stringify(Object.keys(payload).sort()) !== JSON.stringify(payloadFields)) throw new Error('Projection payload contains missing or unknown fields.');
if (payload.schema !== 'portfolio-approval-projection-attestation/v1' || payload.scope !== 'portfolio-approval-projection-attestation/v1') throw new Error('Projection payload schema/scope is invalid.');
if (trust.trustedKey.revoked || trust.trustedKey.scope !== payload.scope || trust.trustedKey.keyId !== payload.keyId || trust.trustedKey.issuerEpoch !== payload.issuerEpoch) {
throw new Error('Projection key, scope, or issuer epoch is not trusted.');
if (
JSON.stringify(Object.keys(payload).sort()) !==
JSON.stringify(payloadFields)
)
throw new Error("Projection payload contains missing or unknown fields.");
if (
payload.schema !== APPROVAL_PROJECTION_ATTESTATION_SCHEMA ||
payload.scope !== APPROVAL_PROJECTION_ATTESTATION_SCOPE
)
throw new Error("Projection payload schema/scope is invalid.");
if (
trust.trustedKey.revoked ||
trust.trustedKey.scope !== payload.scope ||
trust.trustedKey.keyId !== payload.keyId ||
trust.trustedKey.issuerEpoch !== payload.issuerEpoch
) {
throw new Error("Projection key, scope, or issuer epoch is not trusted.");
}
for (const digest of [payload.approvedRecordRoot, payload.approvedAssetRoot, payload.approvalProofSetDigest, payload.productionInputRoot, payload.liveLedgerHeadHash]) {
if (!isSha256Hex(digest)) throw new Error('Projection digest field is invalid.');
for (const digest of [
payload.approvedRecordRoot,
payload.approvedAssetRoot,
payload.approvalProofSetDigest,
payload.productionInputRoot,
payload.liveLedgerHeadHash,
]) {
if (!isSha256Hex(digest))
throw new Error("Projection digest field is invalid.");
}
const inputKeys = Object.keys(payload.inputDigests).sort();
if (JSON.stringify(inputKeys) !== JSON.stringify(['PortfolioContent', 'PreparedAssetSet', 'PublicAssetManifest', 'SiteConfig']) ||
Object.values(payload.inputDigests).some((digest) => !isSha256Hex(digest))) throw new Error('Projection input digest closure is invalid.');
if (
JSON.stringify(inputKeys) !==
JSON.stringify([
"PortfolioContent",
"PreparedAssetSet",
"PublicAssetManifest",
"SiteConfig",
]) ||
Object.values(payload.inputDigests).some((digest) => !isSha256Hex(digest))
)
throw new Error("Projection input digest closure is invalid.");
const expectedRoot = computeProductionInputRoot({
portfolioContentDigest: payload.inputDigests.PortfolioContent,
publicAssetManifestDigest: payload.inputDigests.PublicAssetManifest,
preparedAssetSetDigest: payload.inputDigests.PreparedAssetSet,
siteConfigDigest: payload.inputDigests.SiteConfig,
});
if (expectedRoot !== payload.productionInputRoot) throw new Error('Projection does not bind the exact four-input root.');
if (payload.liveLedgerHeadHash !== trust.latestLedgerHeadHash || payload.liveLedgerRevision !== trust.latestLedgerRevision) throw new Error('Projection live-ledger state is stale.');
for (const [label, value] of [['now', trust.now], ['issuedAt', payload.issuedAt], ['expiresAt', payload.expiresAt], ['earliestApprovalExpiry', payload.earliestApprovalExpiry]] as const) {
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/u.test(value) || Number.isNaN(Date.parse(value))) throw new Error(`${label} timestamp is invalid.`);
if (expectedRoot !== payload.productionInputRoot)
throw new Error("Projection does not bind the exact four-input root.");
if (
payload.liveLedgerHeadHash !== trust.latestLedgerHeadHash ||
payload.liveLedgerRevision !== trust.latestLedgerRevision
)
throw new Error("Projection live-ledger state is stale.");
for (const [label, value] of [
["now", trust.now],
["issuedAt", payload.issuedAt],
["expiresAt", payload.expiresAt],
["earliestApprovalExpiry", payload.earliestApprovalExpiry],
] as const) {
if (
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/u.test(value) ||
Number.isNaN(Date.parse(value))
)
throw new Error(`${label} timestamp is invalid.`);
}
if (Date.parse(payload.issuedAt) > Date.parse(trust.now) || Date.parse(trust.now) >= Date.parse(payload.expiresAt) ||
Date.parse(payload.expiresAt) > Date.parse(payload.earliestApprovalExpiry)) throw new Error('Projection is outside its trusted expiry bounds.');
return verifyEd25519Envelope(envelope as unknown as ApprovalProjectionAttestationEnvelope, trust.trustedKey);
if (
Date.parse(payload.issuedAt) > Date.parse(trust.now) ||
Date.parse(trust.now) >= Date.parse(payload.expiresAt) ||
Date.parse(payload.expiresAt) > Date.parse(payload.earliestApprovalExpiry)
)
throw new Error("Projection is outside its trusted expiry bounds.");
return verifyEd25519Envelope(
envelope as unknown as ApprovalProjectionAttestationEnvelope,
trust.trustedKey,
);
}

View File

@@ -1,23 +1,24 @@
import type { KeyLike } from 'node:crypto';
import type { KeyLike } from "node:crypto";
import {
canonicalJsonDigest,
assertWellFormedNfcString,
isSha256Hex,
parseCanonicalJson,
verifyEd25519Envelope,
} from './canonical-digest.ts';
} from "./canonical-digest.ts";
export interface ApprovalPayload {
schema: 'portfolio-approval-payload/v1';
schema: "portfolio-approval-payload/v1";
keyId: string;
issuerEpoch: number;
authorizedRole: string;
evidenceFingerprints: string[];
recordClass: 'content' | 'asset';
recordClass: "content" | "asset";
recordType: string;
publicRecordIdOrAssetId: string;
approvedPublicRecordHashOrManifestRecordHash: string;
sanitizedOutputHashOrNull: string | null;
decision: 'approve' | 'reject' | 'revoke';
decision: "approve" | "reject" | "revoke";
issuedAt: string;
expiresAt: string;
previousLedgerHeadHash: string;
@@ -26,15 +27,19 @@ export interface ApprovalPayload {
}
export interface ApprovalEnvelope {
schema: 'portfolio-approval-envelope/v1';
schema: "portfolio-approval-envelope/v1";
payload: ApprovalPayload;
signature: { algorithm: 'Ed25519'; encoding: 'base64url-nopad'; value: string };
signature: {
algorithm: "Ed25519";
encoding: "base64url-nopad";
value: string;
};
}
export interface ApprovalTrustKey {
keyId: string;
issuerEpoch: number;
scope: 'portfolio-approval-decision/v1';
scope: "portfolio-approval-decision/v1";
authorizedRoles: string[];
publicKey: KeyLike;
revoked?: boolean;
@@ -45,99 +50,273 @@ export interface ApprovalTrustPolicy {
latestLedgerHeadHash: string;
latestLedgerRevision: number;
trustedKeys: ApprovalTrustKey[];
ledgerProof: ApprovalLedgerProof;
}
export interface ApprovalLedgerProof {
schema: "portfolio-approval-ledger-proof/v1";
genesisHeadHash: string;
entries: ApprovalEnvelope[];
}
const payloadKeys = [
'schema', 'keyId', 'issuerEpoch', 'authorizedRole', 'evidenceFingerprints', 'recordClass', 'recordType',
'publicRecordIdOrAssetId', 'approvedPublicRecordHashOrManifestRecordHash', 'sanitizedOutputHashOrNull',
'decision', 'issuedAt', 'expiresAt', 'previousLedgerHeadHash', 'ledgerRevision', 'policyVersion',
"schema",
"keyId",
"issuerEpoch",
"authorizedRole",
"evidenceFingerprints",
"recordClass",
"recordType",
"publicRecordIdOrAssetId",
"approvedPublicRecordHashOrManifestRecordHash",
"sanitizedOutputHashOrNull",
"decision",
"issuedAt",
"expiresAt",
"previousLedgerHeadHash",
"ledgerRevision",
"policyVersion",
] as const;
const envelopeKeys = ['schema', 'payload', 'signature'] as const;
const signatureKeys = ['algorithm', 'encoding', 'value'] as const;
const envelopeKeys = ["schema", "payload", "signature"] as const;
const signatureKeys = ["algorithm", "encoding", "value"] as const;
const utcTimestamp = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/u;
const safeToken = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/u;
function record(value: unknown, label: string): Record<string, unknown> {
if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must be an object.`);
if (value === null || typeof value !== "object" || Array.isArray(value))
throw new Error(`${label} must be an object.`);
return value as Record<string, unknown>;
}
function exactKeys(value: Record<string, unknown>, keys: readonly string[], label: string): void {
function exactKeys(
value: Record<string, unknown>,
keys: readonly string[],
label: string,
): void {
const actual = Object.keys(value).sort();
const expected = [...keys].sort();
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
throw new Error(`${label} contains missing or unknown fields.`);
}
}
function validTimestamp(value: unknown, label: string): asserts value is string {
if (typeof value !== 'string' || !utcTimestamp.test(value) || Number.isNaN(Date.parse(value))) {
function validTimestamp(
value: unknown,
label: string,
): asserts value is string {
if (
typeof value !== "string" ||
!utcTimestamp.test(value) ||
Number.isNaN(Date.parse(value))
) {
throw new Error(`${label} must be a UTC RFC 3339 timestamp.`);
}
}
export function validateApprovalEnvelope(value: unknown): ApprovalEnvelope {
const outer = record(value, 'approval envelope');
exactKeys(outer, envelopeKeys, 'approval envelope');
if (outer.schema !== 'portfolio-approval-envelope/v1') throw new Error('Approval envelope schema is invalid.');
const payload = record(outer.payload, 'approval payload');
exactKeys(payload, payloadKeys, 'approval payload');
const signature = record(outer.signature, 'approval signature');
exactKeys(signature, signatureKeys, 'approval signature');
const outer = record(value, "approval envelope");
exactKeys(outer, envelopeKeys, "approval envelope");
if (outer.schema !== "portfolio-approval-envelope/v1")
throw new Error("Approval envelope schema is invalid.");
const payload = record(outer.payload, "approval payload");
exactKeys(payload, payloadKeys, "approval payload");
const signature = record(outer.signature, "approval signature");
exactKeys(signature, signatureKeys, "approval signature");
if (payload.schema !== 'portfolio-approval-payload/v1') throw new Error('Approval payload schema is invalid.');
for (const key of ['keyId', 'authorizedRole', 'publicRecordIdOrAssetId', 'policyVersion'] as const) {
if (payload.schema !== "portfolio-approval-payload/v1")
throw new Error("Approval payload schema is invalid.");
for (const key of [
"keyId",
"authorizedRole",
"publicRecordIdOrAssetId",
"policyVersion",
] as const) {
const field = payload[key];
if (typeof field !== 'string' || !safeToken.test(field)) throw new Error(`${key} is invalid.`);
if (typeof field !== "string" || !safeToken.test(field))
throw new Error(`${key} is invalid.`);
assertWellFormedNfcString(field, key);
}
if (!Number.isSafeInteger(payload.issuerEpoch) || (payload.issuerEpoch as number) < 1) throw new Error('issuerEpoch is invalid.');
if (!Number.isSafeInteger(payload.ledgerRevision) || (payload.ledgerRevision as number) < 0) throw new Error('ledgerRevision is invalid.');
if (payload.recordClass !== 'content' && payload.recordClass !== 'asset') throw new Error('recordClass is invalid.');
if (typeof payload.recordType !== 'string') throw new Error('recordType is invalid.');
assertWellFormedNfcString(payload.recordType, 'recordType');
if (payload.recordClass === 'content' && !safeToken.test(payload.recordType)) throw new Error('Content recordType is required.');
if (payload.recordClass === 'asset' && payload.recordType !== '') throw new Error('Asset recordType must be empty.');
if (!isSha256Hex(payload.approvedPublicRecordHashOrManifestRecordHash) || !isSha256Hex(payload.previousLedgerHeadHash)) {
throw new Error('Approval hash field is invalid.');
if (
!Number.isSafeInteger(payload.issuerEpoch) ||
(payload.issuerEpoch as number) < 1
)
throw new Error("issuerEpoch is invalid.");
if (
!Number.isSafeInteger(payload.ledgerRevision) ||
(payload.ledgerRevision as number) < 0
)
throw new Error("ledgerRevision is invalid.");
if (payload.recordClass !== "content" && payload.recordClass !== "asset")
throw new Error("recordClass is invalid.");
if (typeof payload.recordType !== "string")
throw new Error("recordType is invalid.");
assertWellFormedNfcString(payload.recordType, "recordType");
if (payload.recordClass === "content" && !safeToken.test(payload.recordType))
throw new Error("Content recordType is required.");
if (payload.recordClass === "asset" && payload.recordType !== "")
throw new Error("Asset recordType must be empty.");
if (
!isSha256Hex(payload.approvedPublicRecordHashOrManifestRecordHash) ||
!isSha256Hex(payload.previousLedgerHeadHash)
) {
throw new Error("Approval hash field is invalid.");
}
if (!Array.isArray(payload.evidenceFingerprints) || payload.evidenceFingerprints.length === 0 ||
if (
!Array.isArray(payload.evidenceFingerprints) ||
payload.evidenceFingerprints.length === 0 ||
payload.evidenceFingerprints.some((item) => !isSha256Hex(item)) ||
new Set(payload.evidenceFingerprints).size !== payload.evidenceFingerprints.length) {
throw new Error('evidenceFingerprints must be a unique, non-empty digest list.');
new Set(payload.evidenceFingerprints).size !==
payload.evidenceFingerprints.length
) {
throw new Error(
"evidenceFingerprints must be a unique, non-empty digest list.",
);
}
if (payload.recordClass === 'content' && payload.sanitizedOutputHashOrNull !== null) throw new Error('Content approval cannot bind sanitized bytes.');
if (payload.recordClass === 'asset' && !isSha256Hex(payload.sanitizedOutputHashOrNull)) throw new Error('Asset approval must bind sanitized bytes.');
if (!['approve', 'reject', 'revoke'].includes(payload.decision as string)) throw new Error('Decision is invalid.');
validTimestamp(payload.issuedAt, 'issuedAt');
validTimestamp(payload.expiresAt, 'expiresAt');
if (Date.parse(payload.issuedAt) >= Date.parse(payload.expiresAt)) throw new Error('Approval expiry must follow issue time.');
if (signature.algorithm !== 'Ed25519' || signature.encoding !== 'base64url-nopad' || typeof signature.value !== 'string') {
throw new Error('Approval signature metadata is invalid.');
if (
payload.recordClass === "content" &&
payload.sanitizedOutputHashOrNull !== null
)
throw new Error("Content approval cannot bind sanitized bytes.");
if (
payload.recordClass === "asset" &&
!isSha256Hex(payload.sanitizedOutputHashOrNull)
)
throw new Error("Asset approval must bind sanitized bytes.");
if (!["approve", "reject", "revoke"].includes(payload.decision as string))
throw new Error("Decision is invalid.");
validTimestamp(payload.issuedAt, "issuedAt");
validTimestamp(payload.expiresAt, "expiresAt");
if (Date.parse(payload.issuedAt) >= Date.parse(payload.expiresAt))
throw new Error("Approval expiry must follow issue time.");
if (
signature.algorithm !== "Ed25519" ||
signature.encoding !== "base64url-nopad" ||
typeof signature.value !== "string"
) {
throw new Error("Approval signature metadata is invalid.");
}
return outer as unknown as ApprovalEnvelope;
}
export function parseApprovalEnvelopeJson(source: string | Uint8Array): ApprovalEnvelope {
export function parseApprovalEnvelopeJson(
source: string | Uint8Array,
): ApprovalEnvelope {
return validateApprovalEnvelope(parseCanonicalJson(source));
}
export function verifyApprovalEnvelope(envelopeValue: unknown, trust: ApprovalTrustPolicy): ApprovalPayload {
function verifyEnvelopeSignature(
envelopeValue: unknown,
trust: ApprovalTrustPolicy,
): ApprovalEnvelope {
const envelope = validateApprovalEnvelope(envelopeValue);
validTimestamp(trust.now, 'trusted now');
if (!isSha256Hex(trust.latestLedgerHeadHash) || !Number.isSafeInteger(trust.latestLedgerRevision)) {
throw new Error('Protected live-ledger state is invalid.');
}
const matchingKeys = trust.trustedKeys.filter((candidate) =>
candidate.keyId === envelope.payload.keyId && candidate.issuerEpoch === envelope.payload.issuerEpoch);
if (matchingKeys.length !== 1) throw new Error('Approval trust policy must contain exactly one matching key epoch.');
const matchingKeys = trust.trustedKeys.filter(
(candidate) =>
candidate.keyId === envelope.payload.keyId &&
candidate.issuerEpoch === envelope.payload.issuerEpoch,
);
if (matchingKeys.length !== 1)
throw new Error(
"Approval trust policy must contain exactly one matching key epoch.",
);
const key = matchingKeys[0];
if (key === undefined || key.revoked || key.scope !== 'portfolio-approval-decision/v1') throw new Error('Approval key, scope, or epoch is not trusted.');
if (!key.authorizedRoles.includes(envelope.payload.authorizedRole)) throw new Error('Approval role is not authorized.');
if (envelope.payload.ledgerRevision >= trust.latestLedgerRevision) throw new Error('Approval is not anchored before the protected live-ledger revision.');
if (Date.parse(trust.now) < Date.parse(envelope.payload.issuedAt) || Date.parse(trust.now) >= Date.parse(envelope.payload.expiresAt)) {
throw new Error('Approval is not current at the trusted time.');
}
if (envelope.payload.decision !== 'approve') throw new Error('Only a current approval decision can enter the production closure.');
return verifyEd25519Envelope(envelope, key);
if (
key === undefined ||
key.revoked ||
key.scope !== "portfolio-approval-decision/v1"
)
throw new Error("Approval key, scope, or epoch is not trusted.");
if (!key.authorizedRoles.includes(envelope.payload.authorizedRole))
throw new Error("Approval role is not authorized.");
verifyEd25519Envelope(envelope, key);
return envelope;
}
function typedDecisionId(payload: ApprovalPayload): string {
return `${payload.recordClass}\0${payload.recordType}\0${payload.publicRecordIdOrAssetId}`;
}
export function verifyApprovalLedger(
trust: ApprovalTrustPolicy,
): Map<string, ApprovalEnvelope> {
validTimestamp(trust.now, "trusted now");
if (
!isSha256Hex(trust.latestLedgerHeadHash) ||
!Number.isSafeInteger(trust.latestLedgerRevision) ||
trust.latestLedgerRevision < 0
) {
throw new Error("Protected live-ledger state is invalid.");
}
const proof = trust.ledgerProof;
if (
proof === null ||
typeof proof !== "object" ||
Array.isArray(proof) ||
JSON.stringify(Object.keys(proof).sort()) !==
JSON.stringify(["entries", "genesisHeadHash", "schema"]) ||
proof.schema !== "portfolio-approval-ledger-proof/v1" ||
!isSha256Hex(proof.genesisHeadHash) ||
!Array.isArray(proof.entries) ||
proof.entries.length !== trust.latestLedgerRevision
)
throw new Error("Authenticated approval ledger proof is incomplete.");
let head = proof.genesisHeadHash;
const latest = new Map<string, ApprovalEnvelope>();
proof.entries.forEach((raw, index) => {
const envelope = verifyEnvelopeSignature(raw, trust);
const revision = index + 1;
if (
envelope.payload.ledgerRevision !== revision ||
envelope.payload.previousLedgerHeadHash !== head ||
Date.parse(envelope.payload.issuedAt) > Date.parse(trust.now)
)
throw new Error(
"Approval ledger chain is not contiguous through the protected revision.",
);
const envelopeDigest = canonicalJsonDigest(
"portfolio-approval-envelope-digest/v1",
envelope,
);
head = canonicalJsonDigest("portfolio-approval-ledger-entry/v1", {
previousLedgerHeadHash: head,
ledgerRevision: revision,
envelopeDigest,
});
latest.set(typedDecisionId(envelope.payload), envelope);
});
if (head !== trust.latestLedgerHeadHash)
throw new Error(
"Approval ledger proof does not authenticate the protected head.",
);
return latest;
}
export function verifyApprovalEnvelope(
envelopeValue: unknown,
trust: ApprovalTrustPolicy,
): ApprovalPayload {
const envelope = verifyEnvelopeSignature(envelopeValue, trust);
const latest = verifyApprovalLedger(trust).get(
typedDecisionId(envelope.payload),
);
if (
latest === undefined ||
canonicalJsonDigest("portfolio-approval-envelope-digest/v1", latest) !==
canonicalJsonDigest("portfolio-approval-envelope-digest/v1", envelope)
) {
throw new Error(
"Approval is not the latest authenticated decision for this record.",
);
}
if (
Date.parse(trust.now) < Date.parse(envelope.payload.issuedAt) ||
Date.parse(trust.now) >= Date.parse(envelope.payload.expiresAt)
) {
throw new Error("Approval is not current at the trusted time.");
}
if (envelope.payload.decision !== "approve")
throw new Error("The latest decision is reject/revoke, not approval.");
return envelope.payload;
}

View File

@@ -1,101 +1,314 @@
import { generateKeyPairSync, sign } from 'node:crypto';
import { describe, expect, test } from 'vitest';
import { generateKeyPairSync, sign } from "node:crypto";
import { describe, expect, test } from "vitest";
import { canonicalJsonDigest, canonicalValueBytes } from '../../src/security/canonical-digest.ts';
import { computePreparedAssetSetDigest, computeProductionInputRoot } from '../../src/security/production-input-root.ts';
import { validateReleaseManifest } from '../../src/security/release-manifest.ts';
import {
canonicalJsonDigest,
canonicalValueBytes,
} from "../../src/security/canonical-digest.ts";
import {
computePreparedAssetSetDigest,
computeProductionInputRoot,
} from "../../src/security/production-input-root.ts";
import { validateReleaseManifest } from "../../src/security/release-manifest.ts";
// @ts-expect-error The executable JavaScript verifier intentionally has no browser-facing declaration.
import { verifyApprovalRequest } from '../../scripts/release/verify-approvals.mjs';
import { verifyApprovalRequestForTest } from "../../scripts/release/verify-approvals.mjs";
const h = (c: string) => c.repeat(64);
describe('production input and private-source release manifest', () => {
test('hashes an exact manifest-bound PreparedAssetSet and rejects missing or extra members', () => {
const bytes = new TextEncoder().encode('image');
const member = { assetId: 'asset-one', publicPath: 'asset-one.webp', mediaType: 'image/webp', width: 10, height: 20, durationMs: null, bytes };
const manifest = [{ id: 'asset-one', fileName: 'asset-one.webp', sha256: '6105d6cc76af400325e94d588ce511be5bfdbb73b437dc51eca43917d7a43e3d', mediaType: 'image/webp', width: 10, height: 20, durationMs: null }];
expect(computePreparedAssetSetDigest([member], manifest)).toMatch(/^[a-f0-9]{64}$/);
describe("production input and private-source release manifest", () => {
test("hashes an exact manifest-bound PreparedAssetSet and rejects missing or extra members", () => {
const bytes = new TextEncoder().encode("image");
const member = {
assetId: "asset-one",
publicPath: "asset-one.webp",
mediaType: "image/webp",
width: 10,
height: 20,
durationMs: null,
bytes,
};
const manifest = [
{
id: "asset-one",
fileName: "asset-one.webp",
sha256:
"6105d6cc76af400325e94d588ce511be5bfdbb73b437dc51eca43917d7a43e3d",
mediaType: "image/webp",
width: 10,
height: 20,
durationMs: null,
},
];
expect(computePreparedAssetSetDigest([member], manifest)).toMatch(
/^[a-f0-9]{64}$/,
);
expect(() => computePreparedAssetSetDigest([], manifest)).toThrow();
expect(() => computePreparedAssetSetDigest([member, { ...member, assetId: 'asset-two', publicPath: 'asset-two.webp' }], manifest)).toThrow();
expect(() =>
computePreparedAssetSetDigest(
[
member,
{ ...member, assetId: "asset-two", publicPath: "asset-two.webp" },
],
manifest,
),
).toThrow();
});
test('matches the fixed four-label input-root vector', () => {
expect(computeProductionInputRoot({
portfolioContentDigest: h('a'), publicAssetManifestDigest: h('b'),
preparedAssetSetDigest: h('c'), siteConfigDigest: h('d'),
})).toBe('38707dd72ded582a346048ff66a1625cdaca91f6536dd27c935bc3b8af262ae8');
test("matches the fixed four-label input-root vector", () => {
expect(
computeProductionInputRoot({
portfolioContentDigest: h("a"),
publicAssetManifestDigest: h("b"),
preparedAssetSetDigest: h("c"),
siteConfigDigest: h("d"),
}),
).toBe("38707dd72ded582a346048ff66a1625cdaca91f6536dd27c935bc3b8af262ae8");
});
test('validates an exact initial-launch private-source manifest', () => {
test("validates an exact initial-launch private-source manifest", () => {
const inputDigests = {
PortfolioContent: h('a'), PublicAssetManifest: h('b'), PreparedAssetSet: h('c'), SiteConfig: h('d'),
PortfolioContent: h("a"),
PublicAssetManifest: h("b"),
PreparedAssetSet: h("c"),
SiteConfig: h("d"),
};
const manifest = {
schema: 'portfolio-release-manifest/v1', launchMode: 'private-source-initial-launch', buildId: 'release-2026-09-02',
inputDigests, productionInputRoot: computeProductionInputRoot({
portfolioContentDigest: h('a'), publicAssetManifestDigest: h('b'), preparedAssetSetDigest: h('c'), siteConfigDigest: h('d'),
schema: "portfolio-release-manifest/v1",
launchMode: "private-source-initial-launch",
buildId: "release-2026-09-02",
inputDigests,
productionInputRoot: computeProductionInputRoot({
portfolioContentDigest: h("a"),
publicAssetManifestDigest: h("b"),
preparedAssetSetDigest: h("c"),
siteConfigDigest: h("d"),
}),
artifacts: [{ path: 'index.html', sha256: h('e'), bytes: 42, encoding: 'identity' }],
deployFiles: ['index.html'], sourceCommit: h('f'), sourceTree: h('0'),
approvedRecordRoot: h('1'), approvedAssetRoot: h('2'), approvalProofSetDigest: h('3'),
approvalProjectionAttestationDigest: h('4'), scanEvidenceDigest: h('5'),
policyVersions: { approval: '1', privacy: '1', release: '1' }, toolVersions: { node: '22.22.1' },
predecessorManifestDigest: null, currentManifestDigest: null,
issuedAt: '2026-09-02T00:00:00Z', expiresAt: '2026-09-03T00:00:00Z', minimumBoundExpiry: '2026-09-03T00:00:00Z',
artifacts: [
{ path: "index.html", sha256: h("e"), bytes: 42, encoding: "identity" },
],
deployFiles: ["index.html"],
sourceCommit: h("f"),
sourceTree: h("0"),
approvedRecordRoot: h("1"),
approvedAssetRoot: h("2"),
approvalProofSetDigest: h("3"),
approvalProjectionAttestationDigest: h("4"),
scanEvidenceDigest: h("5"),
policyVersions: { approval: "1", privacy: "1", release: "1" },
toolVersions: { node: "22.22.1" },
predecessorManifestDigest: null,
currentManifestDigest: null,
issuedAt: "2026-09-02T00:00:00Z",
expiresAt: "2026-09-03T00:00:00Z",
minimumBoundExpiry: "2026-09-03T00:00:00Z",
};
expect(validateReleaseManifest(manifest, { now: '2026-09-02T01:00:00Z' })).toEqual(manifest);
expect(canonicalJsonDigest('portfolio-release-manifest/v1', manifest)).toMatch(/^[a-f0-9]{64}$/);
expect(
validateReleaseManifest(manifest, { now: "2026-09-02T01:00:00Z" }),
).toEqual(manifest);
expect(
canonicalJsonDigest("portfolio-release-manifest/v1", manifest),
).toMatch(/^[a-f0-9]{64}$/);
});
test.each([
['public Git target', { publicGitTarget: 'https://example.com/repo.git' }],
['extra deploy file', { deployFiles: ['index.html', 'extra.txt'] }],
['case-fold collision', { artifacts: [
{ path: 'A.js', sha256: h('e'), bytes: 1, encoding: 'identity' },
{ path: 'a.js', sha256: h('f'), bytes: 1, encoding: 'identity' },
], deployFiles: ['A.js', 'a.js'] }],
['expired manifest', { expiresAt: '2026-09-02T00:30:00Z' }],
])('rejects %s', (_label, override) => {
const inputDigests = { PortfolioContent: h('a'), PublicAssetManifest: h('b'), PreparedAssetSet: h('c'), SiteConfig: h('d') };
const baseline = {
schema: 'portfolio-release-manifest/v1', launchMode: 'private-source-initial-launch', buildId: 'release', inputDigests,
productionInputRoot: computeProductionInputRoot({ portfolioContentDigest: h('a'), publicAssetManifestDigest: h('b'), preparedAssetSetDigest: h('c'), siteConfigDigest: h('d') }),
artifacts: [{ path: 'index.html', sha256: h('e'), bytes: 42, encoding: 'identity' }], deployFiles: ['index.html'],
sourceCommit: h('f'), sourceTree: h('0'), approvedRecordRoot: h('1'), approvedAssetRoot: h('2'), approvalProofSetDigest: h('3'),
approvalProjectionAttestationDigest: h('4'), scanEvidenceDigest: h('5'), policyVersions: { approval: '1', privacy: '1', release: '1' },
toolVersions: { node: '22' }, predecessorManifestDigest: null, currentManifestDigest: null,
issuedAt: '2026-09-02T00:00:00Z', expiresAt: '2026-09-03T00:00:00Z', minimumBoundExpiry: '2026-09-03T00:00:00Z',
["public Git target", { publicGitTarget: "https://example.com/repo.git" }],
["extra deploy file", { deployFiles: ["index.html", "extra.txt"] }],
[
"case-fold collision",
{
artifacts: [
{ path: "A.js", sha256: h("e"), bytes: 1, encoding: "identity" },
{ path: "a.js", sha256: h("f"), bytes: 1, encoding: "identity" },
],
deployFiles: ["A.js", "a.js"],
},
],
["expired manifest", { expiresAt: "2026-09-02T00:30:00Z" }],
])("rejects %s", (_label, override) => {
const inputDigests = {
PortfolioContent: h("a"),
PublicAssetManifest: h("b"),
PreparedAssetSet: h("c"),
SiteConfig: h("d"),
};
expect(() => validateReleaseManifest({ ...baseline, ...override }, { now: '2026-09-02T01:00:00Z' })).toThrow();
const baseline = {
schema: "portfolio-release-manifest/v1",
launchMode: "private-source-initial-launch",
buildId: "release",
inputDigests,
productionInputRoot: computeProductionInputRoot({
portfolioContentDigest: h("a"),
publicAssetManifestDigest: h("b"),
preparedAssetSetDigest: h("c"),
siteConfigDigest: h("d"),
}),
artifacts: [
{ path: "index.html", sha256: h("e"), bytes: 42, encoding: "identity" },
],
deployFiles: ["index.html"],
sourceCommit: h("f"),
sourceTree: h("0"),
approvedRecordRoot: h("1"),
approvedAssetRoot: h("2"),
approvalProofSetDigest: h("3"),
approvalProjectionAttestationDigest: h("4"),
scanEvidenceDigest: h("5"),
policyVersions: { approval: "1", privacy: "1", release: "1" },
toolVersions: { node: "22" },
predecessorManifestDigest: null,
currentManifestDigest: null,
issuedAt: "2026-09-02T00:00:00Z",
expiresAt: "2026-09-03T00:00:00Z",
minimumBoundExpiry: "2026-09-03T00:00:00Z",
};
expect(() =>
validateReleaseManifest(
{ ...baseline, ...override },
{ now: "2026-09-02T01:00:00Z" },
),
).toThrow();
});
test('CLI verifier emits only canonical public roots from a signed external approval request', () => {
const keys = generateKeyPairSync('ed25519');
test("approval verifier requires an authenticated exact ledger and rejects replay after revoke", () => {
const keys = generateKeyPairSync("ed25519");
const approvalPayload = {
schema: 'portfolio-approval-payload/v1', keyId: 'approval-key', issuerEpoch: 1, authorizedRole: 'approver',
evidenceFingerprints: [h('1')], recordClass: 'content', recordType: 'profile', publicRecordIdOrAssetId: 'profile-main',
approvedPublicRecordHashOrManifestRecordHash: h('2'), sanitizedOutputHashOrNull: null, decision: 'approve',
issuedAt: '2026-09-01T00:00:00Z', expiresAt: '2026-09-04T00:00:00Z', previousLedgerHeadHash: h('8'),
ledgerRevision: 1, policyVersion: 'policy-1',
schema: "portfolio-approval-payload/v1",
keyId: "approval-key",
issuerEpoch: 1,
authorizedRole: "approver",
evidenceFingerprints: [h("1")],
recordClass: "content",
recordType: "profile",
publicRecordIdOrAssetId: "profile-main",
approvedPublicRecordHashOrManifestRecordHash: h("2"),
sanitizedOutputHashOrNull: null,
decision: "approve",
issuedAt: "2026-09-01T00:00:00Z",
expiresAt: "2026-09-04T00:00:00Z",
previousLedgerHeadHash: h("0"),
ledgerRevision: 1,
policyVersion: "policy-1",
};
const envelope = {
schema: 'portfolio-approval-envelope/v1', payload: approvalPayload,
signature: { algorithm: 'Ed25519', encoding: 'base64url-nopad', value: sign(null, canonicalValueBytes(approvalPayload), keys.privateKey).toString('base64url') },
schema: "portfolio-approval-envelope/v1",
payload: approvalPayload,
signature: {
algorithm: "Ed25519",
encoding: "base64url-nopad",
value: sign(
null,
canonicalValueBytes(approvalPayload),
keys.privateKey,
).toString("base64url"),
},
};
const envelopeDigest = canonicalJsonDigest(
"portfolio-approval-envelope-digest/v1",
envelope,
);
const head = canonicalJsonDigest("portfolio-approval-ledger-entry/v1", {
previousLedgerHeadHash: h("0"),
ledgerRevision: 1,
envelopeDigest,
});
const request = {
schema: 'portfolio-approval-verification-request/v1', now: '2026-09-02T00:00:00Z',
latestLedgerHeadHash: h('9'), latestLedgerRevision: 2,
trustedKeys: [{ keyId: 'approval-key', issuerEpoch: 1, scope: 'portfolio-approval-decision/v1', authorizedRoles: ['approver'], publicKeyPem: keys.publicKey.export({ type: 'spki', format: 'pem' }) }],
contentRecords: [{ recordType: 'profile', publicRecordId: 'profile-main', approvedPublicRecordHash: h('2'), envelope }],
schema: "portfolio-approval-verification-request/v1",
latestLedgerHeadHash: head,
latestLedgerRevision: 1,
trustedKeys: [
{
keyId: "approval-key",
issuerEpoch: 1,
scope: "portfolio-approval-decision/v1",
authorizedRoles: ["approver"],
publicKeyPem: keys.publicKey.export({ type: "spki", format: "pem" }),
},
],
ledgerProof: {
schema: "portfolio-approval-ledger-proof/v1",
genesisHeadHash: h("0"),
entries: [envelope],
},
contentRecords: [
{
recordType: "profile",
publicRecordId: "profile-main",
approvedPublicRecordHash: h("2"),
envelope,
},
],
assetRecords: [],
inputDigests: { PortfolioContent: h('a'), PublicAssetManifest: h('b'), PreparedAssetSet: h('c'), SiteConfig: h('d') },
inputDigests: {
PortfolioContent: h("a"),
PublicAssetManifest: h("b"),
PreparedAssetSet: h("c"),
SiteConfig: h("d"),
},
};
const result = verifyApprovalRequest(canonicalValueBytes(request));
const result = verifyApprovalRequestForTest(canonicalValueBytes(request), {
now: "2026-09-02T00:00:00Z",
});
expect(Object.keys(result).sort()).toEqual([
'approvalProofSetDigest', 'approvedAssetRoot', 'approvedRecordRoot', 'earliestApprovalExpiry',
'latestLedgerHeadHash', 'latestLedgerRevision', 'productionInputRoot', 'schema',
"approvalProofSetDigest",
"approvedAssetRoot",
"approvedRecordRoot",
"earliestApprovalExpiry",
"latestLedgerHeadHash",
"latestLedgerRevision",
"productionInputRoot",
"projectionSchema",
"projectionScope",
"schema",
]);
const serialized = new TextDecoder().decode(canonicalValueBytes(result));
expect(serialized).not.toContain('evidenceFingerprints');
expect(serialized).not.toContain("evidenceFingerprints");
const revokePayload = {
...approvalPayload,
decision: "revoke",
issuedAt: "2026-09-02T01:00:00Z",
ledgerRevision: 2,
previousLedgerHeadHash: head,
};
const revoke = {
schema: "portfolio-approval-envelope/v1",
payload: revokePayload,
signature: {
algorithm: "Ed25519",
encoding: "base64url-nopad",
value: sign(
null,
canonicalValueBytes(revokePayload),
keys.privateKey,
).toString("base64url"),
},
};
const revokeDigest = canonicalJsonDigest(
"portfolio-approval-envelope-digest/v1",
revoke,
);
const revokedHead = canonicalJsonDigest(
"portfolio-approval-ledger-entry/v1",
{
previousLedgerHeadHash: head,
ledgerRevision: 2,
envelopeDigest: revokeDigest,
},
);
expect(() =>
verifyApprovalRequestForTest(
canonicalValueBytes({
...request,
latestLedgerHeadHash: revokedHead,
latestLedgerRevision: 2,
ledgerProof: {
schema: "portfolio-approval-ledger-proof/v1",
genesisHeadHash: h("0"),
entries: [envelope, revoke],
},
}),
{ now: "2026-09-02T02:00:00Z" },
),
).toThrow(/latest|revoke|supersed/i);
});
});

View File

@@ -6,9 +6,11 @@ import {
symlink,
writeFile,
} from "node:fs/promises";
import { createHash } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { canonicalJsonDigest } from "../../src/security/canonical-digest.ts";
// @ts-expect-error executable release modules intentionally have no browser declarations
import { enumerateClosure } from "../../scripts/release/enumerate-closure.mjs";
@@ -16,10 +18,24 @@ import { enumerateClosure } from "../../scripts/release/enumerate-closure.mjs";
import * as privacyRunner from "../../scripts/release/run-privacy-workers.mjs";
// @ts-expect-error executable release modules intentionally have no browser declarations
import { reconcileScan } from "../../scripts/release/reconcile-scan.mjs";
// @ts-expect-error container worker is executable JavaScript
import { inspectObject } from "../../containers/privacy-worker-a/worker.mjs";
// @ts-expect-error container matcher is executable JavaScript
import { matchReports } from "../../containers/privacy-matcher/matcher.mjs";
const { buildWorkerCommand, validateScanObjectDescriptor } = privacyRunner;
const {
buildWorkerCommand,
validateScanObjectDescriptor,
requireLiveContainerValidation,
} = privacyRunner;
const h = (c: string) => c.repeat(64);
const sourceClosure = (members: unknown[] = []) => {
return { schema: "portfolio-publication-closure/v1", members };
};
const publicClosure = (members: unknown[] = []) => {
return { schema: "portfolio-public-object-closure/v1", members };
};
function report(
workerId: "worker-a" | "worker-b",
@@ -56,6 +72,124 @@ function matcher(override: Record<string, unknown> = {}) {
}
describe("isolated privacy closure and reconciliation", () => {
test("fails closed while live scanners are pending and for every non-empty public object closure", () => {
expect(() =>
requireLiveContainerValidation({
schema: "portfolio-live-container-validation/v1",
status: "pending",
reason: "not verified",
}),
).toThrow(/pending|verified/i);
const canary = Buffer.from("PRIVATE-PROJECT-NAME\nTOKEN=secret-canary");
const inputDigest = createHash("sha256").update(canary).digest("hex");
expect(
inspectObject(
{
schema: "portfolio-privacy-worker-request/v1",
workerId: "worker-a",
imageDigest: h("a"),
inputDigest,
checkId: "secret",
},
canary,
).verdict,
).not.toBe("pass");
const pythonReport = report("worker-b", {
inputDigest,
checkId: "secret",
verdict: "unsupported",
});
expect(() =>
matchReports({
reports: [
inspectObject(
{
schema: "portfolio-privacy-worker-request/v1",
workerId: "worker-a",
imageDigest: h("a"),
inputDigest,
checkId: "secret",
},
canary,
),
pythonReport,
],
rules: [{ id: "private-name-v1", deniedMemberDigests: [] }],
ruleSetDigest: h("4"),
}),
).toThrow(/pass|unsupported|agree/i);
const source = sourceClosure();
const publicObjectClosure = publicClosure([
{
objectId: "private-leak.txt",
inputDigest,
requiredChecks: ["secret"],
},
]);
expect(() =>
reconcileScan({
sourceClosure: source,
publicObjectClosure,
reports: [
report("worker-a", { inputDigest, checkId: "secret" }),
report("worker-b", { inputDigest, checkId: "secret" }),
],
matcherResults: [matcher({ inputDigest, checkId: "secret" })],
candidate: {
schema: "portfolio-candidate-release-manifest/v1",
buildId: "r1",
sourceClosureDigest: canonicalJsonDigest(
"portfolio-publication-closure/v1",
{ schema: source.schema, members: source.members },
),
publicObjectClosureDigest: canonicalJsonDigest(
"portfolio-public-object-closure/v1",
publicObjectClosure,
),
},
}),
).toThrow(/unavailable|non-empty|scanner/i);
});
test("derives exact scan coverage from canonical closures instead of caller expectedChecks", () => {
const source = sourceClosure();
const publicObjectClosure = publicClosure();
const candidate = {
schema: "portfolio-candidate-release-manifest/v1",
buildId: "r1",
sourceClosureDigest: canonicalJsonDigest(
"portfolio-publication-closure/v1",
source,
),
publicObjectClosureDigest: canonicalJsonDigest(
"portfolio-public-object-closure/v1",
publicObjectClosure,
),
};
const result = reconcileScan({
sourceClosure: source,
publicObjectClosure,
reports: [],
matcherResults: [],
candidate,
});
expect(result.evidence).toMatchObject({
coverageCount: 0,
passedCount: 0,
sourceClosureDigest: candidate.sourceClosureDigest,
publicObjectClosureDigest: candidate.publicObjectClosureDigest,
});
expect(() =>
reconcileScan({
sourceClosure: source,
publicObjectClosure,
expectedChecks: [{ inputDigest: h("1"), checkId: "bytes" }],
reports: [],
matcherResults: [],
candidate,
}),
).toThrow(/missing|extra|unknown|caller|expectedChecks/i);
});
test("enumerates an exact byte-sorted closure and rejects unsafe filesystem members", async () => {
const root = await mkdtemp(join(tmpdir(), "portfolio-closure-"));
await mkdir(join(root, "nested"));
@@ -142,14 +276,25 @@ describe("isolated privacy closure and reconciliation", () => {
).toThrowError(expect.not.stringContaining(secret));
});
test("accepts only ordered heterogeneous agreement for every expected check", () => {
test("accepts only the exact empty public object closure while scanners are unavailable", () => {
const source = sourceClosure();
const publicObjectClosure = publicClosure();
const result = reconcileScan({
expectedChecks: [{ inputDigest: h("1"), checkId: "bytes" }],
reports: [report("worker-a"), report("worker-b")],
matcherResults: [matcher()],
sourceClosure: source,
publicObjectClosure,
reports: [],
matcherResults: [],
candidate: {
schema: "portfolio-candidate-release-manifest/v1",
buildId: "r1",
sourceClosureDigest: canonicalJsonDigest(
"portfolio-publication-closure/v1",
source,
),
publicObjectClosureDigest: canonicalJsonDigest(
"portfolio-public-object-closure/v1",
publicObjectClosure,
),
},
});
expect(result.evidenceDigest).toMatch(/^[a-f0-9]{64}$/);
@@ -217,13 +362,24 @@ describe("isolated privacy closure and reconciliation", () => {
});
test("writes canonical evidence and candidate objects content-addressed without overwrite", async () => {
const source = sourceClosure();
const publicObjectClosure = publicClosure();
const result = reconcileScan({
expectedChecks: [{ inputDigest: h("1"), checkId: "bytes" }],
reports: [report("worker-a"), report("worker-b")],
matcherResults: [matcher()],
sourceClosure: source,
publicObjectClosure,
reports: [],
matcherResults: [],
candidate: {
schema: "portfolio-candidate-release-manifest/v1",
buildId: "r1",
sourceClosureDigest: canonicalJsonDigest(
"portfolio-publication-closure/v1",
source,
),
publicObjectClosureDigest: canonicalJsonDigest(
"portfolio-public-object-closure/v1",
publicObjectClosure,
),
},
});
const root = await mkdtemp(join(tmpdir(), "portfolio-evidence-"));

View File

@@ -0,0 +1,79 @@
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import {
canonicalJsonDigest,
canonicalValueBytes,
} from "../../src/security/canonical-digest.ts";
import {
verifySignedRelease,
verifySignedReleaseForTest,
} from "../../scripts/deploy/lib/release-common.mjs";
const h = (c: string) => c.repeat(64);
describe("protected release verification authority", () => {
test("production seam rejects caller-selected verifier, policy path, and time", async () => {
await expect(
verifySignedRelease({
signedManifestPath: "/not-used",
authorityRoot: "/not-used",
verifierModulePath: "/tmp/evil.mjs",
trustPolicyPath: "/tmp/evil.json",
now: "2000-01-01T00:00:00Z",
protectedState: {},
} as never),
).rejects.toThrow(/unknown|caller|production/i);
});
test("test seam still requires the protected trust-policy digest", async () => {
const root = await mkdtemp(join(tmpdir(), "portfolio-release-authority-"));
await mkdir(join(root, ".control"));
const signedManifestPath = join(root, "signed.json");
const trustPolicyPath = join(root, ".control", "release-trust.json");
const trustPolicy = {
schema: "portfolio-trust-policy/v1",
policyVersion: "test",
};
await writeFile(
signedManifestPath,
canonicalValueBytes({ schema: "signed" }),
);
await writeFile(trustPolicyPath, canonicalValueBytes(trustPolicy));
const protectedState = {
latestLedgerHeadHash: h("1"),
latestLedgerRevision: 1,
currentManifestDigest: null,
publisherFence: 0,
releaseTrustPolicyDigest: h("9"),
};
await expect(
verifySignedReleaseForTest({
signedManifestPath,
trustPolicyPath,
protectedState,
now: "2026-09-02T01:00:00Z",
verifySignedReleaseEnvelope: () => {
throw new Error("must not reach verifier");
},
}),
).rejects.toThrow(/trust.policy|authority|digest/i);
protectedState.releaseTrustPolicyDigest = canonicalJsonDigest(
"portfolio-trust-policy/v1",
trustPolicy,
);
await expect(
verifySignedReleaseForTest({
signedManifestPath,
trustPolicyPath,
protectedState,
now: "2026-09-02T01:00:00Z",
verifySignedReleaseEnvelope: () => {
throw new Error("reached-bound-verifier");
},
}),
).rejects.toThrow("reached-bound-verifier");
});
});

View File

@@ -9,19 +9,22 @@ import {
canonicalValueBytes,
} from "../../src/security/canonical-digest.ts";
import { computeProductionInputRoot } from "../../src/security/production-input-root.ts";
import { APPROVAL_PROJECTION_ATTESTATION_SCOPE } from "../../src/security/approval-closure.ts";
// @ts-expect-error executable release modules intentionally have no browser declarations
import * as releaseSigner from "../../scripts/release/sign-release.mjs";
const {
signReleaseCandidate,
signReleaseCandidateForTest,
validateTrustPolicy,
verifySignedReleaseEnvelope,
verifySignedReleaseEnvelopeForTest,
} = releaseSigner;
const h = (c: string) => c.repeat(64);
const roles = [
["approval", "portfolio-approval-decision/v1"],
["projection", "portfolio-approval-projection/v1"],
["projection", APPROVAL_PROJECTION_ATTESTATION_SCOPE],
["privacyWorkload", "portfolio-privacy-workload/v1"],
["policy", "portfolio-policy-attestation/v1"],
["toolImage", "portfolio-tool-image-provenance/v1"],
@@ -91,6 +94,9 @@ function envelope(
...payload,
};
return {
...(role === "projection"
? { schema: "portfolio-approval-projection-attestation-envelope/v1" }
: {}),
payload: body,
signature: {
algorithm: "Ed25519",
@@ -142,12 +148,27 @@ function fixture() {
expiresAt: "2026-09-02T12:00:00Z",
minimumBoundExpiry: "2026-09-02T12:00:00Z",
};
const sourceClosure = {
schema: "portfolio-publication-closure/v1",
members: [],
};
const publicObjectClosure = {
schema: "portfolio-public-object-closure/v1",
members: [],
};
const candidate = {
schema: "portfolio-candidate-release-manifest/v1",
releaseManifest,
latestLedgerHeadHash: h("1"),
latestLedgerRevision: 7,
sourceClosureDigest: h("4"),
sourceClosureDigest: canonicalJsonDigest(
"portfolio-publication-closure/v1",
sourceClosure,
),
publicObjectClosureDigest: canonicalJsonDigest(
"portfolio-public-object-closure/v1",
publicObjectClosure,
),
publicRefMap: {},
lfsOids: [],
releaseArchives: [],
@@ -157,40 +178,40 @@ function fixture() {
};
const evidence = {
schema: "portfolio-scan-evidence/v1",
coverageCount: 1,
passedCount: 1,
checks: [
{
inputDigest: h("5"),
checkId: "bytes",
normalizedOutputDigest: h("6"),
normalizedMembers: [`bytes:${h("7")}`],
workerReports: [h("8"), h("9")],
matcherResult: h("a"),
ruleSetDigest: h("b"),
appliedRuleIds: ["denylist-v1"],
},
],
sourceClosureDigest: candidate.sourceClosureDigest,
publicObjectClosureDigest: candidate.publicObjectClosureDigest,
coverageCount: 0,
passedCount: 0,
checks: [],
};
const evidenceDigest = canonicalJsonDigest(
"portfolio-scan-evidence/v1",
evidence,
);
candidate.releaseManifest.scanEvidenceDigest = evidenceDigest;
const projection = envelope("projection", roles[1][1], keyPairs, {
approvedRecordRoot: h("9"),
approvedAssetRoot: h("e"),
approvalProofSetDigest: h("f"),
inputDigests,
productionInputRoot: releaseManifest.productionInputRoot,
liveLedgerHeadHash: h("1"),
liveLedgerRevision: 7,
policyVersion: "2026-09-02",
expiresAt: "2026-09-02T12:00:00Z",
earliestApprovalExpiry: "2026-09-02T12:00:00Z",
});
candidate.releaseManifest.approvalProjectionAttestationDigest =
canonicalJsonDigest(
"portfolio-approval-projection-attestation-envelope/v1",
projection,
);
const candidateDigest = canonicalJsonDigest(
"portfolio-candidate-release-manifest/v1",
candidate,
);
const attestations = {
projection: envelope("projection", roles[1][1], keyPairs, {
digest: h("2"),
productionInputRoot: releaseManifest.productionInputRoot,
approvedRecordRoot: h("9"),
approvedAssetRoot: h("e"),
approvalProofSetDigest: h("f"),
latestLedgerHeadHash: h("1"),
latestLedgerRevision: 7,
}),
projection,
privacyWorkload: envelope("privacyWorkload", roles[2][1], keyPairs, {
digest: h("2"),
candidateDigest,
@@ -207,6 +228,8 @@ function fixture() {
};
return {
keyPairs,
sourceClosure,
publicObjectClosure,
candidate,
evidence,
candidateDigest,
@@ -217,6 +240,54 @@ function fixture() {
}
describe("external scoped release signing", () => {
test("production signer/verifier reject caller-selected time and signer reconstructs exact source/public closure", async () => {
const f = fixture();
const dir = await mkdtemp(join(tmpdir(), "portfolio-release-key-"));
const keyPath = join(dir, "release.pem");
await writeFile(
keyPath,
f.keyPairs.release!.privateKey.export({ type: "pkcs8", format: "pem" }),
{ mode: 0o400 },
);
await expect(
signReleaseCandidate({
...f,
releaseKeyPath: keyPath,
now: "2000-01-01T00:00:00Z",
protectedState: {
latestLedgerHeadHash: h("1"),
latestLedgerRevision: 7,
currentManifestDigest: null,
publisherFence: 4,
},
}),
).rejects.toThrow(/now|unknown|caller/i);
const nonEmptyPublic = {
schema: "portfolio-public-object-closure/v1",
members: [
{
objectId: "private.txt",
inputDigest: h("d"),
requiredChecks: ["secret"],
},
],
};
await expect(
signReleaseCandidateForTest({
...f,
publicObjectClosure: nonEmptyPublic,
releaseKeyPath: keyPath,
now: "2026-09-02T01:00:00Z",
protectedState: {
latestLedgerHeadHash: h("1"),
latestLedgerRevision: 7,
currentManifestDigest: null,
publisherFence: 4,
},
}),
).rejects.toThrow(/closure|digest|non-empty/i);
});
test("signs exact canonical candidate bytes with an external Ed25519 key", async () => {
const f = fixture();
const dir = await mkdtemp(join(tmpdir(), "portfolio-release-key-"));
@@ -226,7 +297,7 @@ describe("external scoped release signing", () => {
f.keyPairs.release!.privateKey.export({ type: "pkcs8", format: "pem" }),
{ mode: 0o400 },
);
const result = await signReleaseCandidate({
const result = await signReleaseCandidateForTest({
...f,
releaseKeyPath: keyPath,
now: "2026-09-02T01:00:00Z",
@@ -247,7 +318,7 @@ describe("external scoped release signing", () => {
),
).toBe(true);
expect(
verifySignedReleaseEnvelope({
verifySignedReleaseEnvelopeForTest({
signedRelease: result,
trustPolicy: f.trustPolicy,
now: "2026-09-02T01:00:00Z",
@@ -285,14 +356,14 @@ describe("external scoped release signing", () => {
currentManifestDigest: null,
publisherFence: 4,
};
const signedRelease = await signReleaseCandidate({
const signedRelease = await signReleaseCandidateForTest({
...f,
releaseKeyPath: keyPath,
now: "2026-09-02T01:00:00Z",
protectedState: state,
});
expect(() =>
verifySignedReleaseEnvelope({
verifySignedReleaseEnvelopeForTest({
signedRelease,
trustPolicy: f.trustPolicy,
now: "2026-09-02T01:00:00Z",
@@ -300,7 +371,7 @@ describe("external scoped release signing", () => {
}),
).toThrow(/stale/i);
expect(() =>
verifySignedReleaseEnvelope({
verifySignedReleaseEnvelopeForTest({
signedRelease: {
...signedRelease,
payload: { ...signedRelease.payload, latestLedgerRevision: 8 },
@@ -315,7 +386,7 @@ describe("external scoped release signing", () => {
test("refuses repository-resident keys and non-external key material", async () => {
const f = fixture();
await expect(
signReleaseCandidate({
signReleaseCandidateForTest({
...f,
releaseKeyPath: join(process.cwd(), "release.pem"),
now: "2026-09-02T01:00:00Z",
@@ -424,7 +495,7 @@ describe("external scoped release signing", () => {
{ mode: 0o400 },
);
await expect(
signReleaseCandidate({
signReleaseCandidateForTest({
...f,
releaseKeyPath: keyPath,
now: "2026-09-02T01:00:00Z",

View File

@@ -1,86 +1,242 @@
import { generateKeyPairSync, sign } from 'node:crypto';
import { describe, expect, test } from 'vitest';
import { generateKeyPairSync, sign } from "node:crypto";
import { describe, expect, test } from "vitest";
import { computeApprovalClosure, verifyApprovalProjectionAttestation } from '../../src/security/approval-closure.ts';
import { type ApprovalPayload } from '../../src/security/approval-envelope.ts';
import { canonicalJsonDigest, canonicalValueBytes } from '../../src/security/canonical-digest.ts';
import {
computeApprovalClosure,
verifyApprovalProjectionAttestation,
} from "../../src/security/approval-closure.ts";
import { type ApprovalPayload } from "../../src/security/approval-envelope.ts";
import {
canonicalJsonDigest,
canonicalValueBytes,
} from "../../src/security/canonical-digest.ts";
const h = (c: string) => c.repeat(64);
const keys = generateKeyPairSync('ed25519');
const trust = {
now: '2026-09-02T00:00:00Z', latestLedgerHeadHash: h('9'), latestLedgerRevision: 3,
trustedKeys: [{ keyId: 'key', issuerEpoch: 1, scope: 'portfolio-approval-decision/v1' as const, authorizedRoles: ['approver'], publicKey: keys.publicKey }],
};
const keys = generateKeyPairSync("ed25519");
function envelope(payload: ApprovalPayload) {
return {
schema: 'portfolio-approval-envelope/v1' as const,
schema: "portfolio-approval-envelope/v1" as const,
payload,
signature: { algorithm: 'Ed25519' as const, encoding: 'base64url-nopad' as const, value: sign(null, canonicalValueBytes(payload), keys.privateKey).toString('base64url') },
signature: {
algorithm: "Ed25519" as const,
encoding: "base64url-nopad" as const,
value: sign(null, canonicalValueBytes(payload), keys.privateKey).toString(
"base64url",
),
},
};
}
function base(recordClass: 'content' | 'asset', id: string): ApprovalPayload {
function base(
recordClass: "content" | "asset",
id: string,
ledgerRevision: number,
previousLedgerHeadHash: string,
): ApprovalPayload {
return {
schema: 'portfolio-approval-payload/v1', keyId: 'key', issuerEpoch: 1, authorizedRole: 'approver',
evidenceFingerprints: [h('1')], recordClass, recordType: recordClass === 'content' ? 'profile' : '',
publicRecordIdOrAssetId: id, approvedPublicRecordHashOrManifestRecordHash: h(recordClass === 'content' ? '2' : '3'),
sanitizedOutputHashOrNull: recordClass === 'asset' ? h('4') : null, decision: 'approve',
issuedAt: '2026-09-01T00:00:00Z', expiresAt: '2026-10-01T00:00:00Z', previousLedgerHeadHash: h('8'),
ledgerRevision: 2, policyVersion: 'p1',
schema: "portfolio-approval-payload/v1",
keyId: "key",
issuerEpoch: 1,
authorizedRole: "approver",
evidenceFingerprints: [h("1")],
recordClass,
recordType: recordClass === "content" ? "profile" : "",
publicRecordIdOrAssetId: id,
approvedPublicRecordHashOrManifestRecordHash: h(
recordClass === "content" ? "2" : "3",
),
sanitizedOutputHashOrNull: recordClass === "asset" ? h("4") : null,
decision: "approve",
issuedAt: "2026-09-01T00:00:00Z",
expiresAt: "2026-10-01T00:00:00Z",
previousLedgerHeadHash,
ledgerRevision,
policyVersion: "p1",
};
}
describe('approval closure', () => {
test('computes exact sorted record, asset, and proof roots', () => {
const content = base('content', 'profile-main');
const asset = base('asset', 'asset-main');
function ledgerTrust(entries: ReturnType<typeof envelope>[]) {
let head = h("0");
entries.forEach((entry, index) => {
const envelopeDigest = canonicalJsonDigest(
"portfolio-approval-envelope-digest/v1",
entry,
);
head = canonicalJsonDigest("portfolio-approval-ledger-entry/v1", {
previousLedgerHeadHash: head,
ledgerRevision: index + 1,
envelopeDigest,
});
});
return {
now: "2026-09-02T00:00:00Z",
latestLedgerHeadHash: head,
latestLedgerRevision: entries.length,
trustedKeys: [
{
keyId: "key",
issuerEpoch: 1,
scope: "portfolio-approval-decision/v1" as const,
authorizedRoles: ["approver"],
publicKey: keys.publicKey,
},
],
ledgerProof: {
schema: "portfolio-approval-ledger-proof/v1" as const,
genesisHeadHash: h("0"),
entries,
},
};
}
describe("approval closure", () => {
test("computes exact sorted record, asset, and proof roots", () => {
const content = base("content", "profile-main", 1, h("0"));
const contentEnvelope = envelope(content);
const head1 = canonicalJsonDigest("portfolio-approval-ledger-entry/v1", {
previousLedgerHeadHash: h("0"),
ledgerRevision: 1,
envelopeDigest: canonicalJsonDigest(
"portfolio-approval-envelope-digest/v1",
contentEnvelope,
),
});
const asset = base("asset", "asset-main", 2, head1);
const assetEnvelope = envelope(asset);
const result = computeApprovalClosure({
contentRecords: [{ recordType: 'profile', publicRecordId: 'profile-main', approvedPublicRecordHash: h('2'), envelope: envelope(content) }],
assetRecords: [{ assetId: 'asset-main', approvedManifestRecordHash: h('3'), sanitizedOutputHash: h('4'), envelope: envelope(asset) }],
contentRecords: [
{
recordType: "profile",
publicRecordId: "profile-main",
approvedPublicRecordHash: h("2"),
envelope: contentEnvelope,
},
],
assetRecords: [
{
assetId: "asset-main",
approvedManifestRecordHash: h("3"),
sanitizedOutputHash: h("4"),
envelope: assetEnvelope,
},
],
trust: ledgerTrust([contentEnvelope, assetEnvelope]),
});
const contentEnvelopeDigest = canonicalJsonDigest(
"portfolio-approval-envelope-digest/v1",
envelope(content),
);
const assetEnvelopeDigest = canonicalJsonDigest(
"portfolio-approval-envelope-digest/v1",
envelope(asset),
);
expect(result.approvedRecordRoot).toBe(
canonicalJsonDigest("portfolio-record-closure/v1", {
records: [["profile", "profile-main", h("2"), contentEnvelopeDigest]],
}),
);
expect(result.approvedAssetRoot).toBe(
canonicalJsonDigest("portfolio-asset-closure/v1", {
assets: [["asset-main", h("3"), h("4"), assetEnvelopeDigest]],
}),
);
expect(result.approvalProofSetDigest).toBe(
canonicalJsonDigest("portfolio-approval-proof-set/v1", {
envelopes: [
["asset", "", "asset-main", assetEnvelopeDigest],
["content", "profile", "profile-main", contentEnvelopeDigest],
],
}),
);
});
test("rejects duplicate IDs and stale protected ledger input", () => {
const content = base("content", "profile-main", 1, h("0"));
const record = {
recordType: "profile",
publicRecordId: "profile-main",
approvedPublicRecordHash: h("2"),
envelope: envelope(content),
};
const trust = ledgerTrust([record.envelope]);
expect(() =>
computeApprovalClosure({
contentRecords: [record, record],
assetRecords: [],
trust,
});
const contentEnvelopeDigest = canonicalJsonDigest('portfolio-approval-envelope-digest/v1', envelope(content));
const assetEnvelopeDigest = canonicalJsonDigest('portfolio-approval-envelope-digest/v1', envelope(asset));
expect(result.approvedRecordRoot).toBe(canonicalJsonDigest('portfolio-record-closure/v1', {
records: [['profile', 'profile-main', h('2'), contentEnvelopeDigest]],
}));
expect(result.approvedAssetRoot).toBe(canonicalJsonDigest('portfolio-asset-closure/v1', {
assets: [['asset-main', h('3'), h('4'), assetEnvelopeDigest]],
}));
expect(result.approvalProofSetDigest).toBe(canonicalJsonDigest('portfolio-approval-proof-set/v1', {
envelopes: [['asset', '', 'asset-main', assetEnvelopeDigest], ['content', 'profile', 'profile-main', contentEnvelopeDigest]],
}));
}),
).toThrow();
expect(() =>
computeApprovalClosure({
contentRecords: [record],
assetRecords: [],
trust: { ...trust, latestLedgerRevision: 0 },
}),
).toThrow();
});
test('rejects duplicate IDs and stale protected ledger input', () => {
const content = base('content', 'profile-main');
const record = { recordType: 'profile', publicRecordId: 'profile-main', approvedPublicRecordHash: h('2'), envelope: envelope(content) };
expect(() => computeApprovalClosure({ contentRecords: [record, record], assetRecords: [], trust })).toThrow();
expect(() => computeApprovalClosure({ contentRecords: [record], assetRecords: [], trust: { ...trust, latestLedgerRevision: 1 } })).toThrow();
});
test('verifies a scoped projection attestation against protected state and trusted time', () => {
test("verifies a scoped projection attestation against protected state and trusted time", () => {
const payload = {
schema: 'portfolio-approval-projection-attestation/v1' as const,
keyId: 'projection-key', issuerEpoch: 4, scope: 'portfolio-approval-projection-attestation/v1' as const,
approvedRecordRoot: h('1'), approvedAssetRoot: h('2'), approvalProofSetDigest: h('3'),
inputDigests: { PortfolioContent: h('4'), PublicAssetManifest: h('5'), PreparedAssetSet: h('6'), SiteConfig: h('7') },
productionInputRoot: 'e19fd4083ba973074449e5ecc9364c2b339eadea32e61d8dd2f0d8e3d6e91c85',
liveLedgerHeadHash: h('8'), liveLedgerRevision: 42, policyVersion: 'projection-policy-1',
issuedAt: '2026-09-02T00:00:00Z', expiresAt: '2026-09-03T00:00:00Z', earliestApprovalExpiry: '2026-09-04T00:00:00Z',
schema: "portfolio-approval-projection-attestation/v1" as const,
keyId: "projection-key",
issuerEpoch: 4,
scope: "portfolio-approval-projection-attestation/v1" as const,
approvedRecordRoot: h("1"),
approvedAssetRoot: h("2"),
approvalProofSetDigest: h("3"),
inputDigests: {
PortfolioContent: h("4"),
PublicAssetManifest: h("5"),
PreparedAssetSet: h("6"),
SiteConfig: h("7"),
},
productionInputRoot:
"e19fd4083ba973074449e5ecc9364c2b339eadea32e61d8dd2f0d8e3d6e91c85",
liveLedgerHeadHash: h("8"),
liveLedgerRevision: 42,
policyVersion: "projection-policy-1",
issuedAt: "2026-09-02T00:00:00Z",
expiresAt: "2026-09-03T00:00:00Z",
earliestApprovalExpiry: "2026-09-04T00:00:00Z",
};
const projectionKeys = generateKeyPairSync('ed25519');
const projectionKeys = generateKeyPairSync("ed25519");
const attestation = {
schema: 'portfolio-approval-projection-attestation-envelope/v1' as const,
schema: "portfolio-approval-projection-attestation-envelope/v1" as const,
payload,
signature: { algorithm: 'Ed25519' as const, encoding: 'base64url-nopad' as const, value: sign(null, canonicalValueBytes(payload), projectionKeys.privateKey).toString('base64url') },
signature: {
algorithm: "Ed25519" as const,
encoding: "base64url-nopad" as const,
value: sign(
null,
canonicalValueBytes(payload),
projectionKeys.privateKey,
).toString("base64url"),
},
};
expect(verifyApprovalProjectionAttestation(attestation, {
now: '2026-09-02T01:00:00Z', latestLedgerHeadHash: h('8'), latestLedgerRevision: 42,
trustedKey: { keyId: 'projection-key', issuerEpoch: 4, scope: 'portfolio-approval-projection-attestation/v1', publicKey: projectionKeys.publicKey },
}).liveLedgerRevision).toBe(42);
expect(() => verifyApprovalProjectionAttestation(attestation, {
now: '2026-09-02T01:00:00Z', latestLedgerHeadHash: h('9'), latestLedgerRevision: 42,
trustedKey: { keyId: 'projection-key', issuerEpoch: 4, scope: 'portfolio-approval-projection-attestation/v1', publicKey: projectionKeys.publicKey },
})).toThrow();
expect(
verifyApprovalProjectionAttestation(attestation, {
now: "2026-09-02T01:00:00Z",
latestLedgerHeadHash: h("8"),
latestLedgerRevision: 42,
trustedKey: {
keyId: "projection-key",
issuerEpoch: 4,
scope: "portfolio-approval-projection-attestation/v1",
publicKey: projectionKeys.publicKey,
},
}).liveLedgerRevision,
).toBe(42);
expect(() =>
verifyApprovalProjectionAttestation(attestation, {
now: "2026-09-02T01:00:00Z",
latestLedgerHeadHash: h("9"),
latestLedgerRevision: 42,
trustedKey: {
keyId: "projection-key",
issuerEpoch: 4,
scope: "portfolio-approval-projection-attestation/v1",
publicKey: projectionKeys.publicKey,
},
}),
).toThrow();
});
});

View File

@@ -1,82 +1,137 @@
import { generateKeyPairSync, sign } from 'node:crypto';
import { describe, expect, test } from 'vitest';
import { generateKeyPairSync, sign } from "node:crypto";
import { describe, expect, test } from "vitest";
import {
type ApprovalPayload,
parseApprovalEnvelopeJson,
verifyApprovalEnvelope,
} from '../../src/security/approval-envelope.ts';
import { canonicalValueBytes } from '../../src/security/canonical-digest.ts';
} from "../../src/security/approval-envelope.ts";
import {
canonicalJsonDigest,
canonicalValueBytes,
} from "../../src/security/canonical-digest.ts";
const hash = (character: string) => character.repeat(64);
const payload = (): ApprovalPayload => ({
schema: 'portfolio-approval-payload/v1',
keyId: 'approval-2026',
schema: "portfolio-approval-payload/v1",
keyId: "approval-2026",
issuerEpoch: 3,
authorizedRole: 'portfolio-approver',
evidenceFingerprints: [hash('a')],
recordClass: 'content',
recordType: 'profile',
publicRecordIdOrAssetId: 'profile-main',
approvedPublicRecordHashOrManifestRecordHash: hash('b'),
authorizedRole: "portfolio-approver",
evidenceFingerprints: [hash("a")],
recordClass: "content",
recordType: "profile",
publicRecordIdOrAssetId: "profile-main",
approvedPublicRecordHashOrManifestRecordHash: hash("b"),
sanitizedOutputHashOrNull: null,
decision: 'approve',
issuedAt: '2026-09-01T08:00:00Z',
expiresAt: '2026-10-01T08:00:00Z',
previousLedgerHeadHash: hash('c'),
ledgerRevision: 12,
policyVersion: 'approval-policy-1',
decision: "approve",
issuedAt: "2026-09-01T08:00:00Z",
expiresAt: "2026-10-01T08:00:00Z",
previousLedgerHeadHash: hash("0"),
ledgerRevision: 1,
policyVersion: "approval-policy-1",
});
function signedEnvelope(overrides: Partial<ApprovalPayload> = {}) {
const keys = generateKeyPairSync('ed25519');
const keys = generateKeyPairSync("ed25519");
const signedPayload = { ...payload(), ...overrides };
const signature = sign(null, canonicalValueBytes(signedPayload), keys.privateKey).toString('base64url');
const signature = sign(
null,
canonicalValueBytes(signedPayload),
keys.privateKey,
).toString("base64url");
return {
envelope: {
schema: 'portfolio-approval-envelope/v1' as const,
schema: "portfolio-approval-envelope/v1" as const,
payload: signedPayload,
signature: { algorithm: 'Ed25519' as const, encoding: 'base64url-nopad' as const, value: signature },
signature: {
algorithm: "Ed25519" as const,
encoding: "base64url-nopad" as const,
value: signature,
},
},
publicKey: keys.publicKey,
};
}
describe('approval envelopes', () => {
test('verifies an exact, current, scoped Ed25519 approval', () => {
function trustFor(
envelope: ReturnType<typeof signedEnvelope>["envelope"],
publicKey: ReturnType<typeof signedEnvelope>["publicKey"],
) {
const envelopeDigest = canonicalJsonDigest(
"portfolio-approval-envelope-digest/v1",
envelope,
);
const latestLedgerHeadHash = canonicalJsonDigest(
"portfolio-approval-ledger-entry/v1",
{
previousLedgerHeadHash: hash("0"),
ledgerRevision: 1,
envelopeDigest,
},
);
return {
now: "2026-09-02T00:00:00Z",
latestLedgerHeadHash,
latestLedgerRevision: 1,
trustedKeys: [
{
keyId: "approval-2026",
issuerEpoch: 3,
scope: "portfolio-approval-decision/v1" as const,
authorizedRoles: ["portfolio-approver"],
publicKey,
},
],
ledgerProof: {
schema: "portfolio-approval-ledger-proof/v1" as const,
genesisHeadHash: hash("0"),
entries: [envelope],
},
};
}
describe("approval envelopes", () => {
test("verifies an exact, current, scoped Ed25519 approval", () => {
const { envelope, publicKey } = signedEnvelope();
const verified = verifyApprovalEnvelope(envelope, {
now: '2026-09-02T00:00:00Z',
latestLedgerHeadHash: hash('d'),
latestLedgerRevision: 13,
trustedKeys: [{
keyId: 'approval-2026', issuerEpoch: 3, scope: 'portfolio-approval-decision/v1',
authorizedRoles: ['portfolio-approver'], publicKey,
}],
});
expect(verified.publicRecordIdOrAssetId).toBe('profile-main');
const verified = verifyApprovalEnvelope(
envelope,
trustFor(envelope, publicKey),
);
expect(verified.publicRecordIdOrAssetId).toBe("profile-main");
});
test.each([
['expired', { expiresAt: '2026-09-01T09:00:00Z' }],
['non-UTC timestamp', { issuedAt: '2026-09-01T10:00:00+02:00' }],
['wrong epoch', { issuerEpoch: 2 }],
['rejection', { decision: 'reject' }],
] as const)('rejects a %s envelope', (_label, override) => {
["expired", { expiresAt: "2026-09-01T09:00:00Z" }],
["non-UTC timestamp", { issuedAt: "2026-09-01T10:00:00+02:00" }],
["wrong epoch", { issuerEpoch: 2 }],
["rejection", { decision: "reject" }],
] as const)("rejects a %s envelope", (_label, override) => {
const { envelope, publicKey } = signedEnvelope(override);
expect(() => verifyApprovalEnvelope(envelope, {
now: '2026-09-02T00:00:00Z', latestLedgerHeadHash: hash('d'), latestLedgerRevision: 13,
trustedKeys: [{ keyId: 'approval-2026', issuerEpoch: 3, scope: 'portfolio-approval-decision/v1', authorizedRoles: ['portfolio-approver'], publicKey }],
})).toThrow();
expect(() =>
verifyApprovalEnvelope(envelope, trustFor(envelope, publicKey)),
).toThrow();
});
test('rejects unknown fields, duplicate keys, and alternate signature encodings at ingest', () => {
test("rejects unknown fields, duplicate keys, and alternate signature encodings at ingest", () => {
const { envelope } = signedEnvelope();
expect(() => parseApprovalEnvelopeJson(JSON.stringify({ ...envelope, extra: true }))).toThrow();
expect(() => parseApprovalEnvelopeJson('{"schema":"portfolio-approval-envelope/v1","schema":"portfolio-approval-envelope/v1"}')).toThrow();
expect(() => parseApprovalEnvelopeJson(JSON.stringify({
expect(() =>
parseApprovalEnvelopeJson(JSON.stringify({ ...envelope, extra: true })),
).toThrow();
expect(() =>
parseApprovalEnvelopeJson(
'{"schema":"portfolio-approval-envelope/v1","schema":"portfolio-approval-envelope/v1"}',
),
).toThrow();
expect(() =>
parseApprovalEnvelopeJson(
JSON.stringify({
...envelope,
signature: { ...envelope.signature, value: `${envelope.signature.value}=` },
}))).toThrow();
signature: {
...envelope.signature,
value: `${envelope.signature.value}=`,
},
}),
),
).toThrow();
});
});