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