97 lines
3.5 KiB
TypeScript
97 lines
3.5 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,
|
|
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,
|
|
) {
|
|
return {
|
|
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,
|
|
};
|
|
}
|
|
|
|
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 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-"));
|
|
const fixture = await createReleaseFixture(root);
|
|
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(
|
|
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(fixture.signedManifestPath, canonicalValueBytes(envelope));
|
|
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");
|
|
});
|
|
});
|