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,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');
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) }],
trust,
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,
});
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]],
}));
});
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: 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');
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("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,
}),
).toThrow();
expect(() =>
computeApprovalClosure({
contentRecords: [record],
assetRecords: [],
trust: { ...trust, latestLedgerRevision: 0 },
}),
).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({
...envelope,
signature: { ...envelope.signature, value: `${envelope.signature.value}=` },
}))).toThrow();
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();
});
});