feat: isolate privacy scanning and release signing

This commit is contained in:
2026-09-02 10:07:01 +03:00
parent 847e6e4da9
commit 8592581b18
15 changed files with 2128 additions and 1 deletions

View File

@@ -0,0 +1,136 @@
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { lstat, readFile, readdir, realpath } from "node:fs/promises";
import { relative, resolve, sep } from "node:path";
import { pathToFileURL } from "node:url";
import {
canonicalJsonDigest,
canonicalValueBytes,
} from "../../src/security/canonical-digest.ts";
const validClass =
/^(?:git-object|git-ref|lfs|source|build|precompressed|release-archive)$/u;
const byteCompare = (left, right) =>
Buffer.from(left).compare(Buffer.from(right));
function normalizedRelativePath(root, path) {
const value = relative(root, path).split(sep).join("/");
if (
value === "" ||
value.startsWith("../") ||
value.startsWith("/") ||
value.includes("/../") ||
value !== value.normalize("NFC")
) {
throw new Error(
"Closure member path is outside the root or non-canonical.",
);
}
return value;
}
async function walk(root, current, members, memberClass) {
const entries = await readdir(current, { withFileTypes: true });
entries.sort((a, b) => byteCompare(a.name, b.name));
for (const entry of entries) {
if (
entry.name !== entry.name.normalize("NFC") ||
entry.name === "." ||
entry.name === ".."
)
throw new Error("Closure path is non-canonical.");
const path = resolve(current, entry.name);
const info = await lstat(path);
if (info.isSymbolicLink())
throw new Error("Closure cannot contain a symlink.");
if (info.isDirectory()) {
await walk(root, path, members, memberClass);
continue;
}
if (!info.isFile())
throw new Error(
"Closure cannot contain device, socket, FIFO, or other special entries.",
);
if (info.nlink !== 1)
throw new Error("Closure cannot contain hard-link aliases.");
const bytes = await readFile(path);
members.push({
class: memberClass,
path: normalizedRelativePath(root, path),
bytes: bytes.byteLength,
sha256: createHash("sha256").update(bytes).digest("hex"),
});
}
}
export async function enumerateClosure(inputs) {
if (!Array.isArray(inputs) || inputs.length === 0)
throw new Error("At least one explicit closure root is required.");
const members = [];
const roots = new Set();
for (const input of inputs) {
if (
input === null ||
typeof input !== "object" ||
!validClass.test(input.class) ||
typeof input.root !== "string"
) {
throw new Error("Closure root descriptor is invalid.");
}
const root = await realpath(input.root);
if (roots.has(`${input.class}\0${root}`))
throw new Error("Duplicate closure root.");
roots.add(`${input.class}\0${root}`);
const info = await lstat(root);
if (!info.isDirectory() || info.isSymbolicLink())
throw new Error("Closure root must be a real directory.");
await walk(root, root, members, input.class);
}
members.sort((a, b) =>
byteCompare(`${a.class}\0${a.path}`, `${b.class}\0${b.path}`),
);
const identities = new Set();
const folded = new Set();
for (const member of members) {
const identity = `${member.class}\0${member.path}`;
const fold = identity.toLocaleLowerCase("en-US");
if (identities.has(identity) || folded.has(fold))
throw new Error("Closure contains a duplicate or case-fold collision.");
identities.add(identity);
folded.add(fold);
}
const value = { schema: "portfolio-publication-closure/v1", members };
return {
...value,
digest: canonicalJsonDigest("portfolio-publication-closure/v1", value),
};
}
async function main() {
const descriptors = process.argv.slice(2).map((argument) => {
const separator = argument.indexOf("=");
if (separator < 1)
throw new Error("Usage: enumerate-closure.mjs <class>=<directory> [...]");
return {
class: argument.slice(0, separator),
root: argument.slice(separator + 1),
};
});
process.stdout.write(
Buffer.from(canonicalValueBytes(await enumerateClosure(descriptors))),
);
process.stdout.write("\n");
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
main().catch((error) => {
process.stderr.write(
`Closure enumeration failed: ${error instanceof Error ? error.message : "unknown error"}\n`,
);
process.exitCode = 1;
});
}

View File

@@ -0,0 +1,282 @@
#!/usr/bin/env node
import { mkdir, open, readFile } from "node:fs/promises";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import {
canonicalJsonDigest,
canonicalValueBytes,
isSha256Hex,
parseCanonicalJson,
} from "../../src/security/canonical-digest.ts";
const workerFields = [
"schema",
"workerId",
"imageDigest",
"toolchain",
"inputDigest",
"checkId",
"normalizedOutputDigest",
"normalizedMembers",
"verdict",
"findingsDigest",
];
const matcherFields = [
"schema",
"inputDigest",
"checkId",
"normalizedOutputDigest",
"normalizedMembers",
"ruleSetDigest",
"appliedRuleIds",
"verdict",
"workerImageDigests",
];
const safeToken = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/u;
function object(value, label) {
if (value === null || typeof value !== "object" || Array.isArray(value))
throw new Error(`${label} must be an object.`);
return value;
}
function exact(value, fields, label) {
if (
JSON.stringify(Object.keys(value).sort()) !==
JSON.stringify([...fields].sort())
)
throw new Error(`${label} contains missing or extra fields.`);
}
function stringsEqual(left, right) {
return JSON.stringify(left) === JSON.stringify(right);
}
function validateMembers(value) {
if (
!Array.isArray(value) ||
value.some(
(member) =>
typeof member !== "string" ||
member !== member.normalize("NFC") ||
!/^[a-z][a-z0-9-]*:[a-f0-9]{64}$/u.test(member),
)
)
throw new Error("Normalized members must contain typed digests only.");
const sorted = [...value].sort((a, b) =>
Buffer.from(a).compare(Buffer.from(b)),
);
if (!stringsEqual(value, sorted) || new Set(value).size !== value.length)
throw new Error("Normalized members must be unique and byte sorted.");
}
function validateWorker(value, expectedWorker) {
const report = object(value, "worker report");
exact(report, workerFields, "worker report");
if (
report.schema !== "portfolio-privacy-worker-report/v1" ||
report.workerId !== expectedWorker ||
!isSha256Hex(report.imageDigest) ||
!safeToken.test(report.toolchain) ||
!isSha256Hex(report.inputDigest) ||
!safeToken.test(report.checkId) ||
!isSha256Hex(report.normalizedOutputDigest) ||
!isSha256Hex(report.findingsDigest) ||
!["pass", "fail", "unsupported"].includes(report.verdict)
)
throw new Error("Worker report is invalid.");
validateMembers(report.normalizedMembers);
return report;
}
function validateMatcher(value) {
const result = object(value, "matcher result");
exact(result, matcherFields, "matcher result");
if (
result.schema !== "portfolio-privacy-matcher-result/v1" ||
!isSha256Hex(result.inputDigest) ||
!safeToken.test(result.checkId) ||
!isSha256Hex(result.normalizedOutputDigest) ||
!isSha256Hex(result.ruleSetDigest) ||
!["pass", "fail"].includes(result.verdict) ||
!Array.isArray(result.workerImageDigests) ||
result.workerImageDigests.length !== 2 ||
result.workerImageDigests.some((v) => !isSha256Hex(v))
)
throw new Error("Matcher result is invalid.");
validateMembers(result.normalizedMembers);
if (
!Array.isArray(result.appliedRuleIds) ||
result.appliedRuleIds.length === 0 ||
result.appliedRuleIds.some((value) => !safeToken.test(value)) ||
new Set(result.appliedRuleIds).size !== result.appliedRuleIds.length ||
!stringsEqual(
result.appliedRuleIds,
[...result.appliedRuleIds].sort((a, b) =>
Buffer.from(a).compare(Buffer.from(b)),
),
)
)
throw new Error("Matcher rule IDs must be unique and byte sorted.");
return result;
}
async function writeOnce(root, digest, bytes) {
await mkdir(root, { recursive: true });
const path = join(root, digest);
try {
const handle = await open(path, "wx", 0o400);
try {
await handle.writeFile(bytes);
await handle.sync();
} finally {
await handle.close();
}
} catch (error) {
if (error && error.code === "EEXIST") {
const existing = await readFile(path);
if (Buffer.compare(existing, Buffer.from(bytes)) !== 0)
throw new Error(
"Content-addressed object is immutable and differs from existing bytes.",
);
return;
}
throw error;
}
}
export function reconcileScan({
expectedChecks,
reports,
matcherResults,
candidate,
}) {
if (
!Array.isArray(expectedChecks) ||
!Array.isArray(reports) ||
!Array.isArray(matcherResults) ||
expectedChecks.length === 0
)
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}`),
),
);
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
)
throw new Error(
"Privacy evidence is missing, reordered, or bound to another check.",
);
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,
});
});
const evidence = {
schema: "portfolio-scan-evidence/v1",
coverageCount: checks.length,
passedCount: checks.length,
checks,
};
const evidenceBytes = canonicalValueBytes(evidence);
const evidenceDigest = canonicalJsonDigest(
"portfolio-scan-evidence/v1",
evidence,
);
const candidateRecord = object(candidate, "candidate");
if (candidateRecord.schema !== "portfolio-candidate-release-manifest/v1")
throw new Error("Candidate schema is invalid.");
const candidateBytes = canonicalValueBytes(candidateRecord);
const candidateDigest = canonicalJsonDigest(
"portfolio-candidate-release-manifest/v1",
candidateRecord,
);
return {
evidence,
evidenceBytes,
evidenceDigest,
candidate: candidateRecord,
candidateBytes,
candidateDigest,
async writeContentAddressed(root) {
await writeOnce(root, evidenceDigest, evidenceBytes);
await writeOnce(root, candidateDigest, candidateBytes);
},
};
}
async function main() {
if (process.argv.length !== 4 || process.argv[2] !== "--request")
throw new Error(
"Usage: reconcile-scan.mjs --request <canonical-request.json>",
);
const request = parseCanonicalJson(await readFile(process.argv[3]));
const result = reconcileScan(request);
process.stdout.write(
Buffer.from(
canonicalValueBytes({
schema: "portfolio-reconciliation-result/v1",
evidenceDigest: result.evidenceDigest,
candidateDigest: result.candidateDigest,
}),
),
);
process.stdout.write("\n");
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
main().catch((error) => {
process.stderr.write(
`Privacy reconciliation failed: ${error instanceof Error ? error.message : "unknown error"}\n`,
);
process.exitCode = 1;
});
}

View File

@@ -0,0 +1,115 @@
#!/usr/bin/env node
import { pathToFileURL } from "node:url";
const safeImage = /^[A-Za-z0-9._/-]+@sha256:[a-f0-9]{64}$/u;
const safePath = /^\/[A-Za-z0-9._/-]+$/u;
export function buildWorkerCommand({
runtime,
image,
inputPath,
outputPath,
worker,
}) {
if (runtime !== "docker" && runtime !== "podman")
throw new Error("Only Docker or Podman isolation runtimes are supported.");
if (!safeImage.test(image))
throw new Error(
"Worker image must be pinned by a lowercase SHA-256 digest.",
);
if (
!safePath.test(inputPath) ||
!safePath.test(outputPath) ||
inputPath.includes("..") ||
outputPath.includes("..")
)
throw new Error("Worker mounts must be explicit absolute safe paths.");
if (
worker !== "worker-a" &&
worker !== "worker-b" &&
worker !== "matcher" &&
worker !== "coordinator"
)
throw new Error("Unknown isolated workload.");
return [
runtime,
"run",
"--rm",
"--pull=never",
"--network=none",
"--read-only",
"--cap-drop=ALL",
"--security-opt=no-new-privileges",
"--user=65532:65532",
"--memory=256m",
"--memory-swap=256m",
"--cpus=1",
"--pids-limit=64",
"--tmpfs=/tmp:rw,noexec,nosuid,nodev,size=64m",
`--mount=type=bind,src=${inputPath},dst=/input,readonly`,
`--mount=type=bind,src=${outputPath},dst=/output`,
image,
];
}
export function validateScanObjectDescriptor(value) {
if (
value === null ||
typeof value !== "object" ||
!/^[a-f0-9]{64}$/u.test(value.inputDigest) ||
!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/u.test(value.checkId) ||
typeof value.mediaType !== "string"
)
throw new Error("Scan object descriptor is invalid.");
if (value.encrypted === true)
throw new Error("Encrypted containers are unsupported and fail closed.");
if (value.malformed === true)
throw new Error("Malformed containers fail closed.");
if (
!Number.isSafeInteger(value.archiveDepth) ||
value.archiveDepth < 0 ||
value.archiveDepth > 3
)
throw new Error("Nested archive depth exceeds the limit.");
if (
!Number.isSafeInteger(value.expandedBytes) ||
value.expandedBytes < 0 ||
value.expandedBytes > 64 * 1024 * 1024
)
throw new Error("Expanded archive size exceeds the limit.");
if (!Array.isArray(value.members))
throw new Error("Container member inventory is required.");
for (const member of value.members) {
if (
member === null ||
typeof member !== "object" ||
typeof member.path !== "string" ||
member.path.startsWith("/") ||
member.path.includes("\\") ||
member.path.split("/").some((part) => part === ".." || part === ".") ||
member.path !== member.path.normalize("NFC") ||
!["file", "directory"].includes(member.entryType)
) {
throw new Error(
"Archive traversal, symlink, hard-link, device, or non-canonical member rejected.",
);
}
}
return value;
}
export function liveContainerStatus() {
return {
schema: "portfolio-live-container-validation/v1",
status: "pending",
reason:
"Requires locally cached, provenance-verified pinned images; network pulls are prohibited.",
};
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
process.stdout.write(`${JSON.stringify(liveContainerStatus())}\n`);
}

View File

@@ -0,0 +1,700 @@
#!/usr/bin/env node
import { createPrivateKey, createPublicKey, sign, verify } from "node:crypto";
import { readFile, realpath, stat as fileStat } from "node:fs/promises";
import { isAbsolute, relative, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import {
canonicalJsonDigest,
canonicalValueBytes,
isSha256Hex,
parseCanonicalJson,
} from "../../src/security/canonical-digest.ts";
import { validateReleaseManifest } from "../../src/security/release-manifest.ts";
const rootContract = {
approval: {
scope: "portfolio-approval-decision/v1",
recovery: ["freeze-publication", "audit-checkpoint", "fresh-approvals"],
},
projection: {
scope: "portfolio-approval-projection/v1",
recovery: ["freeze-publication", "revoke-epoch", "fresh-evidence"],
},
privacyWorkload: {
scope: "portfolio-privacy-workload/v1",
recovery: ["freeze-publication", "revoke-epoch", "fresh-evidence"],
},
policy: {
scope: "portfolio-policy-attestation/v1",
recovery: ["freeze-publication", "revoke-epoch", "fresh-evidence"],
},
toolImage: {
scope: "portfolio-tool-image-provenance/v1",
recovery: ["freeze-publication", "revoke-epoch", "fresh-evidence"],
},
release: {
scope: "portfolio-release-signing/v1",
recovery: ["freeze-publication", "revoke-epoch", "fresh-evidence"],
},
publisher: {
scope: "portfolio-publisher-fencing/v1",
recovery: [
"freeze-publication",
"rotate-credential",
"advance-fence",
"reconcile-staging",
],
},
};
const rootFields = [
"keyId",
"issuerEpoch",
"scope",
"publicKeyPem",
"revokedEpochs",
"compromiseResponse",
];
const policyFields = ["schema", "policyVersion", "publicationFrozen", "roots"];
const candidateFields = [
"schema",
"releaseManifest",
"latestLedgerHeadHash",
"latestLedgerRevision",
"sourceClosureDigest",
"publicRefMap",
"lfsOids",
"releaseArchives",
"buildProvenanceDigest",
"privacyPolicyDigest",
"toolImageDigests",
];
const signatureFields = [
"algorithm",
"encoding",
"keyId",
"issuerEpoch",
"scope",
"value",
];
const attestationPayloadFields = {
projection: [
"schema",
"keyId",
"issuerEpoch",
"scope",
"issuedAt",
"expiresAt",
"digest",
"productionInputRoot",
"approvedRecordRoot",
"approvedAssetRoot",
"approvalProofSetDigest",
"latestLedgerHeadHash",
"latestLedgerRevision",
],
privacyWorkload: [
"schema",
"keyId",
"issuerEpoch",
"scope",
"issuedAt",
"expiresAt",
"digest",
"candidateDigest",
"evidenceDigest",
],
policy: [
"schema",
"keyId",
"issuerEpoch",
"scope",
"issuedAt",
"expiresAt",
"digest",
"policyVersion",
],
toolImage: [
"schema",
"keyId",
"issuerEpoch",
"scope",
"issuedAt",
"expiresAt",
"digest",
"imageDigests",
],
};
const signedFields = ["schema", "payload", "payloadDigest", "signature"];
const timestamp = /^\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 object(value, label) {
if (value === null || typeof value !== "object" || Array.isArray(value))
throw new Error(`${label} must be an object.`);
return value;
}
function exact(value, fields, label) {
if (
JSON.stringify(Object.keys(value).sort()) !==
JSON.stringify([...fields].sort())
)
throw new Error(`${label} contains missing or unknown fields.`);
}
function digest(value, label) {
if (!isSha256Hex(value))
throw new Error(`${label} is not a canonical SHA-256 digest.`);
}
function compare(left, right) {
return Buffer.from(left).compare(Buffer.from(right));
}
export function validateTrustPolicy(value) {
const policy = object(value, "trust policy");
exact(policy, policyFields, "trust policy");
if (
policy.schema !== "portfolio-trust-policy/v1" ||
typeof policy.policyVersion !== "string" ||
!safeToken.test(policy.policyVersion) ||
typeof policy.publicationFrozen !== "boolean"
)
throw new Error("Trust policy metadata is invalid.");
const roots = object(policy.roots, "trust roots");
exact(roots, Object.keys(rootContract), "trust roots");
const seenKeys = new Set();
const seenPublicKeys = new Set();
for (const [role, contract] of Object.entries(rootContract)) {
const root = object(roots[role], `${role} root`);
exact(root, rootFields, `${role} root`);
if (
typeof root.keyId !== "string" ||
!safeToken.test(root.keyId) ||
seenKeys.has(root.keyId) ||
!Number.isSafeInteger(root.issuerEpoch) ||
root.issuerEpoch < 1
)
throw new Error(`${role} key ID or issuer epoch is invalid.`);
seenKeys.add(root.keyId);
if (root.scope !== contract.scope)
throw new Error(`${role} trust-root scope is invalid.`);
if (typeof root.publicKeyPem !== "string")
throw new Error(`${role} public key is invalid.`);
const key = createPublicKey(root.publicKeyPem);
if (key.asymmetricKeyType !== "ed25519")
throw new Error(`${role} key must be Ed25519.`);
const publicFingerprint = key
.export({ type: "spki", format: "der" })
.toString("base64");
if (seenPublicKeys.has(publicFingerprint))
throw new Error("Trust roots must use distinct protected keys.");
seenPublicKeys.add(publicFingerprint);
if (
!Array.isArray(root.revokedEpochs) ||
root.revokedEpochs.some(
(epoch) => !Number.isSafeInteger(epoch) || epoch < 1,
) ||
new Set(root.revokedEpochs).size !== root.revokedEpochs.length
)
throw new Error(`${role} revocation list is invalid.`);
if (root.revokedEpochs.includes(root.issuerEpoch))
throw new Error(`${role} current issuer epoch is revoked.`);
if (
!Array.isArray(root.compromiseResponse) ||
contract.recovery.some(
(action) => !root.compromiseResponse.includes(action),
)
) {
const detail =
role === "approval"
? "checkpoint audit and fresh approvals"
: role === "publisher"
? "fence advance and staging reconciliation"
: "fresh evidence";
throw new Error(`${role} compromise response lacks ${detail}.`);
}
}
return policy;
}
function verifyAttestation(value, root, expectedSchema, role, now) {
const envelope = object(value, "attestation");
exact(envelope, ["payload", "signature"], "attestation");
const payload = object(envelope.payload, "attestation payload");
exact(payload, attestationPayloadFields[role], "attestation payload");
const signature = object(envelope.signature, "attestation signature");
exact(signature, ["algorithm", "encoding", "value"], "attestation signature");
if (
payload.schema !== expectedSchema ||
payload.keyId !== root.keyId ||
payload.issuerEpoch !== root.issuerEpoch ||
payload.scope !== root.scope
)
throw new Error(
"Attestation schema, scope, key, or issuer epoch is not trusted.",
);
if (
typeof payload.issuedAt !== "string" ||
typeof payload.expiresAt !== "string" ||
!timestamp.test(payload.issuedAt) ||
!timestamp.test(payload.expiresAt) ||
Date.parse(payload.issuedAt) > Date.parse(now) ||
Date.parse(now) >= Date.parse(payload.expiresAt)
)
throw new Error("Attestation is not current at trusted time.");
for (const field of [
"digest",
"productionInputRoot",
"approvedRecordRoot",
"approvedAssetRoot",
"approvalProofSetDigest",
"latestLedgerHeadHash",
"candidateDigest",
"evidenceDigest",
])
if (field in payload) digest(payload[field], `attestation ${field}`);
if (
"latestLedgerRevision" in payload &&
(!Number.isSafeInteger(payload.latestLedgerRevision) ||
payload.latestLedgerRevision < 0)
)
throw new Error("Attestation ledger revision is invalid.");
if (
"imageDigests" in payload &&
(!Array.isArray(payload.imageDigests) ||
payload.imageDigests.length < 2 ||
payload.imageDigests.some((item) => !isSha256Hex(item)))
)
throw new Error("Attested image digests are invalid.");
if (
signature.algorithm !== "Ed25519" ||
signature.encoding !== "base64url-nopad" ||
typeof signature.value !== "string" ||
!/^[A-Za-z0-9_-]{86}$/u.test(signature.value)
)
throw new Error("Attestation signature metadata is invalid.");
const bytes = Buffer.from(signature.value, "base64url");
if (
bytes.toString("base64url") !== signature.value ||
!verify(
null,
canonicalValueBytes(payload),
createPublicKey(root.publicKeyPem),
bytes,
)
)
throw new Error("Attestation signature is invalid.");
return payload;
}
function validateCandidate(value, now) {
const candidate = object(value, "candidate");
exact(candidate, candidateFields, "candidate");
if (candidate.schema !== "portfolio-candidate-release-manifest/v1")
throw new Error("Candidate schema is invalid.");
digest(candidate.latestLedgerHeadHash, "latestLedgerHeadHash");
digest(candidate.sourceClosureDigest, "sourceClosureDigest");
digest(candidate.buildProvenanceDigest, "buildProvenanceDigest");
digest(candidate.privacyPolicyDigest, "privacyPolicyDigest");
if (
!Number.isSafeInteger(candidate.latestLedgerRevision) ||
candidate.latestLedgerRevision < 0
)
throw new Error("latestLedgerRevision is invalid.");
const refs = object(candidate.publicRefMap, "publicRefMap");
for (const [name, hash] of Object.entries(refs))
if (!safeToken.test(name) || !/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(hash))
throw new Error("Public ref map is invalid.");
if (
!Array.isArray(candidate.lfsOids) ||
candidate.lfsOids.some((oid) => !/^sha256:[a-f0-9]{64}$/u.test(oid)) ||
JSON.stringify(candidate.lfsOids) !==
JSON.stringify([...candidate.lfsOids].sort(compare)) ||
new Set(candidate.lfsOids).size !== candidate.lfsOids.length
)
throw new Error("LFS closure is invalid.");
if (!Array.isArray(candidate.releaseArchives))
throw new Error("Release archive closure is invalid.");
for (const archive of candidate.releaseArchives) {
const record = object(archive, "release archive");
exact(record, ["path", "sha256"], "release archive");
if (
typeof record.path !== "string" ||
record.path.startsWith("/") ||
record.path.includes("..")
)
throw new Error("Release archive path is invalid.");
digest(record.sha256, "release archive digest");
}
const archiveKeys = candidate.releaseArchives.map(
(archive) => `${archive.path}\0${archive.sha256}`,
);
if (
new Set(archiveKeys).size !== archiveKeys.length ||
JSON.stringify(archiveKeys) !==
JSON.stringify([...archiveKeys].sort(compare))
)
throw new Error("Release archive closure must be unique and byte sorted.");
if (
!Array.isArray(candidate.toolImageDigests) ||
candidate.toolImageDigests.length < 2 ||
candidate.toolImageDigests.some((value) => !isSha256Hex(value)) ||
new Set(candidate.toolImageDigests).size !==
candidate.toolImageDigests.length ||
JSON.stringify(candidate.toolImageDigests) !==
JSON.stringify([...candidate.toolImageDigests].sort(compare))
)
throw new Error("Tool image closure must contain unique, sorted digests.");
const releaseManifest = validateReleaseManifest(candidate.releaseManifest, {
now,
});
return { candidate, releaseManifest };
}
function validateEvidence(value, expectedDigest) {
const evidence = object(value, "scan evidence");
exact(
evidence,
["schema", "coverageCount", "passedCount", "checks"],
"scan evidence",
);
if (
evidence.schema !== "portfolio-scan-evidence/v1" ||
!Number.isSafeInteger(evidence.coverageCount) ||
evidence.coverageCount < 1 ||
evidence.passedCount !== evidence.coverageCount ||
!Array.isArray(evidence.checks) ||
evidence.checks.length !== evidence.coverageCount
)
throw new Error("Scan evidence coverage is incomplete.");
const keys = [];
for (const raw of evidence.checks) {
const check = object(raw, "scan evidence check");
exact(
check,
[
"inputDigest",
"checkId",
"normalizedOutputDigest",
"normalizedMembers",
"workerReports",
"matcherResult",
"ruleSetDigest",
"appliedRuleIds",
],
"scan evidence check",
);
digest(check.inputDigest, "scan input digest");
digest(check.normalizedOutputDigest, "normalized output digest");
digest(check.matcherResult, "matcher result digest");
digest(check.ruleSetDigest, "rule set digest");
if (
!safeToken.test(check.checkId) ||
!Array.isArray(check.normalizedMembers) ||
check.normalizedMembers.some(
(item) => !/^[a-z][a-z0-9-]*:[a-f0-9]{64}$/u.test(item),
) ||
!Array.isArray(check.workerReports) ||
check.workerReports.length !== 2 ||
check.workerReports.some((item) => !isSha256Hex(item))
)
throw new Error("Scan evidence check is invalid.");
if (
!Array.isArray(check.appliedRuleIds) ||
check.appliedRuleIds.length === 0 ||
check.appliedRuleIds.some((item) => !safeToken.test(item)) ||
new Set(check.appliedRuleIds).size !== check.appliedRuleIds.length
)
throw new Error("Applied matcher rule IDs are invalid.");
keys.push(`${check.inputDigest}\0${check.checkId}`);
}
if (
new Set(keys).size !== keys.length ||
JSON.stringify(keys) !== JSON.stringify([...keys].sort(compare))
)
throw new Error("Scan evidence checks must be unique and byte sorted.");
if (
canonicalJsonDigest("portfolio-scan-evidence/v1", evidence) !==
expectedDigest
)
throw new Error("Scan evidence bytes were substituted.");
return evidence;
}
function validateProtectedState(state) {
const value = object(state, "protected state");
exact(
value,
[
"latestLedgerHeadHash",
"latestLedgerRevision",
"currentManifestDigest",
"publisherFence",
],
"protected state",
);
digest(value.latestLedgerHeadHash, "protected ledger head");
if (
!Number.isSafeInteger(value.latestLedgerRevision) ||
value.latestLedgerRevision < 0 ||
!Number.isSafeInteger(value.publisherFence) ||
value.publisherFence < 0 ||
(value.currentManifestDigest !== null &&
!isSha256Hex(value.currentManifestDigest))
)
throw new Error("Protected state is invalid.");
return value;
}
function verifyCandidateAuthority({
candidate,
evidence,
attestations,
trustPolicy,
now,
protectedState,
}) {
if (trustPolicy.publicationFrozen)
throw new Error("Publication is frozen by trust policy.");
if (typeof now !== "string" || !timestamp.test(now))
throw new Error("Trusted current time is invalid.");
const { candidate: record, releaseManifest } = validateCandidate(
candidate,
now,
);
const state = validateProtectedState(protectedState);
if (
record.latestLedgerHeadHash !== state.latestLedgerHeadHash ||
record.latestLedgerRevision !== state.latestLedgerRevision
)
throw new Error("Candidate is stale relative to the protected ledger.");
if (
releaseManifest.currentManifestDigest !== state.currentManifestDigest ||
releaseManifest.predecessorManifestDigest !== state.currentManifestDigest
)
throw new Error(
"Candidate predecessor does not equal protected current manifest.",
);
validateEvidence(evidence, releaseManifest.scanEvidenceDigest);
const candidateDigest = canonicalJsonDigest(
"portfolio-candidate-release-manifest/v1",
record,
);
const evidenceDigest = canonicalJsonDigest(
"portfolio-scan-evidence/v1",
evidence,
);
const projection = verifyAttestation(
attestations.projection,
trustPolicy.roots.projection,
"portfolio-approval-projection-attestation/v1",
"projection",
now,
);
const workload = verifyAttestation(
attestations.privacyWorkload,
trustPolicy.roots.privacyWorkload,
"portfolio-privacy-workload-attestation/v1",
"privacyWorkload",
now,
);
const policy = verifyAttestation(
attestations.policy,
trustPolicy.roots.policy,
"portfolio-policy-attestation/v1",
"policy",
now,
);
const tools = verifyAttestation(
attestations.toolImage,
trustPolicy.roots.toolImage,
"portfolio-tool-image-provenance/v1",
"toolImage",
now,
);
for (const [field, expected] of [
["productionInputRoot", releaseManifest.productionInputRoot],
["approvedRecordRoot", releaseManifest.approvedRecordRoot],
["approvedAssetRoot", releaseManifest.approvedAssetRoot],
["approvalProofSetDigest", releaseManifest.approvalProofSetDigest],
["latestLedgerHeadHash", record.latestLedgerHeadHash],
["latestLedgerRevision", record.latestLedgerRevision],
]) {
if (projection[field] !== expected)
throw new Error(`Approval projection mismatch: ${field}.`);
}
if (projection.digest !== releaseManifest.approvalProjectionAttestationDigest)
throw new Error("Projection attestation digest binding is invalid.");
if (
workload.candidateDigest !== candidateDigest ||
workload.evidenceDigest !== evidenceDigest
)
throw new Error(
"Privacy workload does not bind exact candidate and evidence bytes.",
);
if (
policy.digest !== record.privacyPolicyDigest ||
policy.policyVersion !== trustPolicy.policyVersion
)
throw new Error("Privacy policy attestation is stale or substituted.");
if (
!Array.isArray(tools.imageDigests) ||
JSON.stringify(tools.imageDigests) !==
JSON.stringify(record.toolImageDigests)
)
throw new Error("Tool-image provenance does not bind exact images.");
return { record, releaseManifest, state, candidateDigest, evidenceDigest };
}
async function externalPrivateKey(path) {
if (typeof path !== "string" || !isAbsolute(path))
throw new Error("Release key path must be an external absolute path.");
const workspace = await realpath(process.cwd());
const lexicalRelative = relative(workspace, resolve(path));
if (
lexicalRelative === "" ||
(!lexicalRelative.startsWith("..") && !isAbsolute(lexicalRelative))
)
throw new Error(
"Release signing key must live outside the repository/workspace.",
);
const canonicalPath = await realpath(path);
const rel = relative(workspace, canonicalPath);
if (rel === "" || (!rel.startsWith("..") && !isAbsolute(rel)))
throw new Error(
"Release signing key must live outside the repository/workspace.",
);
const info = await fileStat(canonicalPath);
if (!info.isFile() || (info.mode & 0o077) !== 0)
throw new Error("External release key must be a private owner-only file.");
const key = createPrivateKey(await readFile(canonicalPath));
if (key.asymmetricKeyType !== "ed25519")
throw new Error("Release key must be Ed25519.");
return key;
}
export async function signReleaseCandidate(input) {
const trustPolicy = validateTrustPolicy(input.trustPolicy);
const verified = verifyCandidateAuthority({ ...input, trustPolicy });
const key = await externalPrivateKey(input.releaseKeyPath);
const releaseRoot = trustPolicy.roots.release;
const bytes = canonicalValueBytes(verified.record);
const signatureBytes = sign(null, bytes, key);
if (
!verify(
null,
bytes,
createPublicKey(releaseRoot.publicKeyPem),
signatureBytes,
)
)
throw new Error(
"External key does not match the active release trust root.",
);
return {
schema: "portfolio-signed-release-manifest/v1",
payload: verified.record,
payloadDigest: verified.candidateDigest,
signature: {
algorithm: "Ed25519",
encoding: "base64url-nopad",
keyId: releaseRoot.keyId,
issuerEpoch: releaseRoot.issuerEpoch,
scope: releaseRoot.scope,
value: signatureBytes.toString("base64url"),
},
};
}
export function verifySignedReleaseEnvelope({
signedRelease,
trustPolicy: trustValue,
now,
protectedState,
}) {
const trustPolicy = validateTrustPolicy(trustValue);
const envelope = object(signedRelease, "signed release");
exact(envelope, signedFields, "signed release");
if (envelope.schema !== "portfolio-signed-release-manifest/v1")
throw new Error("Signed release schema is invalid.");
const signature = object(envelope.signature, "release signature");
exact(signature, signatureFields, "release signature");
const root = trustPolicy.roots.release;
if (
signature.algorithm !== "Ed25519" ||
signature.encoding !== "base64url-nopad" ||
signature.keyId !== root.keyId ||
signature.issuerEpoch !== root.issuerEpoch ||
signature.scope !== root.scope ||
typeof signature.value !== "string"
)
throw new Error(
"Release signature key, epoch, scope, or encoding is invalid.",
);
const { candidate, releaseManifest } = validateCandidate(
envelope.payload,
now,
);
const payloadDigest = canonicalJsonDigest(
"portfolio-candidate-release-manifest/v1",
candidate,
);
if (envelope.payloadDigest !== payloadDigest)
throw new Error("Signed release payload digest is invalid.");
const bytes = Buffer.from(signature.value, "base64url");
if (
bytes.toString("base64url") !== signature.value ||
!verify(
null,
canonicalValueBytes(candidate),
createPublicKey(root.publicKeyPem),
bytes,
)
)
throw new Error("Release signature is invalid.");
const state = validateProtectedState(protectedState);
if (
candidate.latestLedgerHeadHash !== state.latestLedgerHeadHash ||
candidate.latestLedgerRevision !== state.latestLedgerRevision ||
releaseManifest.currentManifestDigest !== state.currentManifestDigest ||
releaseManifest.predecessorManifestDigest !== state.currentManifestDigest
)
throw new Error("Signed release is stale relative to protected state.");
return {
schema: "portfolio-verified-release-admission/v1",
manifestDigest: payloadDigest,
manifestPayload: releaseManifest,
expiresAt: releaseManifest.expiresAt,
predecessorManifestDigest: releaseManifest.predecessorManifestDigest,
latestLedgerHeadHash: candidate.latestLedgerHeadHash,
latestLedgerRevision: candidate.latestLedgerRevision,
expectedFence: state.publisherFence + 1,
signatureVerified: true,
};
}
async function main() {
const args = process.argv.slice(2);
if (args.length !== 4 || args[0] !== "--request" || args[2] !== "--key")
throw new Error(
"Usage: sign-release.mjs --request <canonical-request.json> --key <external-key.pem>",
);
const request = parseCanonicalJson(await readFile(resolve(args[1])));
const signed = await signReleaseCandidate({
...object(request, "sign request"),
releaseKeyPath: resolve(args[3]),
});
process.stdout.write(Buffer.from(canonicalValueBytes(signed)));
process.stdout.write("\n");
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
main().catch((error) => {
process.stderr.write(
`Release signing failed: ${error instanceof Error ? error.message : "unknown error"}\n`,
);
process.exitCode = 1;
});
}

View File

@@ -0,0 +1,71 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://portfolio.invalid/schemas/trust-policy-v1.json",
"title": "Portfolio release trust policy",
"type": "object",
"additionalProperties": false,
"required": ["schema", "policyVersion", "publicationFrozen", "roots"],
"properties": {
"schema": { "const": "portfolio-trust-policy/v1" },
"policyVersion": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$"
},
"publicationFrozen": { "type": "boolean" },
"roots": {
"type": "object",
"additionalProperties": false,
"required": [
"approval",
"projection",
"privacyWorkload",
"policy",
"toolImage",
"release",
"publisher"
],
"properties": {
"approval": { "$ref": "#/$defs/root" },
"projection": { "$ref": "#/$defs/root" },
"privacyWorkload": { "$ref": "#/$defs/root" },
"policy": { "$ref": "#/$defs/root" },
"toolImage": { "$ref": "#/$defs/root" },
"release": { "$ref": "#/$defs/root" },
"publisher": { "$ref": "#/$defs/root" }
}
}
},
"$defs": {
"root": {
"type": "object",
"additionalProperties": false,
"required": [
"keyId",
"issuerEpoch",
"scope",
"publicKeyPem",
"revokedEpochs",
"compromiseResponse"
],
"properties": {
"keyId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$"
},
"issuerEpoch": { "type": "integer", "minimum": 1 },
"scope": { "type": "string" },
"publicKeyPem": { "type": "string" },
"revokedEpochs": {
"type": "array",
"uniqueItems": true,
"items": { "type": "integer", "minimum": 1 }
},
"compromiseResponse": {
"type": "array",
"uniqueItems": true,
"items": { "type": "string" }
}
}
}
}
}