Files
clean-architecture-frontend…/tests/unit/security-promotion-staging.test.ts
T

371 lines
15 KiB
TypeScript

import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import {
chmod,
lstat,
mkdir,
mkdtemp,
readFile,
readdir,
rename,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { describe, expect, it } from "vitest";
import { PROMOTED_FILE_NAMES } from "../../scripts/contracts/promotion-artifacts.ts";
import { publishPrivatePromotionStaging } from "../../scripts/lib/promotion-stager.ts";
import { verifyExactPromotionBundle } from "../../scripts/lib/exact-promotion-bundle.ts";
import {
PROCESS_HEAVY_TIMEOUT_MS,
privatePromotionFiles,
syntheticSignedPromotionBundle,
} from "./security-followup-fixture.ts";
describe("security private promotion staging contracts", () => {
it("forces exact private staging modes in an isolated child with umask 077", async () => {
const root = await mkdtemp(path.join(tmpdir(), "promotion-umask-"));
try {
const stagerUrl = pathToFileURL(
path.join(process.cwd(), "scripts/lib/promotion-stager.ts"),
).href;
const contractsUrl = pathToFileURL(
path.join(process.cwd(), "scripts/contracts/promotion-artifacts.ts"),
).href;
const childPath = path.join(root, "umask-child.mjs");
const resultPath = path.join(root, "result.json");
await writeFile(resultPath, "{}\n", { mode: 0o600 });
await writeFile(
childPath,
[
`import { lstat, writeFile } from "node:fs/promises";`,
`import path from "node:path";`,
`import { createHash } from "node:crypto";`,
`import { cleanupFinalizedPromotion, publishPrivatePromotionStaging } from ${JSON.stringify(stagerUrl)};`,
`import { PROMOTED_FILE_NAMES } from ${JSON.stringify(contractsUrl)};`,
`process.umask(Number.parseInt(process.argv[2], 8));`,
`const runnerTempRoot = process.argv[3];`,
`const files = PROMOTED_FILE_NAMES.map((name) => { const bytes = Buffer.from(name); return { name, bytes, sha256: createHash("sha256").update(bytes).digest("hex") }; });`,
`const finalized = await publishPrivatePromotionStaging(runnerTempRoot, { id: "umask", attempt: 1 }, files, () => Buffer.alloc(16, 1));`,
`const directoryMode = (await lstat(finalized.stagingRoot)).mode & 0o777;`,
`const fileModes = await Promise.all(PROMOTED_FILE_NAMES.map(async (name) => (await lstat(path.join(finalized.stagingRoot, name))).mode & 0o777));`,
`await cleanupFinalizedPromotion({ runnerTempRoot, stagingRoot: finalized.stagingRoot, cleanupToken: finalized.cleanupToken, runnerTempIdentity: finalized.runnerTempIdentity, stagingIdentity: finalized.stagingIdentity });`,
`await writeFile(process.argv[4], JSON.stringify({ directoryMode, fileModes }));`,
].join("\n"),
);
const child = spawnSync(process.execPath, [childPath, "077", root, resultPath], {
cwd: root,
encoding: "utf8",
timeout: 30_000,
});
expect(child.status, `${child.stdout}\n${child.stderr}`).toBe(0);
expect(JSON.parse(await readFile(resultPath, "utf8"))).toEqual({
directoryMode: 0o700,
fileModes: [0o400, 0o400, 0o400, 0o400, 0o400],
});
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("rejects a staged file unlinked and recreated after its original write", async () => {
const root = await mkdtemp(path.join(tmpdir(), "promotion-seal-recreate-"));
const files = privatePromotionFiles();
const token = `promotion-seal-1-${"11".repeat(16)}`;
try {
await expect(
publishPrivatePromotionStaging(
root,
{ id: "seal", attempt: 1 },
files,
() => Buffer.alloc(16, 0x11),
undefined,
async (name) => {
if (name !== PROMOTED_FILE_NAMES.at(-1)) return;
const first = path.join(root, token, PROMOTED_FILE_NAMES[0]);
await rm(first);
await writeFile(first, "replacement bytes\n", { mode: 0o400 });
},
),
).rejects.toThrow(/staged.*digest|inode|seal/u);
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("rejects staged mode drift before returning the upload root", async () => {
const root = await mkdtemp(path.join(tmpdir(), "promotion-seal-mode-"));
const files = privatePromotionFiles();
const token = `promotion-seal-1-${"12".repeat(16)}`;
try {
await expect(
publishPrivatePromotionStaging(
root,
{ id: "seal", attempt: 1 },
files,
() => Buffer.alloc(16, 0x12),
undefined,
async (name) => {
if (name === PROMOTED_FILE_NAMES.at(-1)) {
await chmod(path.join(root, token, PROMOTED_FILE_NAMES[0]), 0o600);
}
},
),
).rejects.toThrow(/mode.*0400|staged.*mode|seal/u);
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("rejects staging leaf replacement between mkdir and descriptor open", async () => {
const root = await mkdtemp(path.join(tmpdir(), "promotion-preopen-replace-"));
const files = privatePromotionFiles();
const token = `promotion-preopen-1-${"13".repeat(16)}`;
const displaced = path.join(root, `${token}-displaced`);
const replacementCanary = path.join(root, token, PROMOTED_FILE_NAMES[0]);
try {
await expect(
publishPrivatePromotionStaging(
root,
{ id: "preopen", attempt: 1 },
files,
() => Buffer.alloc(16, 0x13),
undefined,
undefined,
undefined,
async (stagingRoot) => {
await rename(stagingRoot, displaced);
await mkdir(stagingRoot, { mode: 0o700 });
await writeFile(replacementCanary, "external replacement canary\n");
},
),
).rejects.toThrow(/staging leaf.*changed|mkdir.*open|identity/u);
await expect(readFile(replacementCanary, "utf8")).resolves.toBe(
"external replacement canary\n",
);
await expect(readdir(displaced)).resolves.toEqual([]);
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("does not scan a crowded parent to recover an unverified pre-open leaf", async () => {
const root = await mkdtemp(path.join(tmpdir(), "promotion-preopen-bounded-"));
const files = privatePromotionFiles();
const token = `promotion-preopen-bound-1-${"14".repeat(16)}`;
const displaced = path.join(root, `${token}-displaced`);
const replacementCanary = path.join(root, token, PROMOTED_FILE_NAMES[0]);
try {
for (let offset = 0; offset < 4_097; offset += 128) {
await Promise.all(
Array.from(
{ length: Math.min(128, 4_097 - offset) },
(_, index) =>
mkdir(
path.join(
root,
`noise-${String(offset + index).padStart(4, "0")}`,
),
),
),
);
}
let failure: unknown;
try {
await publishPrivatePromotionStaging(
root,
{ id: "preopen-bound", attempt: 1 },
files,
() => Buffer.alloc(16, 0x14),
undefined,
undefined,
undefined,
async (stagingRoot) => {
await rename(stagingRoot, displaced);
await mkdir(stagingRoot, { mode: 0o700 });
await writeFile(replacementCanary, "external replacement canary\n");
},
);
} catch (error) {
failure = error;
}
expect(failure).toBeInstanceOf(Error);
expect(failure).not.toBeInstanceOf(AggregateError);
expect((failure as Error).message).toMatch(/staging leaf.*identity/i);
await expect(readdir(displaced)).resolves.toEqual([]);
await expect(readFile(replacementCanary, "utf8")).resolves.toBe(
"external replacement canary\n",
);
} finally {
await rm(root, { recursive: true, force: true });
}
}, 20_000);
it("leaves a non-empty moved original untouched after pre-open mismatch", async () => {
const root = await mkdtemp(path.join(tmpdir(), "promotion-preopen-nonempty-"));
const files = privatePromotionFiles();
const token = `promotion-preopen-nonempty-1-${"15".repeat(16)}`;
const displaced = path.join(root, `${token}-displaced`);
const ownedResidual = path.join(displaced, "owned-residual");
const replacementCanary = path.join(root, token, PROMOTED_FILE_NAMES[0]);
try {
let failure: unknown;
try {
await publishPrivatePromotionStaging(
root,
{ id: "preopen-nonempty", attempt: 1 },
files,
() => Buffer.alloc(16, 0x15),
undefined,
undefined,
undefined,
async (stagingRoot) => {
await rename(stagingRoot, displaced);
await writeFile(ownedResidual, "owned residual\n");
await mkdir(stagingRoot, { mode: 0o700 });
await writeFile(replacementCanary, "external replacement canary\n");
},
);
} catch (error) {
failure = error;
}
expect(failure).toBeInstanceOf(Error);
expect(failure).not.toBeInstanceOf(AggregateError);
expect((failure as Error).message).toMatch(/staging leaf.*identity/i);
await expect(readFile(ownedResidual, "utf8")).resolves.toBe(
"owned residual\n",
);
await expect(readFile(replacementCanary, "utf8")).resolves.toBe(
"external replacement canary\n",
);
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("does not search outside the parent for a moved unverified original", async () => {
const root = await mkdtemp(path.join(tmpdir(), "promotion-preopen-missing-"));
const outside = await mkdtemp(path.join(tmpdir(), "promotion-preopen-moved-"));
const files = privatePromotionFiles();
const token = `promotion-preopen-missing-1-${"16".repeat(16)}`;
const displaced = path.join(outside, token);
const replacementCanary = path.join(root, token, PROMOTED_FILE_NAMES[0]);
try {
let failure: unknown;
try {
await publishPrivatePromotionStaging(
root,
{ id: "preopen-missing", attempt: 1 },
files,
() => Buffer.alloc(16, 0x16),
undefined,
undefined,
undefined,
async (stagingRoot) => {
await rename(stagingRoot, displaced);
await mkdir(stagingRoot, { mode: 0o700 });
await writeFile(replacementCanary, "external replacement canary\n");
},
);
} catch (error) {
failure = error;
}
expect(failure).toBeInstanceOf(Error);
expect(failure).not.toBeInstanceOf(AggregateError);
expect((failure as Error).message).toMatch(/staging leaf.*identity/i);
await expect(lstat(displaced)).resolves.toEqual(
expect.objectContaining({ dev: expect.any(Number), ino: expect.any(Number) }),
);
await expect(readFile(replacementCanary, "utf8")).resolves.toBe(
"external replacement canary\n",
);
} finally {
await rm(root, { recursive: true, force: true });
await rm(outside, { recursive: true, force: true });
}
});
it("rejects a fresh signed exact-five bundle replayed under a different expected run", async () => {
const fixture = syntheticSignedPromotionBundle();
await expect(
verifyExactPromotionBundle(fixture.files, {
...fixture.verification,
expected: {
...fixture.verification.expected,
run: { id: "different-run", attempt: 1 },
},
}),
).rejects.toThrow(/external expected run.*mismatch|expected promotion run/u);
});
it("requires every external expected identity variable at the exact promotion CLI", async () => {
const fixture = syntheticSignedPromotionBundle();
const root = await mkdtemp(path.join(tmpdir(), "promotion-replay-cli-"));
const bundleRoot = path.join(root, "bundle");
try {
await mkdir(bundleRoot);
for (const [name, bytes] of Object.entries(fixture.files)) {
await writeFile(path.join(bundleRoot, name), bytes);
}
await writeFile(path.join(root, "vulnerability.pem"), fixture.vulnerabilityPem);
await writeFile(path.join(root, "provenance.pem"), fixture.provenancePem);
const cliPath = path.join(process.cwd(), "scripts/verify-exact-promotion-bundle.ts");
const baseEnvironment: NodeJS.ProcessEnv = {
...process.env,
PROMOTION_BUNDLE_ROOT: bundleRoot,
VULNERABILITY_PUBLIC_KEY_PATH: "vulnerability.pem",
VULNERABILITY_KEY_ID: "synthetic-vulnerability",
PROVENANCE_PUBLIC_KEY_PATH: "provenance.pem",
PROVENANCE_KEY_ID: "synthetic-provenance",
EXPECTED_PROMOTION_RUN_ID: fixture.verification.expected.run.id,
EXPECTED_PROMOTION_RUN_ATTEMPT: String(
fixture.verification.expected.run.attempt,
),
EXPECTED_PROMOTION_SOURCE_REVISION:
fixture.verification.expected.sourceRevision,
EXPECTED_PROMOTION_ARCHIVE_SHA256:
fixture.verification.expected.archiveSha256,
};
const requiredExpected = [
"EXPECTED_PROMOTION_RUN_ID",
"EXPECTED_PROMOTION_RUN_ATTEMPT",
"EXPECTED_PROMOTION_SOURCE_REVISION",
"EXPECTED_PROMOTION_ARCHIVE_SHA256",
] as const;
for (const missing of requiredExpected) {
const environment = { ...baseEnvironment };
delete environment[missing];
const result = spawnSync(process.execPath, [cliPath], {
cwd: root,
encoding: "utf8",
env: environment,
});
expect(result.status, missing).not.toBe(0);
expect(result.stderr, missing).toContain(
`exact promotion verification environment is missing ${missing}`,
);
}
for (const [name, value, diagnostic] of [
["EXPECTED_PROMOTION_RUN_ID", "different-run", /external expected run.*mismatch/u],
["EXPECTED_PROMOTION_RUN_ATTEMPT", "2", /external expected run.*mismatch/u],
["EXPECTED_PROMOTION_SOURCE_REVISION", "f".repeat(40), /external expected source revision.*mismatch/u],
["EXPECTED_PROMOTION_ARCHIVE_SHA256", "0".repeat(64), /external expected archive digest.*mismatch/u],
] as const) {
const result = spawnSync(process.execPath, [cliPath], {
cwd: root,
encoding: "utf8",
env: { ...baseEnvironment, [name]: value },
});
expect(result.status, name).not.toBe(0);
expect(result.stderr, name).toMatch(diagnostic);
}
} finally {
await rm(root, { recursive: true, force: true });
}
}, PROCESS_HEAVY_TIMEOUT_MS);
});