feat: isolate privacy scanning and release signing
This commit is contained in:
240
tests/security/privacy-workers.test.ts
Normal file
240
tests/security/privacy-workers.test.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import {
|
||||
chmod,
|
||||
mkdtemp,
|
||||
mkdir,
|
||||
readFile,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
// @ts-expect-error executable release modules intentionally have no browser declarations
|
||||
import { enumerateClosure } from "../../scripts/release/enumerate-closure.mjs";
|
||||
// @ts-expect-error executable release modules intentionally have no browser declarations
|
||||
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";
|
||||
|
||||
const { buildWorkerCommand, validateScanObjectDescriptor } = privacyRunner;
|
||||
|
||||
const h = (c: string) => c.repeat(64);
|
||||
|
||||
function report(
|
||||
workerId: "worker-a" | "worker-b",
|
||||
override: Record<string, unknown> = {},
|
||||
) {
|
||||
return {
|
||||
schema: "portfolio-privacy-worker-report/v1",
|
||||
workerId,
|
||||
imageDigest: workerId === "worker-a" ? h("a") : h("b"),
|
||||
toolchain: workerId === "worker-a" ? "bytes-js/v1" : "bytes-python/v1",
|
||||
inputDigest: h("1"),
|
||||
checkId: "bytes",
|
||||
normalizedOutputDigest: h("2"),
|
||||
normalizedMembers: ["text:" + h("3")],
|
||||
verdict: "pass",
|
||||
findingsDigest: h("0"),
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
function matcher(override: Record<string, unknown> = {}) {
|
||||
return {
|
||||
schema: "portfolio-privacy-matcher-result/v1",
|
||||
inputDigest: h("1"),
|
||||
checkId: "bytes",
|
||||
normalizedOutputDigest: h("2"),
|
||||
normalizedMembers: ["text:" + h("3")],
|
||||
ruleSetDigest: h("4"),
|
||||
appliedRuleIds: ["denylist-v1"],
|
||||
verdict: "pass",
|
||||
workerImageDigests: [h("a"), h("b")],
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
||||
describe("isolated privacy closure and reconciliation", () => {
|
||||
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"));
|
||||
await writeFile(join(root, "z.txt"), "z");
|
||||
await writeFile(join(root, "nested", "a.txt"), "a");
|
||||
const closure = await enumerateClosure([{ class: "build", root }]);
|
||||
expect(
|
||||
closure.members.map((member: { path: string }) => member.path),
|
||||
).toEqual(["nested/a.txt", "z.txt"]);
|
||||
expect(
|
||||
closure.members.every((member: { sha256: string }) =>
|
||||
/^[a-f0-9]{64}$/u.test(member.sha256),
|
||||
),
|
||||
).toBe(true);
|
||||
await symlink("z.txt", join(root, "link"));
|
||||
await expect(enumerateClosure([{ class: "build", root }])).rejects.toThrow(
|
||||
/symlink/i,
|
||||
);
|
||||
});
|
||||
|
||||
test("constructs networkless read-only non-root resource-limited jobs", () => {
|
||||
const command = buildWorkerCommand({
|
||||
runtime: "docker",
|
||||
image: "scanner@sha256:" + h("a"),
|
||||
inputPath: "/safe/input",
|
||||
outputPath: "/safe/output",
|
||||
worker: "worker-a",
|
||||
});
|
||||
expect(command).toContain("--network=none");
|
||||
expect(command).toContain("--read-only");
|
||||
expect(command).toContain("--cap-drop=ALL");
|
||||
expect(command).toContain("--security-opt=no-new-privileges");
|
||||
expect(command).toContain("--user=65532:65532");
|
||||
expect(command).toContain("--memory=256m");
|
||||
expect(command).toContain("--cpus=1");
|
||||
expect(command).toContain("--pids-limit=64");
|
||||
expect(command).toContain("--pull=never");
|
||||
expect(command).toContain(
|
||||
"--mount=type=bind,src=/safe/input,dst=/input,readonly",
|
||||
);
|
||||
expect(command).toContain("--mount=type=bind,src=/safe/output,dst=/output");
|
||||
});
|
||||
|
||||
test.each([
|
||||
["encrypted archive", { encrypted: true }],
|
||||
["malformed container", { malformed: true }],
|
||||
["nested archive", { archiveDepth: 4 }],
|
||||
["archive expansion", { expandedBytes: 67_108_865 }],
|
||||
["traversal", { members: [{ path: "../secret", entryType: "file" }] }],
|
||||
["absolute member", { members: [{ path: "/secret", entryType: "file" }] }],
|
||||
["symlink member", { members: [{ path: "safe", entryType: "symlink" }] }],
|
||||
["device member", { members: [{ path: "safe", entryType: "device" }] }],
|
||||
])("fails closed before decoding an unsafe %s", (_name, override) => {
|
||||
expect(() =>
|
||||
validateScanObjectDescriptor({
|
||||
inputDigest: h("1"),
|
||||
checkId: "container",
|
||||
mediaType: "application/zip",
|
||||
encrypted: false,
|
||||
malformed: false,
|
||||
archiveDepth: 1,
|
||||
expandedBytes: 10,
|
||||
members: [],
|
||||
...override,
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test("does not expose OCR or QR canary values when a matcher blocks them", () => {
|
||||
const secret = "PRIVATE-CANARY-DO-NOT-PRINT";
|
||||
expect(() =>
|
||||
reconcileScan({
|
||||
expectedChecks: [{ inputDigest: h("1"), checkId: "ocr" }],
|
||||
reports: [
|
||||
report("worker-a", { checkId: "ocr", verdict: "fail" }),
|
||||
report("worker-b", { checkId: "ocr", verdict: "fail" }),
|
||||
],
|
||||
matcherResults: [matcher({ checkId: "ocr", verdict: "fail" })],
|
||||
candidate: {
|
||||
schema: "portfolio-candidate-release-manifest/v1",
|
||||
buildId: "r1",
|
||||
},
|
||||
}),
|
||||
).toThrowError(expect.not.stringContaining(secret));
|
||||
});
|
||||
|
||||
test("accepts only ordered heterogeneous agreement for every expected check", () => {
|
||||
const result = reconcileScan({
|
||||
expectedChecks: [{ inputDigest: h("1"), checkId: "bytes" }],
|
||||
reports: [report("worker-a"), report("worker-b")],
|
||||
matcherResults: [matcher()],
|
||||
candidate: {
|
||||
schema: "portfolio-candidate-release-manifest/v1",
|
||||
buildId: "r1",
|
||||
},
|
||||
});
|
||||
expect(result.evidenceDigest).toMatch(/^[a-f0-9]{64}$/);
|
||||
expect(result.candidateDigest).toMatch(/^[a-f0-9]{64}$/);
|
||||
expect(new TextDecoder().decode(result.evidenceBytes)).not.toContain(
|
||||
"decoded-secret",
|
||||
);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["single worker", [report("worker-a")], [matcher()]],
|
||||
[
|
||||
"reordered workers",
|
||||
[report("worker-b"), report("worker-a")],
|
||||
[matcher()],
|
||||
],
|
||||
[
|
||||
"schema-valid false pass",
|
||||
[report("worker-a"), report("worker-b", { verdict: "fail" })],
|
||||
[matcher()],
|
||||
],
|
||||
[
|
||||
"normalized disagreement",
|
||||
[
|
||||
report("worker-a"),
|
||||
report("worker-b", { normalizedOutputDigest: h("9") }),
|
||||
],
|
||||
[matcher()],
|
||||
],
|
||||
[
|
||||
"substituted worker image",
|
||||
[report("worker-a"), report("worker-b", { imageDigest: h("a") })],
|
||||
[matcher()],
|
||||
],
|
||||
[
|
||||
"mismatched check",
|
||||
[report("worker-a"), report("worker-b", { checkId: "metadata" })],
|
||||
[matcher()],
|
||||
],
|
||||
[
|
||||
"matcher false pass",
|
||||
[report("worker-a"), report("worker-b")],
|
||||
[matcher({ normalizedOutputDigest: h("9") })],
|
||||
],
|
||||
[
|
||||
"unsupported container",
|
||||
[
|
||||
report("worker-a", { verdict: "unsupported" }),
|
||||
report("worker-b", { verdict: "unsupported" }),
|
||||
],
|
||||
[matcher({ verdict: "fail" })],
|
||||
],
|
||||
])("fails closed for %s", (_name, reports, matcherResults) => {
|
||||
expect(() =>
|
||||
reconcileScan({
|
||||
expectedChecks: [{ inputDigest: h("1"), checkId: "bytes" }],
|
||||
reports,
|
||||
matcherResults,
|
||||
candidate: {
|
||||
schema: "portfolio-candidate-release-manifest/v1",
|
||||
buildId: "r1",
|
||||
},
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test("writes canonical evidence and candidate objects content-addressed without overwrite", async () => {
|
||||
const result = reconcileScan({
|
||||
expectedChecks: [{ inputDigest: h("1"), checkId: "bytes" }],
|
||||
reports: [report("worker-a"), report("worker-b")],
|
||||
matcherResults: [matcher()],
|
||||
candidate: {
|
||||
schema: "portfolio-candidate-release-manifest/v1",
|
||||
buildId: "r1",
|
||||
},
|
||||
});
|
||||
const root = await mkdtemp(join(tmpdir(), "portfolio-evidence-"));
|
||||
await result.writeContentAddressed(root);
|
||||
expect(await readFile(join(root, result.evidenceDigest), "utf8")).toBe(
|
||||
new TextDecoder().decode(result.evidenceBytes),
|
||||
);
|
||||
await chmod(join(root, result.candidateDigest), 0o600);
|
||||
await writeFile(join(root, result.candidateDigest), "substituted");
|
||||
await expect(result.writeContentAddressed(root)).rejects.toThrow(
|
||||
/immutable|content-addressed/i,
|
||||
);
|
||||
});
|
||||
});
|
||||
441
tests/security/release-signing.test.ts
Normal file
441
tests/security/release-signing.test.ts
Normal file
@@ -0,0 +1,441 @@
|
||||
import { generateKeyPairSync, sign, verify } from "node:crypto";
|
||||
import { mkdtemp, 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 { computeProductionInputRoot } from "../../src/security/production-input-root.ts";
|
||||
// @ts-expect-error executable release modules intentionally have no browser declarations
|
||||
import * as releaseSigner from "../../scripts/release/sign-release.mjs";
|
||||
|
||||
const {
|
||||
signReleaseCandidate,
|
||||
validateTrustPolicy,
|
||||
verifySignedReleaseEnvelope,
|
||||
} = releaseSigner;
|
||||
|
||||
const h = (c: string) => c.repeat(64);
|
||||
const roles = [
|
||||
["approval", "portfolio-approval-decision/v1"],
|
||||
["projection", "portfolio-approval-projection/v1"],
|
||||
["privacyWorkload", "portfolio-privacy-workload/v1"],
|
||||
["policy", "portfolio-policy-attestation/v1"],
|
||||
["toolImage", "portfolio-tool-image-provenance/v1"],
|
||||
["release", "portfolio-release-signing/v1"],
|
||||
["publisher", "portfolio-publisher-fencing/v1"],
|
||||
] as const;
|
||||
|
||||
function keys(): Record<string, { publicKey: any; privateKey: any }> {
|
||||
return Object.fromEntries(
|
||||
roles.map(([role]) => [role, generateKeyPairSync("ed25519")]),
|
||||
) as Record<string, { publicKey: any; privateKey: any }>;
|
||||
}
|
||||
|
||||
function policy(keyPairs: ReturnType<typeof keys>): any {
|
||||
return {
|
||||
schema: "portfolio-trust-policy/v1",
|
||||
policyVersion: "2026-09-02",
|
||||
publicationFrozen: false,
|
||||
roots: Object.fromEntries(
|
||||
roles.map(([role, scope]) => [
|
||||
role,
|
||||
{
|
||||
keyId: `${role}-key`,
|
||||
issuerEpoch: 2,
|
||||
scope,
|
||||
publicKeyPem: keyPairs[role]!.publicKey.export({
|
||||
type: "spki",
|
||||
format: "pem",
|
||||
}),
|
||||
revokedEpochs: [1],
|
||||
compromiseResponse:
|
||||
role === "approval"
|
||||
? ["freeze-publication", "audit-checkpoint", "fresh-approvals"]
|
||||
: role === "publisher"
|
||||
? [
|
||||
"freeze-publication",
|
||||
"rotate-credential",
|
||||
"advance-fence",
|
||||
"reconcile-staging",
|
||||
]
|
||||
: ["freeze-publication", "revoke-epoch", "fresh-evidence"],
|
||||
},
|
||||
]),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function envelope(
|
||||
role: string,
|
||||
scope: string,
|
||||
keyPairs: ReturnType<typeof keys>,
|
||||
payload: Record<string, unknown> = {},
|
||||
) {
|
||||
const schema = {
|
||||
projection: "portfolio-approval-projection-attestation/v1",
|
||||
privacyWorkload: "portfolio-privacy-workload-attestation/v1",
|
||||
policy: "portfolio-policy-attestation/v1",
|
||||
toolImage: "portfolio-tool-image-provenance/v1",
|
||||
}[role];
|
||||
const body = {
|
||||
schema,
|
||||
keyId: `${role}-key`,
|
||||
issuerEpoch: 2,
|
||||
scope,
|
||||
issuedAt: "2026-09-02T00:00:00Z",
|
||||
expiresAt: "2026-09-03T00:00:00Z",
|
||||
...payload,
|
||||
};
|
||||
return {
|
||||
payload: body,
|
||||
signature: {
|
||||
algorithm: "Ed25519",
|
||||
encoding: "base64url-nopad",
|
||||
value: sign(
|
||||
null,
|
||||
canonicalValueBytes(body),
|
||||
keyPairs[role]!.privateKey,
|
||||
).toString("base64url"),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function fixture() {
|
||||
const keyPairs = keys();
|
||||
const inputDigests = {
|
||||
PortfolioContent: h("a"),
|
||||
PublicAssetManifest: h("b"),
|
||||
PreparedAssetSet: h("c"),
|
||||
SiteConfig: h("d"),
|
||||
};
|
||||
const releaseManifest = {
|
||||
schema: "portfolio-release-manifest/v1",
|
||||
launchMode: "private-source-initial-launch",
|
||||
buildId: "r1",
|
||||
inputDigests,
|
||||
productionInputRoot: computeProductionInputRoot({
|
||||
portfolioContentDigest: h("a"),
|
||||
publicAssetManifestDigest: h("b"),
|
||||
preparedAssetSetDigest: h("c"),
|
||||
siteConfigDigest: h("d"),
|
||||
}),
|
||||
artifacts: [
|
||||
{ path: "index.html", sha256: h("6"), bytes: 42, encoding: "identity" },
|
||||
],
|
||||
deployFiles: ["index.html"],
|
||||
sourceCommit: h("7"),
|
||||
sourceTree: h("8"),
|
||||
approvedRecordRoot: h("9"),
|
||||
approvedAssetRoot: h("e"),
|
||||
approvalProofSetDigest: h("f"),
|
||||
approvalProjectionAttestationDigest: h("2"),
|
||||
scanEvidenceDigest: h("3"),
|
||||
policyVersions: { approval: "1", privacy: "1", release: "1" },
|
||||
toolVersions: { node: "22" },
|
||||
predecessorManifestDigest: null,
|
||||
currentManifestDigest: null,
|
||||
issuedAt: "2026-09-02T00:00:00Z",
|
||||
expiresAt: "2026-09-02T12:00:00Z",
|
||||
minimumBoundExpiry: "2026-09-02T12:00:00Z",
|
||||
};
|
||||
const candidate = {
|
||||
schema: "portfolio-candidate-release-manifest/v1",
|
||||
releaseManifest,
|
||||
latestLedgerHeadHash: h("1"),
|
||||
latestLedgerRevision: 7,
|
||||
sourceClosureDigest: h("4"),
|
||||
publicRefMap: {},
|
||||
lfsOids: [],
|
||||
releaseArchives: [],
|
||||
buildProvenanceDigest: h("5"),
|
||||
privacyPolicyDigest: h("a"),
|
||||
toolImageDigests: [h("b"), h("c")],
|
||||
};
|
||||
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"],
|
||||
},
|
||||
],
|
||||
};
|
||||
const evidenceDigest = canonicalJsonDigest(
|
||||
"portfolio-scan-evidence/v1",
|
||||
evidence,
|
||||
);
|
||||
candidate.releaseManifest.scanEvidenceDigest = evidenceDigest;
|
||||
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,
|
||||
}),
|
||||
privacyWorkload: envelope("privacyWorkload", roles[2][1], keyPairs, {
|
||||
digest: h("2"),
|
||||
candidateDigest,
|
||||
evidenceDigest,
|
||||
}),
|
||||
policy: envelope("policy", roles[3][1], keyPairs, {
|
||||
digest: h("a"),
|
||||
policyVersion: "2026-09-02",
|
||||
}),
|
||||
toolImage: envelope("toolImage", roles[4][1], keyPairs, {
|
||||
digest: h("b"),
|
||||
imageDigests: [h("b"), h("c")],
|
||||
}),
|
||||
};
|
||||
return {
|
||||
keyPairs,
|
||||
candidate,
|
||||
evidence,
|
||||
candidateDigest,
|
||||
evidenceDigest,
|
||||
attestations,
|
||||
trustPolicy: policy(keyPairs),
|
||||
};
|
||||
}
|
||||
|
||||
describe("external scoped release signing", () => {
|
||||
test("signs exact canonical candidate bytes with an external Ed25519 key", 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 },
|
||||
);
|
||||
const result = await signReleaseCandidate({
|
||||
...f,
|
||||
releaseKeyPath: keyPath,
|
||||
now: "2026-09-02T01:00:00Z",
|
||||
protectedState: {
|
||||
latestLedgerHeadHash: h("1"),
|
||||
latestLedgerRevision: 7,
|
||||
currentManifestDigest: null,
|
||||
publisherFence: 4,
|
||||
},
|
||||
});
|
||||
expect(result.payloadDigest).toBe(f.candidateDigest);
|
||||
expect(
|
||||
verify(
|
||||
null,
|
||||
canonicalValueBytes(f.candidate),
|
||||
f.keyPairs.release!.publicKey,
|
||||
Buffer.from(result.signature.value, "base64url"),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
verifySignedReleaseEnvelope({
|
||||
signedRelease: result,
|
||||
trustPolicy: f.trustPolicy,
|
||||
now: "2026-09-02T01:00:00Z",
|
||||
protectedState: {
|
||||
latestLedgerHeadHash: h("1"),
|
||||
latestLedgerRevision: 7,
|
||||
currentManifestDigest: null,
|
||||
publisherFence: 4,
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
schema: "portfolio-verified-release-admission/v1",
|
||||
manifestDigest: f.candidateDigest,
|
||||
expiresAt: "2026-09-02T12:00:00Z",
|
||||
predecessorManifestDigest: null,
|
||||
latestLedgerHeadHash: h("1"),
|
||||
latestLedgerRevision: 7,
|
||||
expectedFence: 5,
|
||||
signatureVerified: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("verified admission rejects stale publisher state and substituted signed payloads", 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 },
|
||||
);
|
||||
const state = {
|
||||
latestLedgerHeadHash: h("1"),
|
||||
latestLedgerRevision: 7,
|
||||
currentManifestDigest: null,
|
||||
publisherFence: 4,
|
||||
};
|
||||
const signedRelease = await signReleaseCandidate({
|
||||
...f,
|
||||
releaseKeyPath: keyPath,
|
||||
now: "2026-09-02T01:00:00Z",
|
||||
protectedState: state,
|
||||
});
|
||||
expect(() =>
|
||||
verifySignedReleaseEnvelope({
|
||||
signedRelease,
|
||||
trustPolicy: f.trustPolicy,
|
||||
now: "2026-09-02T01:00:00Z",
|
||||
protectedState: { ...state, latestLedgerRevision: 8 },
|
||||
}),
|
||||
).toThrow(/stale/i);
|
||||
expect(() =>
|
||||
verifySignedReleaseEnvelope({
|
||||
signedRelease: {
|
||||
...signedRelease,
|
||||
payload: { ...signedRelease.payload, latestLedgerRevision: 8 },
|
||||
},
|
||||
trustPolicy: f.trustPolicy,
|
||||
now: "2026-09-02T01:00:00Z",
|
||||
protectedState: state,
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test("refuses repository-resident keys and non-external key material", async () => {
|
||||
const f = fixture();
|
||||
await expect(
|
||||
signReleaseCandidate({
|
||||
...f,
|
||||
releaseKeyPath: join(process.cwd(), "release.pem"),
|
||||
now: "2026-09-02T01:00:00Z",
|
||||
protectedState: {
|
||||
latestLedgerHeadHash: h("1"),
|
||||
latestLedgerRevision: 7,
|
||||
currentManifestDigest: null,
|
||||
publisherFence: 4,
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/outside|external/i);
|
||||
});
|
||||
|
||||
test.each(roles)(
|
||||
"requires current non-revoked %s issuer epoch and exact scope",
|
||||
(role) => {
|
||||
const f = fixture();
|
||||
const mutated = structuredClone(f.trustPolicy);
|
||||
mutated.roots[role].issuerEpoch = 1;
|
||||
expect(() => validateTrustPolicy(mutated)).toThrow(/revoked|epoch/i);
|
||||
const wrongScope = structuredClone(f.trustPolicy);
|
||||
wrongScope.roots[role].scope = "wrong/v1";
|
||||
expect(() => validateTrustPolicy(wrongScope)).toThrow(/scope/i);
|
||||
},
|
||||
);
|
||||
|
||||
test("requires explicit compromise recovery semantics for approval and publisher roots", () => {
|
||||
const f = fixture();
|
||||
const approval = structuredClone(f.trustPolicy);
|
||||
approval.roots.approval.compromiseResponse = ["freeze-publication"];
|
||||
expect(() => validateTrustPolicy(approval)).toThrow(
|
||||
/checkpoint|fresh approvals/i,
|
||||
);
|
||||
const publisher = structuredClone(f.trustPolicy);
|
||||
publisher.roots.publisher.compromiseResponse = [
|
||||
"freeze-publication",
|
||||
"rotate-credential",
|
||||
];
|
||||
expect(() => validateTrustPolicy(publisher)).toThrow(/fence|staging/i);
|
||||
});
|
||||
|
||||
test.each([
|
||||
[
|
||||
"stale ledger",
|
||||
(f: ReturnType<typeof fixture>) => ({
|
||||
protectedState: {
|
||||
latestLedgerHeadHash: h("9"),
|
||||
latestLedgerRevision: 7,
|
||||
currentManifestDigest: null,
|
||||
publisherFence: 4,
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
"expired candidate",
|
||||
(f: ReturnType<typeof fixture>) => ({ now: "2026-09-02T12:00:00Z" }),
|
||||
],
|
||||
[
|
||||
"substituted artifact",
|
||||
(f: ReturnType<typeof fixture>) => ({
|
||||
candidate: {
|
||||
...f.candidate,
|
||||
releaseManifest: {
|
||||
...f.candidate.releaseManifest,
|
||||
artifacts: [
|
||||
{ ...f.candidate.releaseManifest.artifacts[0], bytes: 43 },
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
"forged workload epoch",
|
||||
(f: ReturnType<typeof fixture>) => ({
|
||||
attestations: {
|
||||
...f.attestations,
|
||||
privacyWorkload: {
|
||||
...f.attestations.privacyWorkload,
|
||||
payload: {
|
||||
...f.attestations.privacyWorkload.payload,
|
||||
issuerEpoch: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
"projection mismatch",
|
||||
(f: ReturnType<typeof fixture>) => ({
|
||||
candidate: {
|
||||
...f.candidate,
|
||||
releaseManifest: {
|
||||
...f.candidate.releaseManifest,
|
||||
productionInputRoot: h("9"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
])("rejects %s before signing", async (_name, mutate) => {
|
||||
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: "2026-09-02T01:00:00Z",
|
||||
protectedState: {
|
||||
latestLedgerHeadHash: h("1"),
|
||||
latestLedgerRevision: 7,
|
||||
currentManifestDigest: null,
|
||||
publisherFence: 4,
|
||||
},
|
||||
...mutate(f),
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user