api9nin / src /mitm /cert /install.js
github-actions[bot]
deploy from github actions 2026-06-18
c8ae75d
Raw
History Blame Contribute Delete
9.25 kB
const fs = require("fs");
const crypto = require("crypto");
const { exec } = require("child_process");
const { execWithPassword, isSudoAvailable } = require("../dns/dnsConfig.js");
const { runElevatedPowerShell, quotePs } = require("../winElevated.js");
const { log, err } = require("../logger");
const IS_WIN = process.platform === "win32";
const IS_MAC = process.platform === "darwin";
const LINUX_CERT_PATHS = [
// Debian / Ubuntu
{ dir: "/usr/local/share/ca-certificates", cmd: "update-ca-certificates" },
// Arch Linux / CachyOS / Manjaro
{ dir: "/etc/ca-certificates/trust-source/anchors", cmd: "update-ca-trust" },
// Fedora / RHEL / CentOS
{ dir: "/etc/pki/ca-trust/source/anchors", cmd: "update-ca-trust" },
// openSUSE
{ dir: "/etc/pki/trust/anchors", cmd: "update-ca-certificates" }
];
function getLinuxCertConfig() {
for (const config of LINUX_CERT_PATHS) {
if (fs.existsSync(config.dir)) {
return config;
}
}
// Fallback to Debian default if none exist
return LINUX_CERT_PATHS[0];
}
const ROOT_CA_CN = "9Router MITM Root CA";
// Get SHA1 fingerprint from cert file using Node.js crypto
function getCertFingerprint(certPath) {
const pem = fs.readFileSync(certPath, "utf-8");
const der = Buffer.from(pem.replace(/-----[^-]+-----/g, "").replace(/\s/g, ""), "base64");
return crypto.createHash("sha1").update(der).digest("hex").toUpperCase().match(/.{2}/g).join(":");
}
/**
* Check if certificate is already installed in system store
*/
async function checkCertInstalled(certPath) {
if (IS_WIN) return checkCertInstalledWindows(certPath);
if (IS_MAC) return checkCertInstalledMac(certPath);
return checkCertInstalledLinux();
}
function checkCertInstalledMac(certPath) {
return new Promise((resolve) => {
try {
const fingerprint = getCertFingerprint(certPath).replace(/:/g, "");
// Verify exact cert bytes match β€” same CN with different fingerprint = stale cert
exec(`security find-certificate -a -c "${ROOT_CA_CN}" -Z /Library/Keychains/System.keychain 2>/dev/null`, { windowsHide: true }, (error, stdout) => {
if (error || !stdout) return resolve(false);
const match = new RegExp(`SHA-1 hash:\\s*${fingerprint}`, "i").test(stdout);
if (!match) return resolve(false);
// Cert exists with matching fingerprint β€” confirm trust policy
exec(`security verify-cert -c "${certPath}" -p ssl -k /Library/Keychains/System.keychain 2>/dev/null`, { windowsHide: true }, (err2) => {
resolve(!err2);
});
});
} catch {
resolve(false);
}
});
}
function checkCertInstalledWindows(certPath) {
return new Promise((resolve) => {
// Check by SHA1 fingerprint β€” detects stale cert with same CN but different key
let fingerprint;
try {
fingerprint = getCertFingerprint(certPath).replace(/:/g, "");
} catch {
return resolve(false);
}
exec(`certutil -store Root ${fingerprint}`, { windowsHide: true }, (error) => {
resolve(!error);
});
});
}
/**
* Install SSL certificate to system trust store
*/
async function installCert(sudoPassword, certPath) {
if (!fs.existsSync(certPath)) {
throw new Error(`Certificate file not found: ${certPath}`);
}
const isInstalled = await checkCertInstalled(certPath);
if (isInstalled) {
log("πŸ” Cert: already trusted βœ…");
return;
}
if (IS_WIN) {
await installCertWindows(certPath);
} else if (IS_MAC) {
await installCertMac(sudoPassword, certPath);
} else {
await installCertLinux(sudoPassword, certPath);
}
}
async function installCertMac(sudoPassword, certPath) {
// Remove all old certs with same name first to avoid duplicate/stale cert conflict
const deleteOld = `security delete-certificate -c "9Router MITM Root CA" /Library/Keychains/System.keychain 2>/dev/null || true`;
const install = `security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${certPath}"`;
try {
await execWithPassword(`${deleteOld} && ${install}`, sudoPassword);
log("πŸ” Cert: βœ… installed to system keychain");
} catch (error) {
const msg = error.message?.includes("canceled") ? "User canceled authorization" : "Certificate install failed";
throw new Error(msg);
}
}
async function installCertWindows(certPath) {
// Auto-elevate via UAC popup if not admin (zero popup if already admin).
// Delete any stale cert with same CN before adding to avoid duplicates.
const script = `
certutil -delstore Root ${quotePs(ROOT_CA_CN)} 2>$null | Out-Null
$exit = & certutil -addstore Root ${quotePs(certPath)} 2>&1
if ($LASTEXITCODE -ne 0) { throw "certutil exit $LASTEXITCODE" }
`;
try {
await runElevatedPowerShell(script);
log("πŸ” Cert: βœ… installed to Windows Root store");
} catch (e) {
throw new Error(`Failed to install certificate: ${e.message}`);
}
}
/**
* Uninstall SSL certificate from system store
*/
async function uninstallCert(sudoPassword, certPath) {
const isInstalled = await checkCertInstalled(certPath);
if (!isInstalled) {
log("πŸ” Cert: not found in system store");
return;
}
if (IS_WIN) {
await uninstallCertWindows();
} else if (IS_MAC) {
await uninstallCertMac(sudoPassword, certPath);
} else {
await uninstallCertLinux(sudoPassword);
}
}
async function uninstallCertMac(sudoPassword, certPath) {
const fingerprint = getCertFingerprint(certPath).replace(/:/g, "");
const command = `security delete-certificate -Z "${fingerprint}" /Library/Keychains/System.keychain`;
try {
await execWithPassword(command, sudoPassword);
log("πŸ” Cert: βœ… uninstalled from system keychain");
} catch (err) {
throw new Error("Failed to uninstall certificate");
}
}
async function uninstallCertWindows() {
// Auto-elevate via UAC popup if not admin
const script = `certutil -delstore Root ${quotePs(ROOT_CA_CN)}`;
try {
await runElevatedPowerShell(script);
log("πŸ” Cert: βœ… uninstalled from Windows Root store");
} catch (e) {
throw new Error(`Failed to uninstall certificate: ${e.message}`);
}
}
function checkCertInstalledLinux() {
const config = getLinuxCertConfig();
const certFile = `${config.dir}/9router-root-ca.crt`;
return Promise.resolve(fs.existsSync(certFile));
}
async function updateNssDatabases(certPath, action = 'add') {
const certName = "9Router MITM Root CA";
const script = `
if ! command -v certutil &> /dev/null; then
exit 0
fi
DIRS="$HOME/.pki/nssdb $HOME/snap/chromium/current/.pki/nssdb"
if [ -d "$HOME/.mozilla/firefox" ]; then
for profile in "$HOME"/.mozilla/firefox/*/; do
if [ -f "\${profile}cert9.db" ] || [ -f "\${profile}cert8.db" ]; then
DIRS="$DIRS $profile"
fi
done
fi
if [ -d "$HOME/snap/firefox/common/.mozilla/firefox" ]; then
for profile in "$HOME"/snap/firefox/common/.mozilla/firefox/*/; do
if [ -f "\${profile}cert9.db" ] || [ -f "\${profile}cert8.db" ]; then
DIRS="$DIRS $profile"
fi
done
fi
for db in $DIRS; do
if [ -d "$db" ]; then
if [ "${action}" = "add" ]; then
certutil -d sql:"$db" -A -t "C,," -n "${certName}" -i "${certPath}" 2>/dev/null || \\
certutil -d "$db" -A -t "C,," -n "${certName}" -i "${certPath}" 2>/dev/null || true
else
certutil -d sql:"$db" -D -n "${certName}" 2>/dev/null || \\
certutil -d "$db" -D -n "${certName}" 2>/dev/null || true
fi
fi
done
`;
return new Promise((resolve) => {
exec(script, { shell: "/bin/bash" }, () => resolve());
});
}
async function installCertLinux(sudoPassword, certPath) {
if (!isSudoAvailable()) {
log(`πŸ” Cert: cannot install to system store without sudo β€” trust this file on clients: ${certPath}`);
// Still try to update user NSS DBs even if no sudo!
await updateNssDatabases(certPath, 'add');
return;
}
const config = getLinuxCertConfig();
const destFile = `${config.dir}/9router-root-ca.crt`;
// Copy to the discovered directory and execute the specific update command
const cmd = `cp "${certPath}" "${destFile}" && (${config.cmd} 2>/dev/null || true)`;
try {
await execWithPassword(cmd, sudoPassword);
await updateNssDatabases(certPath, 'add');
log(`πŸ” Cert: βœ… installed to Linux trust store (${config.dir}) and user browser databases`);
} catch (error) {
throw new Error(`Certificate install failed: ${error.message}`);
}
}
async function uninstallCertLinux(sudoPassword) {
// Always try to uninstall from user DBs even without sudo
await updateNssDatabases(null, 'delete');
if (!isSudoAvailable()) {
return;
}
const config = getLinuxCertConfig();
const destFile = `${config.dir}/9router-root-ca.crt`;
const cmd = `rm -f "${destFile}" && (${config.cmd} 2>/dev/null || true)`;
try {
await execWithPassword(cmd, sudoPassword);
log("πŸ” Cert: βœ… uninstalled from Linux trust store and user browser databases");
} catch (error) {
throw new Error("Failed to uninstall certificate");
}
}
module.exports = { installCert, uninstallCert, checkCertInstalled };