feat: root-CA + per-host leaf certs for AgentBridge static server (#6684)#7731
Merged
Conversation
…er (#6684) 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.
Contributor
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
| const ca = await generateMitmCa("Test MITM CA"); | ||
| const store = new DynamicCertStore("Test MITM CA", ca); | ||
| const neverRegisteredHost = "some-brand-new-tool-host.example.com"; | ||
| assert.ok(!allHosts().includes(neverRegisteredHost)); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the AgentBridge static MITM server's single self-signed leaf cert (
src/mitm/cert/generate.ts, scoped only to the 4 antigravity hosts — see #6494) with a persisted local root CA + per-SNI leaf certs, covering the fullMITM_TOOL_HOSTSset (9 tool entries). Reuses the CA/leaf crypto already proven for the TPROXY capture mode (src/mitm/tproxy/dynamicCert.ts) rather than writing new signing logic.src/mitm/cert/rootCa.ts(new) —loadOrCreateMitmCa(): loads a persistedca.key/ca.crtpair if present, otherwise generates once and writes both, chmod'ing the private key0o600.src/mitm/cert/migration.ts(new) — pure migration-gate function. An install that already trusted the old static leaf (server.crt/server.keypresent, no CA pair yet) stays on the legacy leaf this run; a fresh install (no legacy leaf) gets the CA model automatically. Existing trusted installs opt in explicitly viaMITM_ROOT_CA_ENABLED=true. 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(), a thin named wrapper over the existinginstallCertResult()(already cert-path-agnostic). Installs under the sameomniroute-mitm.crttrust-store slot the old leaf used — the CA simply supersedes it, no dual-trust cleanup needed. Distinct from TPROXY's ownomniroute-tproxy-ca.crtslot, untouched.src/mitm/manager.ts— wires the migration gate + CA load/install into the bridge-start sequence, preserving the existing best-effort/non-aborting error-handling shape ([BUG] Agent Bridge cannot start in Docker container #4546). Passes the resolvedMITM_CERT_MODEto the spawnedserver.cjschild so its own gate can't drift from manager.ts's decision.src/mitm/server.cjs— server creation is now wrapped in an asyncstartMitmServer()bootstrap, gated byMITM_CERT_MODE. Default (legacy, unset) reproduces the exact prior synchronous behavior (byte-for-byte, verified withgit diff -w) — existing installs are never silently changed.root-camode builds anSNICallback-drivenhttps.createServer. Sinceserver.cjsis spawned via plainnode(no TS/ESM loader — seemanager.ts'sspawn(process.execPath, [MITM_SERVER_PATH], ...)), it cannotimport()the TS sources directly; the CJS/ESM boundary is crossed via a newsrc/mitm/_internal/rootCaShim.cjs, a CJS twin of the CA/leaf crypto (byte-identicalselfsigned.generate()parameters totproxy/dynamicCert.ts), matching the established pattern of the sibling_internal/*.cjsshims already in this file (e.g. the hand-portedsanitizeErrorMessage).How validated
TDD (Hard Rule #18) — 14 new unit tests, all genuinely new (none of
rootCa.ts/migration.tsexist on the base branch, so they fail-to-import before this change and pass after):tests/unit/mitm-root-ca-persistence-6684.test.ts— generate-once/load-on-repeat,0o600key perms, CAbasicConstraints.tests/unit/mitm-root-ca-leaf-issuance-6684.test.ts— everyMITM_TOOL_HOSTShost resolves aSecureContext, issued leaf SAN matches the hostname, leaf chain validates against the CA cert (X509Certificate.checkIssued/verify), leaf caching (identicalSecureContexton repeat lookup), and a drift guard proving a newly-added tool host needs no matching entry anywhere.tests/unit/mitm-cert-migration-6684.test.ts— every migration-gate branch (legacy stays legacy, fresh install gets CA, explicit opt-in overrides, partial legacy state treated as no-legacy).All 14 pass (
node --import tsx/esm --test tests/unit/mitm-root-ca-persistence-6684.test.ts tests/unit/mitm-root-ca-leaf-issuance-6684.test.ts tests/unit/mitm-cert-migration-6684.test.ts). Pre-existing mitm tests (mitm-cert-mac-fingerprint,mitm-cert-removal-wiring,mitm-manager-*) still pass unchanged.Manual live smoke test (in-repo, not the VPS): spawned
server.cjsdirectly in both modes.MITM_CERT_MODE): boots and listens exactly as before.MITM_CERT_MODE=root-ca, freshDATA_DIR): generatesca.key/ca.crtwith0600perms and boots viaSNICallback. A real TLS handshake withservername: "api.githubcopilot.com"(a Copilot host fromMITM_TOOL_HOSTS, previously uncovered by the legacy single-SAN antigravity-only cert) returned a CA-issued leaf whose subject/SAN matched that hostname exactly, and chained to the CA.Gates run in the worktree, all green:
check-test-discovery.mjs,typecheck:core,typecheck:noimplicit:core(2 pre-existing unrelated files only, confirmed viagit show origin/release/v3.8.49:<path>),eslinton every changed file (0 errors),check:complexity-ratchets(both cyclomatic + cognitive, no regression),check-file-size.mjs(no frozen-file growth),check:cycles(no new cycle),check-docs-sync.mjs,check-fabricated-docs.mjs --strict(3 pre-existing failures, all in files this PR does not touch —docs/INCIDENT_RESPONSE.md,docs/PERF_BUDGETS.md,docs/routing/REASONING_ROUTING.md, confirmed base-red).Deferred to VPS live validation
Actual OS trust-store mutation (
installCaCert()'s end effect) is not unit-testable. Deferred per Hard Rule #18 path 2:Scoping notes
MITM_ROOT_CA_ENABLEDas a plain env var (not a DB-backedfeatureFlagDefinitions.tsentry) — consistent with howserver.cjs/manager.tsalready gate other MITM behavior (MITM_VERBOSE,MITM_LOCAL_PORT,OMNIROUTE_SKIP_SYSTEM_TRUST), and avoids pulling in the i18n-coverage gate for a flag with no dashboard UI in this PR.docs/security/MITM-TPROXY-DECRYPT.md§4 anddocs/frameworks/AGENTBRIDGE.md§2.2 now describe the unified CA architecture, the one-time trust step,MITM_ROOT_CA_ENABLED, and the legacy-leaf back-compat behavior.Refs #6684 — the dashboard UI toggle / one-click "opt into root-CA" flow and the VPS trust-store live check above are left for a follow-up so this PR stays reviewable; the core persistence/issuance/migration/server-wiring behavior is complete and tested.