Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,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.

> **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.

---
Expand Down Expand Up @@ -344,6 +346,7 @@ python3 main.py --disable-socks5 # Disable SOCKS5 listener
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 --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 --scan # Scan Google IPs and find the fastest one
```
Expand Down
1 change: 1 addition & 0 deletions README_FA.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ python3 main.py --disable-socks5
python3 main.py --log-level DEBUG
python3 main.py -c /path/to/config.json
python3 main.py --install-cert # نصب گواهی CA و خروج
python3 main.py --uninstall-cert # حذف گراهی CA و خروج
python3 main.py --no-cert-check # رد شدن از بررسی خودکار گواهی
python3 main.py --scan # اسکن IP های Google و یافتن سریع‌ترین
```
Expand Down
17 changes: 16 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
if _SRC_DIR not in sys.path:
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 lan_utils import log_lan_access
from google_ip_scanner import scan_sync
Expand Down Expand Up @@ -88,6 +88,11 @@ def parse_args():
action="store_true",
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(
"--no-cert-check",
action="store_true",
Expand Down Expand Up @@ -198,6 +203,16 @@ def main():
ok = install_ca(CA_CERT_FILE)
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.")
# ── Google IP Scanner ──────────────────────────────────────────────────
if args.scan:
setup_logging("INFO")
Expand Down
192 changes: 192 additions & 0 deletions src/cert_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,172 @@ def _install_firefox(cert_path: str, cert_name: str):
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
# ─────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -403,3 +569,29 @@ def install_ca(cert_path: str, cert_name: str = CERT_NAME) -> bool:
_install_firefox(cert_path, cert_name)

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
11 changes: 10 additions & 1 deletion start.bat
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ cd /d "%~dp0"

REM -------- MasterHttpRelayVPN one-click launcher (Windows) --------
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 "PY="
Expand Down Expand Up @@ -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 [*] Starting MasterHttpRelayVPN ...
echo.
Expand Down