feat: verify canonical portfolio release provenance

This commit is contained in:
2026-09-02 09:41:01 +03:00
parent 1cec658baf
commit 672ba80d9b
12 changed files with 3536 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
import { generateKeyPairSync, sign } from 'node:crypto';
import { describe, expect, test } from 'vitest';
import { canonicalJsonDigest, canonicalValueBytes } from '../../src/security/canonical-digest.ts';
import { computePreparedAssetSetDigest, computeProductionInputRoot } from '../../src/security/production-input-root.ts';
import { validateReleaseManifest } from '../../src/security/release-manifest.ts';
// @ts-expect-error The executable JavaScript verifier intentionally has no browser-facing declaration.
import { verifyApprovalRequest } from '../../scripts/release/verify-approvals.mjs';
const h = (c: string) => c.repeat(64);
describe('production input and private-source release manifest', () => {
test('hashes an exact manifest-bound PreparedAssetSet and rejects missing or extra members', () => {
const bytes = new TextEncoder().encode('image');
const member = { assetId: 'asset-one', publicPath: 'asset-one.webp', mediaType: 'image/webp', width: 10, height: 20, durationMs: null, bytes };
const manifest = [{ id: 'asset-one', fileName: 'asset-one.webp', sha256: '6105d6cc76af400325e94d588ce511be5bfdbb73b437dc51eca43917d7a43e3d', mediaType: 'image/webp', width: 10, height: 20, durationMs: null }];
expect(computePreparedAssetSetDigest([member], manifest)).toMatch(/^[a-f0-9]{64}$/);
expect(() => computePreparedAssetSetDigest([], manifest)).toThrow();
expect(() => computePreparedAssetSetDigest([member, { ...member, assetId: 'asset-two', publicPath: 'asset-two.webp' }], manifest)).toThrow();
});
test('matches the fixed four-label input-root vector', () => {
expect(computeProductionInputRoot({
portfolioContentDigest: h('a'), publicAssetManifestDigest: h('b'),
preparedAssetSetDigest: h('c'), siteConfigDigest: h('d'),
})).toBe('38707dd72ded582a346048ff66a1625cdaca91f6536dd27c935bc3b8af262ae8');
});
test('validates an exact initial-launch private-source manifest', () => {
const inputDigests = {
PortfolioContent: h('a'), PublicAssetManifest: h('b'), PreparedAssetSet: h('c'), SiteConfig: h('d'),
};
const manifest = {
schema: 'portfolio-release-manifest/v1', launchMode: 'private-source-initial-launch', buildId: 'release-2026-09-02',
inputDigests, productionInputRoot: computeProductionInputRoot({
portfolioContentDigest: h('a'), publicAssetManifestDigest: h('b'), preparedAssetSetDigest: h('c'), siteConfigDigest: h('d'),
}),
artifacts: [{ path: 'index.html', sha256: h('e'), bytes: 42, encoding: 'identity' }],
deployFiles: ['index.html'], sourceCommit: h('f'), sourceTree: h('0'),
approvedRecordRoot: h('1'), approvedAssetRoot: h('2'), approvalProofSetDigest: h('3'),
approvalProjectionAttestationDigest: h('4'), scanEvidenceDigest: h('5'),
policyVersions: { approval: '1', privacy: '1', release: '1' }, toolVersions: { node: '22.22.1' },
predecessorManifestDigest: null, currentManifestDigest: null,
issuedAt: '2026-09-02T00:00:00Z', expiresAt: '2026-09-03T00:00:00Z', minimumBoundExpiry: '2026-09-03T00:00:00Z',
};
expect(validateReleaseManifest(manifest, { now: '2026-09-02T01:00:00Z' })).toEqual(manifest);
expect(canonicalJsonDigest('portfolio-release-manifest/v1', manifest)).toMatch(/^[a-f0-9]{64}$/);
});
test.each([
['public Git target', { publicGitTarget: 'https://example.com/repo.git' }],
['extra deploy file', { deployFiles: ['index.html', 'extra.txt'] }],
['case-fold collision', { artifacts: [
{ path: 'A.js', sha256: h('e'), bytes: 1, encoding: 'identity' },
{ path: 'a.js', sha256: h('f'), bytes: 1, encoding: 'identity' },
], deployFiles: ['A.js', 'a.js'] }],
['expired manifest', { expiresAt: '2026-09-02T00:30:00Z' }],
])('rejects %s', (_label, override) => {
const inputDigests = { PortfolioContent: h('a'), PublicAssetManifest: h('b'), PreparedAssetSet: h('c'), SiteConfig: h('d') };
const baseline = {
schema: 'portfolio-release-manifest/v1', launchMode: 'private-source-initial-launch', buildId: 'release', inputDigests,
productionInputRoot: computeProductionInputRoot({ portfolioContentDigest: h('a'), publicAssetManifestDigest: h('b'), preparedAssetSetDigest: h('c'), siteConfigDigest: h('d') }),
artifacts: [{ path: 'index.html', sha256: h('e'), bytes: 42, encoding: 'identity' }], deployFiles: ['index.html'],
sourceCommit: h('f'), sourceTree: h('0'), approvedRecordRoot: h('1'), approvedAssetRoot: h('2'), approvalProofSetDigest: h('3'),
approvalProjectionAttestationDigest: h('4'), scanEvidenceDigest: h('5'), policyVersions: { approval: '1', privacy: '1', release: '1' },
toolVersions: { node: '22' }, predecessorManifestDigest: null, currentManifestDigest: null,
issuedAt: '2026-09-02T00:00:00Z', expiresAt: '2026-09-03T00:00:00Z', minimumBoundExpiry: '2026-09-03T00:00:00Z',
};
expect(() => validateReleaseManifest({ ...baseline, ...override }, { now: '2026-09-02T01:00:00Z' })).toThrow();
});
test('CLI verifier emits only canonical public roots from a signed external approval request', () => {
const keys = generateKeyPairSync('ed25519');
const approvalPayload = {
schema: 'portfolio-approval-payload/v1', keyId: 'approval-key', issuerEpoch: 1, authorizedRole: 'approver',
evidenceFingerprints: [h('1')], recordClass: 'content', recordType: 'profile', publicRecordIdOrAssetId: 'profile-main',
approvedPublicRecordHashOrManifestRecordHash: h('2'), sanitizedOutputHashOrNull: null, decision: 'approve',
issuedAt: '2026-09-01T00:00:00Z', expiresAt: '2026-09-04T00:00:00Z', previousLedgerHeadHash: h('8'),
ledgerRevision: 1, policyVersion: 'policy-1',
};
const envelope = {
schema: 'portfolio-approval-envelope/v1', payload: approvalPayload,
signature: { algorithm: 'Ed25519', encoding: 'base64url-nopad', value: sign(null, canonicalValueBytes(approvalPayload), keys.privateKey).toString('base64url') },
};
const request = {
schema: 'portfolio-approval-verification-request/v1', now: '2026-09-02T00:00:00Z',
latestLedgerHeadHash: h('9'), latestLedgerRevision: 2,
trustedKeys: [{ keyId: 'approval-key', issuerEpoch: 1, scope: 'portfolio-approval-decision/v1', authorizedRoles: ['approver'], publicKeyPem: keys.publicKey.export({ type: 'spki', format: 'pem' }) }],
contentRecords: [{ recordType: 'profile', publicRecordId: 'profile-main', approvedPublicRecordHash: h('2'), envelope }],
assetRecords: [],
inputDigests: { PortfolioContent: h('a'), PublicAssetManifest: h('b'), PreparedAssetSet: h('c'), SiteConfig: h('d') },
};
const result = verifyApprovalRequest(canonicalValueBytes(request));
expect(Object.keys(result).sort()).toEqual([
'approvalProofSetDigest', 'approvedAssetRoot', 'approvedRecordRoot', 'earliestApprovalExpiry',
'latestLedgerHeadHash', 'latestLedgerRevision', 'productionInputRoot', 'schema',
]);
const serialized = new TextDecoder().decode(canonicalValueBytes(result));
expect(serialized).not.toContain('evidenceFingerprints');
});
});