83 lines
3.3 KiB
TypeScript
83 lines
3.3 KiB
TypeScript
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';
|
|
|
|
const hash = (character: string) => character.repeat(64);
|
|
const payload = (): ApprovalPayload => ({
|
|
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'),
|
|
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',
|
|
});
|
|
|
|
function signedEnvelope(overrides: Partial<ApprovalPayload> = {}) {
|
|
const keys = generateKeyPairSync('ed25519');
|
|
const signedPayload = { ...payload(), ...overrides };
|
|
const signature = sign(null, canonicalValueBytes(signedPayload), keys.privateKey).toString('base64url');
|
|
return {
|
|
envelope: {
|
|
schema: 'portfolio-approval-envelope/v1' as const,
|
|
payload: signedPayload,
|
|
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', () => {
|
|
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');
|
|
});
|
|
|
|
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) => {
|
|
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();
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|