feat: add unistall CA cert feature

This commit is contained in:
mahan-bst
2026-04-23 11:03:27 +03:30
parent 57738ec5c8
commit e0961ed2db
5 changed files with 224 additions and 2 deletions
+3
View File
@@ -209,6 +209,8 @@ Firefox uses its own certificate store, so even after OS-level install you need
> **Auto-install on startup:** When running in `apps_script` mode the proxy will automatically detect if the CA is not yet trusted and attempt to install it for you. If it succeeds you'll see a confirmation in the log; if it fails (e.g. needs administrator rights) it will print instructions. You can also run `python main.py --install-cert` at any time to (re-)install the certificate. > **Auto-install on startup:** When running in `apps_script` mode the proxy will automatically detect if the CA is not yet trusted and attempt to install it for you. If it succeeds you'll see a confirmation in the log; if it fails (e.g. needs administrator rights) it will print instructions. You can also run `python main.py --install-cert` at any time to (re-)install the certificate.
> **Uninstalling:** To remove the certificate from your system's trust stores, run `python main.py --uninstall-cert` or use `python start.bat --uninstall-cert` on Windows. This removes the certificate from all system trust stores and Firefox profiles.
> ⚠️ **Security note:** This certificate only works locally on your machine. Don't share the `ca/` folder with anyone. If you want to start fresh, delete the `ca/` folder and the tool will generate a new one. > ⚠️ **Security note:** This certificate only works locally on your machine. Don't share the `ca/` folder with anyone. If you want to start fresh, delete the `ca/` folder and the tool will generate a new one.
--- ---
@@ -312,6 +314,7 @@ python3 main.py --disable-socks5 # Disable SOCKS5 listener
python3 main.py --log-level DEBUG # Show detailed logs python3 main.py --log-level DEBUG # Show detailed logs
python3 main.py -c /path/to/config.json # Use a different config file python3 main.py -c /path/to/config.json # Use a different config file
python3 main.py --install-cert # Install MITM CA certificate and exit python3 main.py --install-cert # Install MITM CA certificate and exit
python3 main.py --uninstall-cert # Remove MITM CA certificate and exit
python3 main.py --no-cert-check # Skip automatic CA install check on startup python3 main.py --no-cert-check # Skip automatic CA install check on startup
``` ```
+1
View File
@@ -260,6 +260,7 @@ python3 main.py --disable-socks5
python3 main.py --log-level DEBUG python3 main.py --log-level DEBUG
python3 main.py -c /path/to/config.json python3 main.py -c /path/to/config.json
python3 main.py --install-cert # نصب گواهی CA و خروج python3 main.py --install-cert # نصب گواهی CA و خروج
python3 main.py --uninstall-cert # حذف گراهی CA و خروج
python3 main.py --no-cert-check # رد شدن از بررسی خودکار گواهی python3 main.py --no-cert-check # رد شدن از بررسی خودکار گواهی
``` ```
+18 -1
View File
@@ -20,7 +20,7 @@ _SRC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "src")
if _SRC_DIR not in sys.path: if _SRC_DIR not in sys.path:
sys.path.insert(0, _SRC_DIR) sys.path.insert(0, _SRC_DIR)
from cert_installer import install_ca, is_ca_trusted from cert_installer import install_ca, uninstall_ca, is_ca_trusted
from constants import __version__ from constants import __version__
from lan_utils import log_lan_access from lan_utils import log_lan_access
from logging_utils import configure as configure_logging, print_banner from logging_utils import configure as configure_logging, print_banner
@@ -87,6 +87,11 @@ def parse_args():
action="store_true", action="store_true",
help="Install the MITM CA certificate as a trusted root and exit.", help="Install the MITM CA certificate as a trusted root and exit.",
) )
parser.add_argument(
"--uninstall-cert",
action="store_true",
help="Remove the MITM CA certificate from trusted roots and exit.",
)
parser.add_argument( parser.add_argument(
"--no-cert-check", "--no-cert-check",
action="store_true", action="store_true",
@@ -192,6 +197,18 @@ def main():
ok = install_ca(CA_CERT_FILE) ok = install_ca(CA_CERT_FILE)
sys.exit(0 if ok else 1) sys.exit(0 if ok else 1)
# ── Certificate uninstallation ───────────────────────────────────────────
if args.uninstall_cert:
setup_logging("INFO")
_log = logging.getLogger("Main")
_log.info("Removing CA certificate…")
ok = uninstall_ca(CA_CERT_FILE)
if ok:
_log.info("CA certificate removed successfully.")
else:
_log.warning("CA certificate removal may have failed. Check logs above.")
sys.exit(0 if ok else 1)
setup_logging(config.get("log_level", "INFO")) setup_logging(config.get("log_level", "INFO"))
log = logging.getLogger("Main") log = logging.getLogger("Main")
+192
View File
@@ -358,6 +358,172 @@ def _install_firefox(cert_path: str, cert_name: str):
log.warning("Firefox profile %s: %s", os.path.basename(profile), exc) log.warning("Firefox profile %s: %s", os.path.basename(profile), exc)
def _uninstall_firefox(cert_name: str):
"""Remove certificate from all detected Firefox profile NSS databases."""
if not _has_cmd("certutil"):
log.debug("NSS certutil not found — skipping Firefox uninstall.")
return
profile_dirs: list[str] = []
system = platform.system()
if system == "Windows":
appdata = os.environ.get("APPDATA", "")
profile_dirs += glob.glob(os.path.join(appdata, r"Mozilla\Firefox\Profiles\*"))
elif system == "Darwin":
profile_dirs += glob.glob(os.path.expanduser("~/Library/Application Support/Firefox/Profiles/*"))
else:
profile_dirs += glob.glob(os.path.expanduser("~/.mozilla/firefox/*.default*"))
profile_dirs += glob.glob(os.path.expanduser("~/.mozilla/firefox/*.release*"))
if not profile_dirs:
log.debug("No Firefox profiles found.")
return
for profile in profile_dirs:
db = f"sql:{profile}" if os.path.exists(os.path.join(profile, "cert9.db")) else f"dbm:{profile}"
try:
_run(["certutil", "-D", "-n", cert_name, "-d", db], check=False)
log.info("Removed from Firefox profile: %s", os.path.basename(profile))
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
log.debug("Firefox profile %s: %s", os.path.basename(profile), exc)
# ─────────────────────────────────────────────────────────────────────────────
# Uninstall functions
# ─────────────────────────────────────────────────────────────────────────────
def _uninstall_windows(cert_name: str) -> bool:
"""Remove certificate from the Windows Trusted Root store."""
# Try per-user store first (no admin required)
try:
_run(["certutil", "-delstore", "-user", "Root", cert_name])
log.info("Certificate removed from Windows user Trusted Root store.")
return True
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
log.warning("certutil user store removal failed: %s", exc)
# Try system store (requires admin)
try:
_run(["certutil", "-delstore", "Root", cert_name])
log.info("Certificate removed from Windows system Trusted Root store.")
return True
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
log.warning("certutil system store removal failed: %s", exc)
# Fallback: use PowerShell
try:
ps_cmd = (
f"Remove-Item -Path Cert:\\CurrentUser\\Root\\{cert_name} -Force -ErrorAction SilentlyContinue"
)
_run(["powershell", "-NoProfile", "-Command", ps_cmd], check=False)
log.info("Attempted certificate removal via PowerShell.")
return True
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
log.error("PowerShell removal failed: %s", exc)
return False
def _uninstall_macos(cert_name: str) -> bool:
"""Remove certificate from the macOS keychains."""
login_keychain = os.path.expanduser("~/Library/Keychains/login.keychain-db")
if not os.path.exists(login_keychain):
login_keychain = os.path.expanduser("~/Library/Keychains/login.keychain")
try:
_run([
"security", "delete-certificate",
"-c", cert_name,
login_keychain,
], check=False)
log.info("Certificate removed from macOS login keychain.")
return True
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
log.warning("login keychain removal failed: %s", exc)
# Try system keychain (needs sudo)
try:
_run([
"sudo", "security", "delete-certificate",
"-c", cert_name,
"/Library/Keychains/System.keychain",
], check=False)
log.info("Certificate removed from macOS system keychain.")
return True
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
log.debug("System keychain removal failed: %s", exc)
return False
def _uninstall_linux(cert_path: str, cert_name: str) -> bool:
"""Remove certificate from Linux trust stores."""
distro = _detect_linux_distro()
log.info("Detected Linux distro family: %s", distro)
removed = False
if distro == "debian":
dest_file = f"/usr/local/share/ca-certificates/{cert_name.replace(' ', '_')}.crt"
try:
if os.path.exists(dest_file):
os.remove(dest_file)
_run(["update-ca-certificates"])
log.info("Certificate removed via update-ca-certificates.")
removed = True
except (OSError, subprocess.CalledProcessError) as exc:
log.warning("Debian removal failed (needs sudo?): %s", exc)
try:
_run(["sudo", "rm", "-f", dest_file])
_run(["sudo", "update-ca-certificates"])
log.info("Certificate removed via sudo update-ca-certificates.")
removed = True
except (subprocess.CalledProcessError, FileNotFoundError) as exc2:
log.warning("sudo Debian removal failed: %s", exc2)
elif distro == "rhel":
dest_file = f"/etc/pki/ca-trust/source/anchors/{cert_name.replace(' ', '_')}.crt"
try:
if os.path.exists(dest_file):
os.remove(dest_file)
_run(["update-ca-trust", "extract"])
log.info("Certificate removed via update-ca-trust.")
removed = True
except (OSError, subprocess.CalledProcessError) as exc:
log.warning("RHEL removal failed (needs sudo?): %s", exc)
try:
_run(["sudo", "rm", "-f", dest_file])
_run(["sudo", "update-ca-trust", "extract"])
log.info("Certificate removed via sudo update-ca-trust.")
removed = True
except (subprocess.CalledProcessError, FileNotFoundError) as exc2:
log.warning("sudo RHEL removal failed: %s", exc2)
elif distro == "arch":
dest_file = f"/etc/ca-certificates/trust-source/anchors/{cert_name.replace(' ', '_')}.crt"
try:
if os.path.exists(dest_file):
os.remove(dest_file)
_run(["trust", "extract-compat"])
log.info("Certificate removed via trust extract-compat.")
removed = True
except (OSError, subprocess.CalledProcessError) as exc:
log.warning("Arch removal failed (needs sudo?): %s", exc)
try:
_run(["sudo", "rm", "-f", dest_file])
_run(["sudo", "trust", "extract-compat"])
log.info("Certificate removed via sudo trust extract-compat.")
removed = True
except (subprocess.CalledProcessError, FileNotFoundError) as exc2:
log.warning("sudo Arch removal failed: %s", exc2)
else:
log.warning("Unknown Linux distro. Manually remove %s from trusted CAs.", cert_name)
return removed
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Public API # Public API
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
@@ -403,3 +569,29 @@ def install_ca(cert_path: str, cert_name: str = CERT_NAME) -> bool:
_install_firefox(cert_path, cert_name) _install_firefox(cert_path, cert_name)
return ok return ok
def uninstall_ca(cert_path: str, cert_name: str = CERT_NAME) -> bool:
"""
Remove *cert_name* from the system's trusted root CAs on the current platform.
Also attempts Firefox NSS removal.
Returns True if the system store removal succeeded.
"""
system = platform.system()
log.info("Removing CA certificate from %s", system)
if system == "Windows":
ok = _uninstall_windows(cert_name)
elif system == "Darwin":
ok = _uninstall_macos(cert_name)
elif system == "Linux":
ok = _uninstall_linux(cert_path, cert_name)
else:
log.error("Unsupported platform: %s", system)
return False
# Best-effort Firefox uninstall on all platforms
_uninstall_firefox(cert_name)
return ok
+10 -1
View File
@@ -4,7 +4,8 @@ cd /d "%~dp0"
REM -------- MasterHttpRelayVPN one-click launcher (Windows) -------- REM -------- MasterHttpRelayVPN one-click launcher (Windows) --------
REM Creates a local virtualenv, installs deps, runs the setup wizard REM Creates a local virtualenv, installs deps, runs the setup wizard
REM if needed, then starts the proxy. REM if needed, then starts the proxy. Also checks and installs CA cert
REM if not already trusted.
set "VENV_DIR=.venv" set "VENV_DIR=.venv"
set "PY=" set "PY="
@@ -63,6 +64,14 @@ if not exist "config.json" (
) )
) )
REM -------- Check for uninstall flag --------
if "%~1"=="--uninstall-cert" (
echo [*] Uninstalling CA certificate ...
"%VPY%" main.py --uninstall-cert
exit /b %errorlevel%
)
echo. echo.
echo [*] Starting MasterHttpRelayVPN ... echo [*] Starting MasterHttpRelayVPN ...
echo. echo.