fix: recover and fence atomic activation

This commit is contained in:
2026-09-02 11:49:49 +03:00
parent 79e5d04395
commit a8fa09e21e
12 changed files with 1733 additions and 543 deletions

View File

@@ -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");
});
});