-
Notifications
You must be signed in to change notification settings - Fork 0
Phase 1: Local MITM proxy with CA lifecycle and trace capture #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
45f923f
aa4dc63
81d7be1
fb76640
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import { existsSync } from "node:fs"; | ||
| import { mkdir } from "node:fs/promises"; | ||
| import { join } from "node:path"; | ||
| import { generateRootCA, type KeyCertPair } from "./cert-generator"; | ||
|
|
||
| export type CAStatus = | ||
| | "not-generated" | ||
| | "generated" | ||
| | "installed" | ||
| | "expired"; | ||
|
|
||
| const CA_DIR = join( | ||
| process.env.HOME ?? "~", | ||
| "Library", | ||
| "Application Support", | ||
| "AgentTap", | ||
| "ca", | ||
| ); | ||
| const CA_KEY_PATH = join(CA_DIR, "ca-key.pem"); | ||
| const CA_CERT_PATH = join(CA_DIR, "ca.pem"); | ||
|
|
||
| let loaded: KeyCertPair | null = null; | ||
|
|
||
| export function getCAPaths() { | ||
| return { keyPath: CA_KEY_PATH, certPath: CA_CERT_PATH, dir: CA_DIR }; | ||
| } | ||
|
|
||
| export async function ensureCA(): Promise<KeyCertPair> { | ||
| if (loaded) return loaded; | ||
|
|
||
| if (existsSync(CA_KEY_PATH) && existsSync(CA_CERT_PATH)) { | ||
| const key = await Bun.file(CA_KEY_PATH).text(); | ||
| const cert = await Bun.file(CA_CERT_PATH).text(); | ||
|
|
||
| if (isCertExpired(cert)) { | ||
| console.log("[CA] Certificate expired, regenerating..."); | ||
| return regenerateCA(); | ||
| } | ||
|
|
||
| loaded = { key, cert }; | ||
| console.log("[CA] Loaded existing CA from", CA_DIR); | ||
| return loaded; | ||
|
Comment on lines
+31
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Treat unreadable CA certs as invalid. If Suggested fix function isCertExpired(pem: string): boolean {
try {
const crypto = require("node:crypto");
const cert = new crypto.X509Certificate(pem);
return new Date(cert.validTo) < new Date();
} catch {
- return false;
+ return true;
}
}Also applies to: 75-83 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| return regenerateCA(); | ||
| } | ||
|
|
||
| export async function regenerateCA(): Promise<KeyCertPair> { | ||
| await mkdir(CA_DIR, { recursive: true }); | ||
|
|
||
| console.log("[CA] Generating new root CA..."); | ||
| const pair = await generateRootCA(); | ||
|
|
||
| await Bun.write(CA_KEY_PATH, pair.key, { mode: 0o600 }); | ||
| await Bun.write(CA_CERT_PATH, pair.cert); | ||
|
|
||
| loaded = pair; | ||
| console.log("[CA] Root CA written to", CA_DIR); | ||
| return loaded; | ||
| } | ||
|
|
||
| export function getCA(): KeyCertPair | null { | ||
| return loaded; | ||
| } | ||
|
|
||
| export function getCAStatus(): CAStatus { | ||
| if (!loaded) { | ||
| if (!existsSync(CA_CERT_PATH)) return "not-generated"; | ||
| } | ||
| if (loaded && isCertExpired(loaded.cert)) return "expired"; | ||
| if (loaded) return "generated"; | ||
| return "not-generated"; | ||
| } | ||
|
|
||
| function isCertExpired(pem: string): boolean { | ||
| try { | ||
| const crypto = require("node:crypto"); | ||
| const cert = new crypto.X509Certificate(pem); | ||
| return new Date(cert.validTo) < new Date(); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,56 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { KeyCertPair } from "./cert-generator"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { generateLeafCert } from "./cert-generator"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const MAX_CACHE_SIZE = 100; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface CacheEntry { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pair: KeyCertPair; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expiresAt: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastUsed: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cache = new Map<string, CacheEntry>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function getOrGenerateLeafCert( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| domain: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| caCert: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| caKey: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<KeyCertPair> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const existing = cache.get(domain); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const now = Date.now(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (existing && existing.expiresAt > now) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| existing.lastUsed = now; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return existing.pair; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const pair = await generateLeafCert(domain, caCert, caKey); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (cache.size >= MAX_CACHE_SIZE) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| evictLRU(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cache.set(domain, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pair, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expiresAt: now + 25 * 86400_000, // 25 days (leaf certs valid 30) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastUsed: now, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return pair; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+19
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deduplicate concurrent cache misses per domain. Multiple simultaneous misses for the same domain will each generate a new cert. This is avoidable work on a hot path. Proposed fix const cache = new Map<string, CacheEntry>();
+const inflight = new Map<string, Promise<KeyCertPair>>();
@@
if (existing && existing.expiresAt > now) {
existing.lastUsed = now;
return existing.pair;
}
- const pair = await generateLeafCert(domain, caCert, caKey);
+ const pending = inflight.get(domain);
+ if (pending) return pending;
+
+ const generation = generateLeafCert(domain, caCert, caKey).finally(() => {
+ inflight.delete(domain);
+ });
+ inflight.set(domain, generation);
+ const pair = await generation;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function clearCache(): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cache.clear(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function evictLRU(): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let oldest: string | null = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let oldestTime = Infinity; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const [domain, entry] of cache) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (entry.lastUsed < oldestTime) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| oldestTime = entry.lastUsed; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| oldest = domain; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (oldest) cache.delete(oldest); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not persist the CA private key under
$HOME/Library/....This stores the MITM root key outside the app container, which weakens the trust boundary around the most sensitive secret in the system. Based on learnings,
Never store the custom CA private key outside the app's sandboxed container.🤖 Prompt for AI Agents