mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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.
66 lines
2.3 KiB
TypeScript
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 });
|
|
}
|
|
});
|