fix: close release provenance and privacy gaps
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user