Files
OmniRoute/tests/unit/mitm-root-ca-persistence-6684.test.ts
Diego Rodrigues de Sa e Souza a2004060c5 feat(mitm): root-CA + per-host leaf certs for AgentBridge static server (#6684) (#7731)
Replace the AgentBridge static server's single self-signed leaf cert
(scoped only to the 4 antigravity hosts) with a persisted local root CA
+ per-SNI leaf certs, reusing the CA/leaf crypto already proven for the
TPROXY capture mode (tproxy/dynamicCert.ts). server.cjs switches from a
static key/cert to an SNICallback so every host in MITM_TOOL_HOSTS gets
a matching leaf, not just antigravity.

- src/mitm/cert/rootCa.ts: load-or-generate-once CA persistence
  (ca.key/ca.crt under <DATA_DIR>/mitm/), private key chmod 0o600.
- src/mitm/cert/migration.ts: pure migration gate — an already-trusted
  legacy leaf install stays on the old leaf until the operator opts in
  via MITM_ROOT_CA_ENABLED=true; a fresh install gets the CA model
  automatically. A CA that can sign a leaf for any host is materially
  more powerful than the old fixed-SAN leaf, so the switch is never
  silent for an already-trusted install.
- src/mitm/cert/install.ts: installCaCert() — thin wrapper over the
  existing cert-path-agnostic installCertResult(), same
  omniroute-mitm.crt trust-store slot the old leaf used (supersedes it,
  no dual-trust cleanup needed).
- src/mitm/manager.ts: wires the migration gate + CA load/install into
  the bridge-start sequence, passes the resolved MITM_CERT_MODE to the
  spawned server.cjs child so it can't drift from manager.ts's decision.
- src/mitm/server.cjs: async-bootstraps server creation behind the same
  MITM_CERT_MODE gate; default ("legacy") reproduces the exact prior
  synchronous behavior. The CJS/ESM boundary (server.cjs is spawned via
  plain `node`, no TS loader) is crossed via a new
  _internal/rootCaShim.cjs CJS twin of the CA/leaf crypto, matching the
  established pattern of the sibling _internal/*.cjs shims in this file.

Validated: 14 new unit tests (CA generate-once, 0o600 key perms, CA
basicConstraints, leaf issuance across every MITM_TOOL_HOSTS host, SAN
match, chain validation against the CA, leaf caching, migration-gate
branches) plus a manual live smoke test spawning server.cjs in both
legacy and root-ca mode (confirmed a real TLS handshake with SNI
api.githubcopilot.com returns a CA-issued leaf for that host).

Deferred to VPS live validation (OS-trust-store mutation is not
unit-testable): actual OS trust-store install of the CA cert via
installCaCert() on Linux/macOS/Windows.
2026-07-19 14:37:27 -03:00

66 lines
2.3 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { loadOrCreateMitmCa } from "../../src/mitm/cert/rootCa.ts";
// #6684: root-CA persistence — the OS trust prompt must only fire once per
// machine, so `loadOrCreateMitmCa()` must generate on first run and load
// (byte-identical) on every subsequent run, with a restrictively-permissioned
// private key file.
function tmpCertDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "mitm-root-ca-test-"));
}
test("loadOrCreateMitmCa: first call with an empty dir generates and persists a CA", async () => {
const certDir = tmpCertDir();
try {
assert.equal(fs.existsSync(path.join(certDir, "ca.key")), false);
const ca = await loadOrCreateMitmCa(certDir);
assert.ok(ca.key.includes("PRIVATE KEY"));
assert.ok(ca.cert.includes("CERTIFICATE"));
assert.equal(fs.existsSync(path.join(certDir, "ca.key")), true);
assert.equal(fs.existsSync(path.join(certDir, "ca.crt")), true);
} finally {
fs.rmSync(certDir, { recursive: true, force: true });
}
});
test("loadOrCreateMitmCa: a second call loads the same CA instead of regenerating", async () => {
const certDir = tmpCertDir();
try {
const first = await loadOrCreateMitmCa(certDir);
const second = await loadOrCreateMitmCa(certDir);
assert.equal(second.key, first.key);
assert.equal(second.cert, first.cert);
} finally {
fs.rmSync(certDir, { recursive: true, force: true });
}
});
test("loadOrCreateMitmCa: the written CA private key file mode is 0o600", { skip: process.platform === "win32" }, async () => {
const certDir = tmpCertDir();
try {
const ca = await loadOrCreateMitmCa(certDir);
const mode = fs.statSync(ca.keyPath).mode & 0o777;
assert.equal(mode, 0o600);
} finally {
fs.rmSync(certDir, { recursive: true, force: true });
}
});
test("loadOrCreateMitmCa: the CA cert carries CA basicConstraints (matches generateMitmCa)", async () => {
const certDir = tmpCertDir();
try {
const ca = await loadOrCreateMitmCa(certDir);
const { X509Certificate } = await import("node:crypto");
const cert = new X509Certificate(ca.cert);
assert.equal(cert.ca, true);
} finally {
fs.rmSync(certDir, { recursive: true, force: true });
}
});