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