fix: recover and fence atomic activation
This commit is contained in:
@@ -1,51 +1,96 @@
|
||||
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 { 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';
|
||||
import {
|
||||
byteDigest,
|
||||
canonicalValueBytes,
|
||||
} from "../../src/security/canonical-digest.ts";
|
||||
import {
|
||||
buildReleasePackage,
|
||||
buildReleasePackageForTest,
|
||||
} 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) {
|
||||
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,
|
||||
distDir: fixture.distDir,
|
||||
signedManifestPath: fixture.signedManifestPath,
|
||||
sbomPath: fixture.sbomPath,
|
||||
trustPolicyPath: fixture.trustPolicyPath,
|
||||
verifySignedReleaseEnvelope: fixture.verifySignedReleaseEnvelope,
|
||||
protectedState: fixture.protectedState, now: '2026-09-02T01:00:00Z', outputPath,
|
||||
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-'));
|
||||
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',
|
||||
const first = join(root, "release-one.tar");
|
||||
const second = join(root, "release-two.tar");
|
||||
await buildReleasePackageForTest(buildInput(fixture, first));
|
||||
await buildReleasePackageForTest(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-'));
|
||||
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 writeFile(join(fixture.distDir, "extra.txt"), "not in manifest");
|
||||
await expect(
|
||||
buildReleasePackageForTest(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 unlink(join(fixture.distDir, "extra.txt"));
|
||||
await writeFile(join(fixture.distDir, "index.html"), "substituted");
|
||||
await expect(
|
||||
buildReleasePackageForTest(
|
||||
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(
|
||||
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');
|
||||
await expect(
|
||||
buildReleasePackageForTest(
|
||||
buildInput(fixture, join(root, "invalid.tar")),
|
||||
),
|
||||
).rejects.toThrow("signature");
|
||||
});
|
||||
|
||||
test("production package requests reject caller-selected verifier, policy, and clock fields", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "portfolio-package-authority-"));
|
||||
const fixture = await createReleaseFixture(root);
|
||||
await expect(
|
||||
buildReleasePackage(
|
||||
buildInput(fixture, join(root, "release.tar")) as any,
|
||||
),
|
||||
).rejects.toThrow("rejects caller");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user