Files
Portfolio/tests/security/release-signing.test.ts

442 lines
13 KiB
TypeScript

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();
});
});