243 lines
7.4 KiB
TypeScript
243 lines
7.4 KiB
TypeScript
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";
|
|
|
|
const h = (c: string) => c.repeat(64);
|
|
const keys = generateKeyPairSync("ed25519");
|
|
function envelope(payload: ApprovalPayload) {
|
|
return {
|
|
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",
|
|
),
|
|
},
|
|
};
|
|
}
|
|
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,
|
|
ledgerRevision,
|
|
policyVersion: "p1",
|
|
};
|
|
}
|
|
|
|
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,
|
|
});
|
|
});
|
|
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", 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", () => {
|
|
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",
|
|
};
|
|
const projectionKeys = generateKeyPairSync("ed25519");
|
|
const attestation = {
|
|
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"),
|
|
},
|
|
};
|
|
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();
|
|
});
|
|
});
|