52 lines
2.9 KiB
TypeScript
52 lines
2.9 KiB
TypeScript
import { mkdtemp, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { describe, expect, test } from 'vitest';
|
|
|
|
import { byteDigest, canonicalValueBytes } from '../../src/security/canonical-digest.ts';
|
|
import { buildReleasePackage } from '../../scripts/build-release.mjs';
|
|
import { parseDeterministicTar } from '../../scripts/deploy/lib/release-common.mjs';
|
|
import { createReleaseFixture } from './release-test-fixture.ts';
|
|
|
|
function buildInput(fixture: Awaited<ReturnType<typeof createReleaseFixture>>, outputPath: string) {
|
|
return {
|
|
distDir: fixture.distDir, signedManifestPath: fixture.signedManifestPath, sbomPath: fixture.sbomPath,
|
|
verifierModulePath: fixture.verifierModulePath, trustPolicyPath: fixture.trustPolicyPath,
|
|
verifySignedReleaseEnvelope: fixture.verifySignedReleaseEnvelope,
|
|
protectedState: fixture.protectedState, now: '2026-09-02T01:00:00Z', outputPath,
|
|
};
|
|
}
|
|
|
|
describe('deterministic exact-manifest release package', () => {
|
|
test('builds byte-identical archives containing only the signed closure and release metadata', async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'portfolio-package-'));
|
|
const fixture = await createReleaseFixture(root);
|
|
const first = join(root, 'release-one.tar');
|
|
const second = join(root, 'release-two.tar');
|
|
await buildReleasePackage(buildInput(fixture, first));
|
|
await buildReleasePackage(buildInput(fixture, second));
|
|
expect(byteDigest(await readFile(first))).toBe(byteDigest(await readFile(second)));
|
|
expect(parseDeterministicTar(await readFile(first)).map((file) => file.path)).toEqual([
|
|
'.release/package-index.json', '.release/sbom.cdx.json', '.release/signed-release-manifest.json',
|
|
'assets/app.js', 'index.html',
|
|
]);
|
|
});
|
|
|
|
test('fails closed on extra, substituted, or invalidly signed inputs', async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'portfolio-package-invalid-'));
|
|
const fixture = await createReleaseFixture(root);
|
|
await writeFile(join(fixture.distDir, 'extra.txt'), 'not in manifest');
|
|
await expect(buildReleasePackage(buildInput(fixture, join(root, 'extra.tar')))).rejects.toThrow('exactly match');
|
|
|
|
await unlink(join(fixture.distDir, 'extra.txt'));
|
|
await writeFile(join(fixture.distDir, 'index.html'), 'substituted');
|
|
await expect(buildReleasePackage(buildInput(fixture, join(root, 'substituted.tar')))).rejects.toThrow('hash');
|
|
|
|
await writeFile(join(fixture.distDir, 'index.html'), '<!doctype html><title>Portfolio</title>\n');
|
|
const envelope = JSON.parse(await readFile(fixture.signedManifestPath, 'utf8'));
|
|
envelope.signature.value = `${envelope.signature.value[0] === 'A' ? 'B' : 'A'}${envelope.signature.value.slice(1)}`;
|
|
await writeFile(fixture.signedManifestPath, canonicalValueBytes(envelope));
|
|
await expect(buildReleasePackage(buildInput(fixture, join(root, 'invalid.tar')))).rejects.toThrow('signature');
|
|
});
|
|
});
|