Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 46 additions & 12 deletions src/codex/desktop-compatibility/windows-certificate-trust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@ export type DesktopCertificateTrust = "trusted" | "not-trusted" | "unknown";
export type TrustOperation = "inspect" | "trust" | "remove";
export type CertificateTrustRunner = (operation: TrustOperation, certificateDer: string, fingerprint: string) => Promise<DesktopCertificateTrust>;

/** Public status is an interactive read; a store that cannot answer within it reports `unknown`. */
export const TRUST_INSPECT_DEADLINE_MS = 10_000;
/** A mutation may wait on the user's certificate prompt. */
export const TRUST_CHANGE_DEADLINE_MS = 120_000;

/** The slice of `child_process.execFile` the runner uses, so a deadline breach can be driven without a real store. */
export type TrustCommandExec = (
file: string,
args: string[],
options: { timeout: number; maxBuffer: number; windowsHide: boolean; encoding: "utf8" },
callback: (error: Error | null, stdout: string) => void,
) => { stdin?: { on(event: "error", listener: () => void): unknown; end(chunk: string): unknown } | null };

export type PowerShellTrustRunnerOptions = {
inspectDeadlineMs?: number;
platform?: NodeJS.Platform;
exec?: TrustCommandExec;
resolvePowerShell?: () => string;
};

const SCRIPT = [
"$ErrorActionPreference='Stop'",
"$inputData=[Console]::In.ReadToEnd()|ConvertFrom-Json",
Expand All @@ -31,18 +51,32 @@ const SCRIPT = [
"}finally{$store.Close();$store.Dispose();$cert.Dispose()}",
].join("\n");

const run: CertificateTrustRunner = (operation, certificateDer, fingerprint) => {
if (process.platform !== "win32") return Promise.resolve("unknown");
if (operation !== "inspect" && isTestHomeGuardArmed()) return Promise.resolve("unknown");
return new Promise(resolve => {
const child = execFile(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", SCRIPT], {
timeout: operation === "inspect" ? 10_000 : 120_000, maxBuffer: 4096, windowsHide: true, encoding: "utf8",
}, (error, stdout) => resolve(!error && (stdout.trim() === "trusted" || stdout.trim() === "not-trusted")
? stdout.trim() as DesktopCertificateTrust : "unknown"));
child.stdin?.on("error", () => { /* completion and readback decide the result */ });
child.stdin?.end(JSON.stringify({ operation, certificate: certificateDer, fingerprint }));
});
};
/**
* The real CurrentUser Root runner. Any failure, including a deadline breach, is `unknown`, which
* never authorizes a write. The options exist so a test can replay this exact call with a cold-start
* deadline and can drive a breach deterministically; production uses the defaults.
*/
export function createPowerShellCertificateTrustRunner({
inspectDeadlineMs = TRUST_INSPECT_DEADLINE_MS,
platform = process.platform,
exec = execFile as unknown as TrustCommandExec,
resolvePowerShell = resolveTrustedWindowsPowerShellExe,
}: PowerShellTrustRunnerOptions = {}): CertificateTrustRunner {
return (operation, certificateDer, fingerprint) => {
if (platform !== "win32") return Promise.resolve("unknown");
if (operation !== "inspect" && isTestHomeGuardArmed()) return Promise.resolve("unknown");
return new Promise(resolve => {
const child = exec(resolvePowerShell(), ["-NoProfile", "-NonInteractive", "-Command", SCRIPT], {
timeout: operation === "inspect" ? inspectDeadlineMs : TRUST_CHANGE_DEADLINE_MS, maxBuffer: 4096, windowsHide: true, encoding: "utf8",
}, (error, stdout) => resolve(!error && (stdout.trim() === "trusted" || stdout.trim() === "not-trusted")
? stdout.trim() as DesktopCertificateTrust : "unknown"));
child.stdin?.on("error", () => { /* completion and readback decide the result */ });
child.stdin?.end(JSON.stringify({ operation, certificate: certificateDer, fingerprint }));
});
};
}

const run = createPowerShellCertificateTrustRunner();

/** Public status cannot create a certificate, decrypt a key or alter OS trust. */
export async function inspectWindowsCertificateTrust(certPem: string, expectedFingerprint: string, runner: CertificateTrustRunner = run): Promise<DesktopCertificateTrust> {
Expand Down
45 changes: 30 additions & 15 deletions tests/cli/ocx-launcher-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
import { chmodSync, copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, realpathSync, readlinkSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
import { createServer } from "node:net";
Expand All @@ -10,6 +10,7 @@ import { findDesktopCli, findPathBun, type PathBunIo } from "../../src/lib/bun-p
import { REAL_BUN_MIN_BYTES } from "../../src/lib/bun-binary-validator.mjs";
import { killProxy } from "../../src/lib/process-control";
import { repoPath } from "../helpers/repo-root";
import { COLD_SPAWN_WARMUP_HOOK_BUDGET_MS, warmColdSpawn } from "../helpers/cold-spawn-warmup";
import { INTERNAL_DEADLINE_MS, SPAWN_BUDGET_MS } from "../helpers/test-budget";
import { trustedFixtureRoot } from "../helpers/trusted-fixture-root";

Expand Down Expand Up @@ -762,14 +763,29 @@ describe("real PATH Bun validation", () => {
});
});

function warmCopiedBun(path: string): void {
if (process.platform !== "win32") return;
const warmed = spawnSync(path, ["--version"], {
encoding: "utf8",
timeout: WINDOWS_COLD_SPAWN_MS,
windowsHide: true,
/**
* The one real Bun copy every PATH-fallback case puts on PATH, created and run once in setup.
*
* The first exec of a freshly copied bun binary is held by the OS's first-run scan: about 1s on an
* idle macOS host (later execs of the same path take ~3ms) and well over that for a cold win32
* bun.exe. A copy per case put that scan inside each case's 5s budget and inside the launcher's own
* 5s PATH probe, so the verdict tracked host load (#7578). The scan is keyed by path, so a hardlink
* does not inherit it; sharing the warmed path is what keeps it out of the measured window.
* The launcher only reads and executes this file, so sharing it changes nothing a case asserts.
*/
let sharedPathBun: { root: string; dir: string; bun: string } | undefined;

async function createWarmPathBun(): Promise<void> {
const root = realpathSync.native(pathFixtureRoot("ocx-path-launcher-bun-"));
const dir = join(root, "path"); mkdirSync(dir);
const bun = join(dir, process.platform === "win32" ? "bun.exe" : "bun");
copyFileSync(process.execPath, bun);
chmodSync(bun, 0o755);
sharedPathBun = { root, dir, bun };
await warmColdSpawn("bun/launcher-path-fallback-copy", deadlineMs => {
const warmed = spawnSync(bun, ["--version"], { encoding: "utf8", timeout: deadlineMs, windowsHide: true });
if (warmed.status !== 0) throw new Error(`[cold-spawn-warmup] PATH Bun copy did not run: status=${String(warmed.status)} ${warmed.stderr}`);
});
expect(warmed.status, warmed.stderr).toBe(0);
}

function launcherFallbackFixture(mode: "missing" | "bundled" | "repair" | "broken" | "override" | "none" | "inspection" | "rejected") {
Expand All @@ -783,17 +799,14 @@ function launcherFallbackFixture(mode: "missing" | "bundled" | "repair" | "broke
copyFileSync(BIN_OCX, join(root, "bin", "ocx.mjs"));
writeFileSync(join(root, "package.json"), JSON.stringify({ version: "2.81.0", dependencies: { bun: "1.4.2" } }));
writeFileSync(join(root, "src", "cli", "index.ts"), 'console.log(JSON.stringify({source:process.env.OCX_BUN_RUNTIME_SOURCE,path:process.env.OCX_BUN_RUNTIME_PATH}));');
const pathDir = join(root, "path"); mkdirSync(pathDir);
const shared = mode !== "none" && mode !== "rejected";
if (shared && !sharedPathBun) throw new Error("the shared PATH Bun is created by the activation suite's beforeAll");
const pathDir = shared ? sharedPathBun!.dir : join(root, "path");
if (!shared) mkdirSync(pathDir);
const pathBun = join(pathDir, process.platform === "win32" ? "bun.exe" : "bun");
if (mode === "rejected") {
writeFileSync(pathBun, '#!/bin/sh\nprintf "2.0.0\\n"\n#' + "x".repeat(REAL_BUN_MIN_BYTES));
chmodSync(pathBun, 0o755);
} else if (mode !== "none") {
copyFileSync(process.execPath, pathBun);
chmodSync(pathBun, 0o755);
// The product PATH probe is 5s for both execs together. Warm the copy first so
// that budget measures a scanned binary; a cold win32 bun.exe is the flake.
warmCopiedBun(pathBun);
}
const bundleDir = join(root, "node_modules", "bun");
if (["bundled", "repair", "broken", "inspection"].includes(mode)) {
Expand All @@ -820,6 +833,8 @@ function launcherFallbackFixture(mode: "missing" | "bundled" | "repair" | "broke
}

describe.skipIf(!nodeAvailable)("Node launcher PATH fallback activation", () => {
beforeAll(async () => { await createWarmPathBun(); }, COLD_SPAWN_WARMUP_HOOK_BUDGET_MS);
afterAll(() => { if (sharedPathBun) removeTree(sharedPathBun.root); sharedPathBun = undefined; });
test.each(["missing", "broken", "inspection"] as const)("%s bundle reaches validated PATH Bun", mode => {
const result = launcherFallbackFixture(mode);
expect(result.status).toBe(0);
Expand Down
44 changes: 41 additions & 3 deletions tests/clients/desktop-compatibility-trust.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { describe, expect, test } from "bun:test";
import { beforeAll, describe, expect, test } from "bun:test";
import { randomUUID, X509Certificate } from "node:crypto";
import { createCertificateAuthority } from "../../src/claude/intercept/local-ca";
import type { StoredDesktopAuthority } from "../../src/codex/desktop-compatibility/certificate-store";
import { createWindowsCertificateTrust, inspectWindowsCertificateTrust, type DesktopCertificateTrust, type TrustOperation } from "../../src/codex/desktop-compatibility/windows-certificate-trust";
import {
createPowerShellCertificateTrustRunner, createWindowsCertificateTrust, inspectWindowsCertificateTrust, TRUST_INSPECT_DEADLINE_MS,
type DesktopCertificateTrust, type TrustCommandExec, type TrustOperation,
} from "../../src/codex/desktop-compatibility/windows-certificate-trust";
import { COLD_SPAWN_WARMUP_HOOK_BUDGET_MS, warmColdSpawn } from "../helpers/cold-spawn-warmup";

function fixture(serverAuthOnly = true): StoredDesktopAuthority {
const commonName = `OpenCodex Codex Desktop ${randomUUID()}`;
Expand Down Expand Up @@ -73,7 +77,41 @@ describe("exact Windows compatibility certificate trust", () => {
release("trusted"); expect(await first).toBe("trusted");
});

test.skipIf(process.platform !== "win32")("real CurrentUser Root inspection remains read-only for an unregistered synthetic CA", async () => {
test("a store read that outlives the status deadline is unknown and requests nothing but an inspection", async () => {
// The runner's own deadline, driven without a real store: execFile reports a killed child the way
// node does when `timeout` elapses, with whatever the child printed before the kill. This is the
// half of the real-store case that depends on timing, so it is proven here on every platform.
const value = fixture(), calls: { timeout: number; input: string }[] = [];
const exec: TrustCommandExec = (_file, _args, options, callback) => {
let input = "";
queueMicrotask(() => {
calls.push({ timeout: options.timeout, input });
callback(Object.assign(new Error("Command failed: powershell.exe"), { killed: true, signal: "SIGTERM", code: null }), "not-trusted");
});
return { stdin: { on: () => undefined, end: (chunk: string) => { input = chunk; } } };
};
const runner = createPowerShellCertificateTrustRunner({ platform: "win32", exec, resolvePowerShell: () => "powershell.exe" });
expect(await inspectWindowsCertificateTrust(value.authority.certPem, value.fingerprint, runner)).toBe("unknown");
expect(calls).toHaveLength(1);
expect(calls[0].timeout).toBe(TRUST_INSPECT_DEADLINE_MS);
expect(JSON.parse(calls[0].input).operation).toBe("inspect");
});
});

describe.skipIf(process.platform !== "win32")("exact Windows compatibility certificate trust on the real store", () => {
// The first PowerShell in a shard pays process start, .NET load and the X509 store open; on a
// loaded runner that crossed the 10s status deadline and read as `unknown` (#7569). Replaying the
// same inspection once here leaves the unchanged product deadline below measuring a warm read.
beforeAll(async () => {
await warmColdSpawn("powershell/desktop-certificate-trust-inspect", async deadlineMs => {
const value = fixture();
const runner = createPowerShellCertificateTrustRunner({ inspectDeadlineMs: deadlineMs });
const state = await inspectWindowsCertificateTrust(value.authority.certPem, value.fingerprint, runner);
if (state === "unknown") throw new Error(`[cold-spawn-warmup] real CurrentUser Root inspection gave no answer within ${deadlineMs}ms`);
});
}, COLD_SPAWN_WARMUP_HOOK_BUDGET_MS);

test("real CurrentUser Root inspection remains read-only for an unregistered synthetic CA", async () => {
const value = fixture();
expect(await inspectWindowsCertificateTrust(value.authority.certPem, value.fingerprint)).toBe("not-trusted");
}, 15_000);
Expand Down
Loading