feat(mitm): implement dynamic linux cert resolution and NSS db injection (#1010)
- Replaced hardcoded LINUX_CERT_DIR with dynamic filesystem probing to support Debian, Arch, Fedora, and openSUSE system trust stores. - Added updateNssDatabases helper to seamlessly inject root certificates directly into browser NSS databases (e.g., ~/.pki/nssdb, ~/.mozilla/firefox). - Supported standard and snap-based Chrome/Chromium and Firefox installations. - Made browser cert injection resilient, executing under the current user to prevent file ownership issues, and safely falling back if certutil is absent.
This commit is contained in:
parent
80a2bfcfd7
commit
76f3d4b74e
1 changed files with 87 additions and 10 deletions
|
|
@ -7,7 +7,26 @@ const { log, err } = require("../logger");
|
|||
|
||||
const IS_WIN = process.platform === "win32";
|
||||
const IS_MAC = process.platform === "darwin";
|
||||
const LINUX_CERT_DIR = "/usr/local/share/ca-certificates";
|
||||
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
|
||||
|
|
@ -153,35 +172,93 @@ async function uninstallCertWindows() {
|
|||
}
|
||||
|
||||
function checkCertInstalledLinux() {
|
||||
const certFile = `${LINUX_CERT_DIR}/9router-root-ca.crt`;
|
||||
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 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)`;
|
||||
|
||||
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);
|
||||
log("🔐 Cert: ✅ installed to Linux trust store");
|
||||
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");
|
||||
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 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)`;
|
||||
|
||||
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");
|
||||
log("🔐 Cert: ✅ uninstalled from Linux trust store and user browser databases");
|
||||
} catch (error) {
|
||||
throw new Error("Failed to uninstall certificate");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue