File size: 9,252 Bytes
88c4c60 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 | 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 };
|