Fix : MITM
This commit is contained in:
parent
1c3ba6ef69
commit
f4e08fcd16
11 changed files with 497 additions and 150 deletions
|
|
@ -1,55 +1,32 @@
|
|||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const { MITM_DIR } = require("../paths");
|
||||
|
||||
// Wildcard domains — covers all subdomains without needing cert update per tool
|
||||
const WILDCARD_DOMAINS = [
|
||||
"*.googleapis.com",
|
||||
"*.githubcopilot.com",
|
||||
"*.individual.githubcopilot.com",
|
||||
"*.business.githubcopilot.com"
|
||||
];
|
||||
const { generateRootCA, loadRootCA, generateLeafCert } = require("./rootCA");
|
||||
|
||||
/**
|
||||
* Generate self-signed SSL certificate with wildcard SAN.
|
||||
* Covers all current and future MITM tool domains automatically.
|
||||
* Uses selfsigned (pure JS, no openssl needed).
|
||||
* Generate Root CA certificate (one-time setup)
|
||||
* This replaces the old static wildcard cert approach
|
||||
*/
|
||||
async function generateCert() {
|
||||
const certDir = MITM_DIR;
|
||||
const keyPath = path.join(certDir, "server.key");
|
||||
const certPath = path.join(certDir, "server.crt");
|
||||
|
||||
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
||||
console.log("✅ SSL certificate already exists");
|
||||
return { key: keyPath, cert: certPath };
|
||||
}
|
||||
|
||||
if (!fs.existsSync(certDir)) {
|
||||
fs.mkdirSync(certDir, { recursive: true });
|
||||
}
|
||||
|
||||
const selfsigned = require("selfsigned");
|
||||
const attrs = [{ name: "commonName", value: "9router-mitm" }];
|
||||
const notAfter = new Date();
|
||||
notAfter.setFullYear(notAfter.getFullYear() + 1);
|
||||
const pems = await selfsigned.generate(attrs, {
|
||||
keySize: 2048,
|
||||
algorithm: "sha256",
|
||||
notAfterDate: notAfter,
|
||||
extensions: [
|
||||
{
|
||||
name: "subjectAltName",
|
||||
altNames: WILDCARD_DOMAINS.map(domain => ({ type: 2, value: domain }))
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
fs.writeFileSync(keyPath, pems.private);
|
||||
fs.writeFileSync(certPath, pems.cert);
|
||||
|
||||
console.log(`✅ Generated wildcard SSL certificate: ${WILDCARD_DOMAINS.join(", ")}`);
|
||||
return { key: keyPath, cert: certPath };
|
||||
return await generateRootCA();
|
||||
}
|
||||
|
||||
module.exports = { generateCert };
|
||||
/**
|
||||
* Get certificate for a specific domain (dynamic generation)
|
||||
* Used by SNICallback in server.js
|
||||
*/
|
||||
function getCertForDomain(domain) {
|
||||
try {
|
||||
const rootCA = loadRootCA();
|
||||
const leafCert = generateLeafCert(domain, rootCA);
|
||||
return {
|
||||
key: leafCert.key,
|
||||
cert: leafCert.cert
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to generate cert for ${domain}:`, error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { generateCert, getCertForDomain };
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ function checkCertInstalledMac(certPath) {
|
|||
|
||||
function checkCertInstalledWindows(certPath) {
|
||||
return new Promise((resolve) => {
|
||||
// Check Root store for our cert by subject name
|
||||
exec("certutil -store Root daily-cloudcode-pa.googleapis.com", (error) => {
|
||||
// Check Root store for our Root CA by common name
|
||||
exec("certutil -store Root \"9Router MITM Root CA\"", (error) => {
|
||||
resolve(!error);
|
||||
});
|
||||
});
|
||||
|
|
@ -130,7 +130,7 @@ async function uninstallCertMac(sudoPassword, certPath) {
|
|||
}
|
||||
|
||||
async function uninstallCertWindows() {
|
||||
const psCommand = `Start-Process certutil -ArgumentList '-delstore','Root','daily-cloudcode-pa.googleapis.com' -Verb RunAs -Wait -WindowStyle Hidden`;
|
||||
const psCommand = `Start-Process certutil -ArgumentList '-delstore','Root','9Router MITM Root CA' -Verb RunAs -Wait -WindowStyle Hidden`;
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(
|
||||
`powershell -NonInteractive -WindowStyle Hidden -Command "${psCommand}"`,
|
||||
|
|
@ -144,12 +144,12 @@ async function uninstallCertWindows() {
|
|||
}
|
||||
|
||||
function checkCertInstalledLinux() {
|
||||
const certFile = `${LINUX_CERT_DIR}/9router-mitm.crt`;
|
||||
const certFile = `${LINUX_CERT_DIR}/9router-root-ca.crt`;
|
||||
return Promise.resolve(fs.existsSync(certFile));
|
||||
}
|
||||
|
||||
async function installCertLinux(sudoPassword, certPath) {
|
||||
const destFile = `${LINUX_CERT_DIR}/9router-mitm.crt`;
|
||||
const destFile = `${LINUX_CERT_DIR}/9router-root-ca.crt`;
|
||||
// Try update-ca-certificates (Debian/Ubuntu), fallback to update-ca-trust (Fedora/RHEL)
|
||||
const cmd = `cp "${certPath}" "${destFile}" && (update-ca-certificates 2>/dev/null || update-ca-trust 2>/dev/null || true)`;
|
||||
try {
|
||||
|
|
@ -161,7 +161,7 @@ async function installCertLinux(sudoPassword, certPath) {
|
|||
}
|
||||
|
||||
async function uninstallCertLinux(sudoPassword) {
|
||||
const destFile = `${LINUX_CERT_DIR}/9router-mitm.crt`;
|
||||
const destFile = `${LINUX_CERT_DIR}/9router-root-ca.crt`;
|
||||
const cmd = `rm -f "${destFile}" && (update-ca-certificates 2>/dev/null || update-ca-trust 2>/dev/null || true)`;
|
||||
try {
|
||||
await execWithPassword(cmd, sudoPassword);
|
||||
|
|
|
|||
153
src/mitm/cert/rootCA.js
Normal file
153
src/mitm/cert/rootCA.js
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const forge = require("node-forge");
|
||||
const { MITM_DIR } = require("../paths");
|
||||
|
||||
const ROOT_CA_KEY_PATH = path.join(MITM_DIR, "rootCA.key");
|
||||
const ROOT_CA_CERT_PATH = path.join(MITM_DIR, "rootCA.crt");
|
||||
|
||||
/**
|
||||
* Generate Root CA certificate (only once)
|
||||
* This Root CA will sign all dynamic leaf certificates
|
||||
*/
|
||||
async function generateRootCA() {
|
||||
if (fs.existsSync(ROOT_CA_KEY_PATH) && fs.existsSync(ROOT_CA_CERT_PATH)) {
|
||||
console.log("✅ Root CA already exists");
|
||||
return { key: ROOT_CA_KEY_PATH, cert: ROOT_CA_CERT_PATH };
|
||||
}
|
||||
|
||||
if (!fs.existsSync(MITM_DIR)) {
|
||||
fs.mkdirSync(MITM_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
console.log("🔐 Generating Root CA certificate...");
|
||||
|
||||
// Generate RSA key pair
|
||||
const keys = forge.pki.rsa.generateKeyPair(2048);
|
||||
|
||||
// Create Root CA certificate
|
||||
const cert = forge.pki.createCertificate();
|
||||
cert.publicKey = keys.publicKey;
|
||||
cert.serialNumber = "01";
|
||||
cert.validity.notBefore = new Date();
|
||||
cert.validity.notAfter = new Date();
|
||||
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10);
|
||||
|
||||
const attrs = [
|
||||
{ name: "commonName", value: "9Router MITM Root CA" },
|
||||
{ name: "organizationName", value: "9Router" },
|
||||
{ name: "countryName", value: "US" }
|
||||
];
|
||||
|
||||
cert.setSubject(attrs);
|
||||
cert.setIssuer(attrs); // Self-signed
|
||||
|
||||
cert.setExtensions([
|
||||
{
|
||||
name: "basicConstraints",
|
||||
cA: true,
|
||||
critical: true
|
||||
},
|
||||
{
|
||||
name: "keyUsage",
|
||||
keyCertSign: true,
|
||||
cRLSign: true,
|
||||
critical: true
|
||||
},
|
||||
{
|
||||
name: "subjectKeyIdentifier"
|
||||
}
|
||||
]);
|
||||
|
||||
// Self-sign the certificate
|
||||
cert.sign(keys.privateKey, forge.md.sha256.create());
|
||||
|
||||
// Save to disk
|
||||
const privateKeyPem = forge.pki.privateKeyToPem(keys.privateKey);
|
||||
const certPem = forge.pki.certificateToPem(cert);
|
||||
|
||||
fs.writeFileSync(ROOT_CA_KEY_PATH, privateKeyPem);
|
||||
fs.writeFileSync(ROOT_CA_CERT_PATH, certPem);
|
||||
|
||||
console.log("✅ Root CA generated successfully");
|
||||
return { key: ROOT_CA_KEY_PATH, cert: ROOT_CA_CERT_PATH };
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Root CA from disk
|
||||
*/
|
||||
function loadRootCA() {
|
||||
if (!fs.existsSync(ROOT_CA_KEY_PATH) || !fs.existsSync(ROOT_CA_CERT_PATH)) {
|
||||
throw new Error("Root CA not found. Generate it first.");
|
||||
}
|
||||
|
||||
const keyPem = fs.readFileSync(ROOT_CA_KEY_PATH, "utf8");
|
||||
const certPem = fs.readFileSync(ROOT_CA_CERT_PATH, "utf8");
|
||||
|
||||
return {
|
||||
key: forge.pki.privateKeyFromPem(keyPem),
|
||||
cert: forge.pki.certificateFromPem(certPem)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate leaf certificate for a specific domain, signed by Root CA
|
||||
*/
|
||||
function generateLeafCert(domain, rootCA) {
|
||||
// Generate key pair for leaf cert
|
||||
const keys = forge.pki.rsa.generateKeyPair(2048);
|
||||
|
||||
// Create leaf certificate
|
||||
const cert = forge.pki.createCertificate();
|
||||
cert.publicKey = keys.publicKey;
|
||||
cert.serialNumber = Math.floor(Math.random() * 1000000).toString();
|
||||
cert.validity.notBefore = new Date();
|
||||
cert.validity.notAfter = new Date();
|
||||
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
|
||||
|
||||
cert.setSubject([
|
||||
{ name: "commonName", value: domain }
|
||||
]);
|
||||
|
||||
cert.setIssuer(rootCA.cert.subject.attributes);
|
||||
|
||||
cert.setExtensions([
|
||||
{
|
||||
name: "basicConstraints",
|
||||
cA: false
|
||||
},
|
||||
{
|
||||
name: "keyUsage",
|
||||
digitalSignature: true,
|
||||
keyEncipherment: true
|
||||
},
|
||||
{
|
||||
name: "extKeyUsage",
|
||||
serverAuth: true,
|
||||
clientAuth: true
|
||||
},
|
||||
{
|
||||
name: "subjectAltName",
|
||||
altNames: [
|
||||
{ type: 2, value: domain }, // DNS
|
||||
{ type: 2, value: `*.${domain}` } // Wildcard
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
// Sign with Root CA
|
||||
cert.sign(rootCA.key, forge.md.sha256.create());
|
||||
|
||||
return {
|
||||
key: forge.pki.privateKeyToPem(keys.privateKey),
|
||||
cert: forge.pki.certificateToPem(cert)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateRootCA,
|
||||
loadRootCA,
|
||||
generateLeafCert,
|
||||
ROOT_CA_CERT_PATH,
|
||||
ROOT_CA_KEY_PATH
|
||||
};
|
||||
|
|
@ -245,37 +245,6 @@ function pollMitmHealth(timeoutMs, port = MITM_PORT) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check which tools have their domains covered by the installed cert SAN.
|
||||
* Uses built-in crypto.X509Certificate (Node 15.6+).
|
||||
*/
|
||||
function getCertToolCoverage(certPath) {
|
||||
try {
|
||||
const pem = fs.readFileSync(certPath, "utf8");
|
||||
const cert = new crypto.X509Certificate(pem);
|
||||
const san = cert.subjectAltName || "";
|
||||
// Extract all DNS SANs
|
||||
const sans = san.split(",").map(s => s.trim().replace(/^DNS:/, ""));
|
||||
const matchesSan = (domain) => sans.some(s => {
|
||||
if (s === domain) return true;
|
||||
// Wildcard: *.foo.com matches bar.foo.com
|
||||
if (s.startsWith("*.")) {
|
||||
const suffix = s.slice(1); // .foo.com
|
||||
return domain.endsWith(suffix) && !domain.slice(0, -suffix.length).includes(".");
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const { TOOL_HOSTS } = require("./dns/dnsConfig");
|
||||
const coverage = {};
|
||||
for (const [tool, hosts] of Object.entries(TOOL_HOSTS)) {
|
||||
coverage[tool] = hosts.every(matchesSan);
|
||||
}
|
||||
return coverage;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full MITM status including per-tool DNS status
|
||||
*/
|
||||
|
|
@ -298,11 +267,10 @@ async function getMitmStatus() {
|
|||
}
|
||||
|
||||
const dnsStatus = checkAllDNSStatus();
|
||||
const certPath = path.join(MITM_DIR, "server.crt");
|
||||
const certExists = fs.existsSync(certPath);
|
||||
const certCoversTools = certExists ? getCertToolCoverage(certPath) : {};
|
||||
const rootCACertPath = path.join(MITM_DIR, "rootCA.crt");
|
||||
const certExists = fs.existsSync(rootCACertPath);
|
||||
|
||||
return { running, pid, certExists, dnsStatus, certCoversTools };
|
||||
return { running, pid, certExists, dnsStatus };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -352,39 +320,34 @@ async function startServer(apiKey, sudoPassword) {
|
|||
}
|
||||
}
|
||||
|
||||
// Step 1: Generate SSL certificate if not exists or missing domain coverage
|
||||
const certPath = path.join(MITM_DIR, "server.crt");
|
||||
const keyPath = path.join(MITM_DIR, "server.key");
|
||||
let needsRegenerate = false;
|
||||
// Step 1: Auto-migration - Generate Root CA if not exists
|
||||
const rootCACertPath = path.join(MITM_DIR, "rootCA.crt");
|
||||
const rootCAKeyPath = path.join(MITM_DIR, "rootCA.key");
|
||||
|
||||
if (!fs.existsSync(certPath)) {
|
||||
console.log("[MITM] Generating SSL certificate...");
|
||||
needsRegenerate = true;
|
||||
} else {
|
||||
// Check if cert covers all tool domains
|
||||
const coverage = getCertToolCoverage(certPath);
|
||||
const { TOOL_HOSTS } = require("./dns/dnsConfig");
|
||||
const allCovered = Object.keys(TOOL_HOSTS).every(tool => coverage[tool] === true);
|
||||
if (!allCovered) {
|
||||
console.log("[MITM] Certificate missing domain coverage — regenerating...");
|
||||
needsRegenerate = true;
|
||||
try {
|
||||
fs.unlinkSync(certPath);
|
||||
if (fs.existsSync(keyPath)) fs.unlinkSync(keyPath);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
if (needsRegenerate) {
|
||||
if (!fs.existsSync(rootCACertPath) || !fs.existsSync(rootCAKeyPath)) {
|
||||
console.log("[MITM] Generating Root CA certificate (first time or migration)...");
|
||||
await generateCert();
|
||||
}
|
||||
|
||||
// Step 2: Install cert + spawn server
|
||||
// Step 1.5: Auto-install Root CA if not trusted yet
|
||||
const { checkCertInstalled } = require("./cert/install");
|
||||
const rootCATrusted = await checkCertInstalled(rootCACertPath);
|
||||
if (!rootCATrusted) {
|
||||
console.log("[MITM] Installing Root CA to system trust store...");
|
||||
// Use provided password or cached/stored password
|
||||
const password = sudoPassword || getCachedPassword() || await loadEncryptedPassword();
|
||||
if (!password && !IS_WIN) {
|
||||
throw new Error("Sudo password required to install Root CA certificate");
|
||||
}
|
||||
await installCert(password, rootCACertPath);
|
||||
console.log("✅ Root CA installed successfully");
|
||||
}
|
||||
|
||||
// Step 2: Spawn server (Root CA already installed in Step 1.5)
|
||||
if (IS_WIN) {
|
||||
const hostsFile = path.join(process.env.SystemRoot || "C:\\Windows", "System32", "drivers", "etc", "hosts");
|
||||
const flagFile = path.join(os.tmpdir(), `mitm_ready_${Date.now()}.flag`);
|
||||
const psSQ = (s) => s.replace(/'/g, "''");
|
||||
const certPs = psSQ(certPath);
|
||||
const nodePs = psSQ(process.execPath);
|
||||
const serverPs = psSQ(SERVER_PATH);
|
||||
const flagPs = psSQ(flagFile);
|
||||
|
|
@ -393,7 +356,6 @@ async function startServer(apiKey, sudoPassword) {
|
|||
`$conn = Get-NetTCPConnection -LocalPort 443 -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1`,
|
||||
`if ($conn -and $conn.OwningProcess -gt 4) { Stop-Process -Id $conn.OwningProcess -Force -ErrorAction SilentlyContinue }`,
|
||||
`Start-Sleep -Milliseconds 500`,
|
||||
`& certutil -addstore Root '${certPs}' | Out-Null`,
|
||||
`$nodeCmd = 'set ROUTER_API_KEY=${psSQ(apiKey)}&& set NODE_ENV=production&& "${nodePs}" "${serverPs}"'`,
|
||||
`Start-Process cmd -ArgumentList '/c',$nodeCmd -WindowStyle Hidden`,
|
||||
`Start-Sleep -Milliseconds 500`,
|
||||
|
|
@ -429,13 +391,7 @@ async function startServer(apiKey, sudoPassword) {
|
|||
|
||||
if (_updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { });
|
||||
} else {
|
||||
const { checkCertInstalled } = require("./cert/install");
|
||||
const certTrusted = await checkCertInstalled(certPath);
|
||||
if (!certTrusted) {
|
||||
await installCert(sudoPassword, certPath);
|
||||
if (_updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { });
|
||||
}
|
||||
|
||||
// Non-Windows: Root CA already installed in Step 1.5, just spawn server
|
||||
const inlineCmd = `ROUTER_API_KEY='${apiKey}' NODE_ENV='production' '${process.execPath}' '${SERVER_PATH}'`;
|
||||
serverProcess = spawn(
|
||||
"sudo", ["-S", "-E", "sh", "-c", inlineCmd],
|
||||
|
|
@ -583,7 +539,10 @@ async function stopServer(sudoPassword) {
|
|||
async function enableToolDNS(tool, sudoPassword) {
|
||||
const status = await getMitmStatus();
|
||||
if (!status.running) throw new Error("MITM server is not running. Start the server first.");
|
||||
await addDNSEntry(tool, sudoPassword);
|
||||
|
||||
// Use cached password if not provided
|
||||
const password = sudoPassword || getCachedPassword() || await loadEncryptedPassword();
|
||||
await addDNSEntry(tool, password);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
|
|
@ -591,7 +550,9 @@ async function enableToolDNS(tool, sudoPassword) {
|
|||
* Disable DNS for a specific tool
|
||||
*/
|
||||
async function disableToolDNS(tool, sudoPassword) {
|
||||
await removeDNSEntry(tool, sudoPassword);
|
||||
// Use cached password if not provided
|
||||
const password = sudoPassword || getCachedPassword() || await loadEncryptedPassword();
|
||||
await removeDNSEntry(tool, password);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,15 +26,57 @@ if (!API_KEY) {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
const { getCertForDomain } = require("./cert/generate");
|
||||
|
||||
// Certificate cache for performance
|
||||
const certCache = new Map();
|
||||
|
||||
// SNI callback for dynamic certificate generation
|
||||
function sniCallback(servername, cb) {
|
||||
try {
|
||||
// Check cache first
|
||||
if (certCache.has(servername)) {
|
||||
const cached = certCache.get(servername);
|
||||
return cb(null, cached);
|
||||
}
|
||||
|
||||
// Generate new cert for this domain
|
||||
const certData = getCertForDomain(servername);
|
||||
if (!certData) {
|
||||
return cb(new Error(`Failed to generate cert for ${servername}`));
|
||||
}
|
||||
|
||||
// Create secure context
|
||||
const ctx = require("tls").createSecureContext({
|
||||
key: certData.key,
|
||||
cert: certData.cert
|
||||
});
|
||||
|
||||
// Cache it
|
||||
certCache.set(servername, ctx);
|
||||
console.log(`✅ Generated cert for: ${servername}`);
|
||||
|
||||
cb(null, ctx);
|
||||
} catch (error) {
|
||||
console.error(`❌ SNI error for ${servername}:`, error.message);
|
||||
cb(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load Root CA for default context
|
||||
const certDir = MITM_DIR;
|
||||
const rootCAKeyPath = path.join(certDir, "rootCA.key");
|
||||
const rootCACertPath = path.join(certDir, "rootCA.crt");
|
||||
|
||||
let sslOptions;
|
||||
try {
|
||||
sslOptions = {
|
||||
key: fs.readFileSync(path.join(certDir, "server.key")),
|
||||
cert: fs.readFileSync(path.join(certDir, "server.crt"))
|
||||
key: fs.readFileSync(rootCAKeyPath),
|
||||
cert: fs.readFileSync(rootCACertPath),
|
||||
SNICallback: sniCallback
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(`❌ SSL cert not found in ${certDir}: ${e.message}`);
|
||||
console.error(`❌ Root CA not found in ${certDir}: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue