From 44bf3b90c29dffd23ba67b84b1f84961eb51ad73 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Wed, 16 Sep 2026 10:51:07 +0000 Subject: [PATCH] Pit https: import trust into the NSS database the Flatpak engine actually reads Still an invalid certificate on bonita after v3.12.0. The launcher runs the Flathub ungoogled-chromium when it is the newest engine, and that Flatpak is sandboxed with --persist=.pki: inside it ~/.pki is ~/.var/app/io.github.ungoogled_software.ungoogled_chromium/.pki, so the helper's import into the real ~/.pki/nssdb never reached the browser. The launcher's own Local CA sync had the same blind spot. The helper now imports each leaf into every database a Chromium could read: ~/.pki/nssdb, the database the launcher names for the engine it started (TRON_PIT_NSSDB_EXTRA, created if missing), and any ~/.var/app/*chromium*/.pki/nssdb. The launcher passes the Flatpak engine's database to the helper and its sync writes both. /pit/status lists the databases. Helper 3.4.1. Co-Authored-By: Claude Fable 5.1 --- apps/desktop/launcher/tron-tor-helper | 70 ++++++++++++++++++++++----- apps/desktop/launcher/tronbrowser | 21 ++++++-- docs/moshpit-pit-toggle.md | 10 +++- 3 files changed, 84 insertions(+), 17 deletions(-) diff --git a/apps/desktop/launcher/tron-tor-helper b/apps/desktop/launcher/tron-tor-helper index 1f4acd0..ffb3d3d 100755 --- a/apps/desktop/launcher/tron-tor-helper +++ b/apps/desktop/launcher/tron-tor-helper @@ -54,7 +54,7 @@ BUNDLED_DIR = os.environ.get("TRON_TOR_BIN_DIR", "") PIDFILE = os.environ.get("TRON_TOR_PIDFILE", "") # Bumped whenever the helper protocol/behaviour changes; the launcher kills a # stale helper so the current version always runs. -HELPER_VERSION = "3.4.0" +HELPER_VERSION = "3.4.1" _lock = threading.Lock() _proc = None # the running tor subprocess (or None) _ready = False # True once tor reported Bootstrapped 100% @@ -347,6 +347,12 @@ def pit_resolve(name): # handshake that follows already finds the certificate trusted. PIT_REGISTRY = os.environ.get("TRON_PIT_REGISTRY", "https://pit.moshcode.sh").rstrip("/") PIT_NSSDB = os.environ.get("TRON_PIT_NSSDB", os.path.expanduser("~/.pki/nssdb")) +# The launcher names the database of the engine it actually started (colon +# separated). A Flatpak Chromium is sandboxed with `--persist=.pki`: inside it, +# ~/.pki IS ~/.var/app//.pki, so an import into the real ~/.pki/nssdb is +# invisible to it. Every Chromium-looking Flatpak database that exists is +# covered as well, so the import lands wherever the browser will look. +PIT_NSSDB_EXTRA = os.environ.get("TRON_PIT_NSSDB_EXTRA", "") PIT_CERT_DIR = os.environ.get("TRON_PIT_CERT_DIR", os.path.expanduser("~/.tronbrowser/pit-certs")) _trust_lock = threading.Lock() _trust_seen = {} # name -> (ok, why); retried after a failure only once the pit restarts @@ -435,14 +441,50 @@ def trust_available(): return {"available": False, "why": "unsupported-platform"} if not shutil.which("certutil"): return {"available": False, "why": "no-certutil"} - return {"available": True, "why": "certutil"} + return {"available": True, "why": "certutil", "nssdbs": pit_nssdbs()} + + +def pit_nssdbs(): + """Every NSS database a Chromium on this machine might read, primary first.""" + dbs = [PIT_NSSDB] + [d for d in PIT_NSSDB_EXTRA.split(":") if d] + for cand in sorted(glob.glob(os.path.expanduser("~/.var/app/*/.pki/nssdb"))): + app = os.path.basename(os.path.dirname(os.path.dirname(cand))) + if "chromium" in app.lower(): + dbs.append(cand) + seen, out = set(), [] + for d in dbs: + d = os.path.abspath(os.path.expanduser(d)) + if d not in seen: + seen.add(d) + out.append(d) + return out -def _certutil(*args): - return subprocess.run(["certutil", "-d", "sql:" + PIT_NSSDB] + list(args), +def _certutil(db, *args): + return subprocess.run(["certutil", "-d", "sql:" + db] + list(args), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=20) +def _db_ready(db): + """Create the database if it is missing, like the launcher does. False on failure.""" + if os.path.exists(os.path.join(db, "cert9.db")): + return True + try: + os.makedirs(db, mode=0o700, exist_ok=True) + except OSError: + return False + return _certutil(db, "-N", "--empty-password").returncode == 0 + + +def _import_leaf(db, nick, cert_file): + """'already' | 'trusted' | 'failed' for one database.""" + if not _db_ready(db): + return "failed" + if _certutil(db, "-L", "-n", nick).returncode == 0: + return "already" + return "trusted" if _certutil(db, "-A", "-t", "P,,", "-n", nick, "-i", cert_file).returncode == 0 else "failed" + + def ensure_leaf_trust(name, ip): """Make the browser trust what `name` serves on 443, if the registry vouches for it. Returns (ok, why); never raises. Runs once per name per pit run.""" @@ -463,11 +505,9 @@ def _ensure_leaf_trust(name, ip): if not name or "." not in name: return False, "bad-name" nick = "moshpit %s" % name - if not os.path.exists(os.path.join(PIT_NSSDB, "cert9.db")): - os.makedirs(PIT_NSSDB, mode=0o700, exist_ok=True) - if _certutil("-N", "--empty-password").returncode != 0: - return False, "nssdb-create-failed" - if _certutil("-L", "-n", nick).returncode == 0: + dbs = pit_nssdbs() + missing = [db for db in dbs if not (_db_ready(db) and _certutil(db, "-L", "-n", nick).returncode == 0)] + if not missing: return True, "already-trusted" try: der = served_certificate(ip, name) @@ -496,11 +536,15 @@ def _ensure_leaf_trust(name, ip): f.write(ssl.DER_cert_to_PEM_cert(der)) except OSError as exc: return False, "write-failed: %s" % exc - res = _certutil("-A", "-t", "P,,", "-n", nick, "-i", cert_file) - if res.returncode != 0: - log("pit: https for %s: certutil failed: %s" % (name, res.stdout.strip())) + done, failed = [], [] + for db in missing: + (done if _import_leaf(db, nick, cert_file) != "failed" else failed).append(db) + if failed: + log("pit: https for %s: certutil could not write %s" % (name, ", ".join(failed))) + if not done and failed: return False, "certutil-failed" - log("pit: https for %s: trusted its certificate (pin %s matches the registry) in %s" % (name, pin, PIT_NSSDB)) + log("pit: https for %s: trusted its certificate (pin %s matches the registry) in %s" + % (name, pin, ", ".join(done))) return True, "trusted" diff --git a/apps/desktop/launcher/tronbrowser b/apps/desktop/launcher/tronbrowser index 784e66a..08bf349 100755 --- a/apps/desktop/launcher/tronbrowser +++ b/apps/desktop/launcher/tronbrowser @@ -175,7 +175,7 @@ if [ "$TOR" != "1" ]; then # running helper isn't this version — otherwise leave a healthy current # helper alone (don't drop an active Tor session). All backgrounded so the # kill+settle never holds up the browser launch. - HELPER_VERSION=3.4.0 + HELPER_VERSION=3.4.1 ( _pf="$DATA/tor-helper.pid" _rv="$(curl -fsS --max-time 1 http://127.0.0.1:9061/status 2>/dev/null | sed -n 's/.*"version"[^"]*"\([^"]*\)".*/\1/p')" @@ -187,7 +187,14 @@ if [ "$TOR" != "1" ]; then lsof -ti tcp:9061 2>/dev/null | while read -r _p; do kill "$_p" 2>/dev/null || true; done fi sleep 1 # let the control port free up before re-binding + # A Flatpak engine reads ~/.var/app//.pki/nssdb (its --persist=.pki), + # so the helper's per-name trust must land there, not in ~/.pki/nssdb. + _pit_db_extra="" + if [ "$BROWSER" = "flatpak" ] && [ -n "$FLATPAK_APP" ]; then + _pit_db_extra="$HOME/.var/app/$FLATPAK_APP/.pki/nssdb" + fi exec env TRON_TOR_DATA="$DATA/tor" TRON_TOR_BIN_DIR="$DIR" TRON_TOR_PIDFILE="$_pf" \ + TRON_PIT_NSSDB_EXTRA="$_pit_db_extra" \ python3 "$DIR/tron-tor-helper" fi ) >>"$DATA/tor-helper.log" 2>&1 & @@ -557,7 +564,14 @@ sync_moshpit_trust() { if [ "$(uname -s)" != "Linux" ]; then return 0; fi if [ "${TRONBROWSER_NO_MOSHPIT_TRUST:-0}" = "1" ]; then return 0; fi - _nssdb="$HOME/.pki/nssdb" + # A Flatpak engine is sandboxed with --persist=.pki: inside it, ~/.pki is + # ~/.var/app//.pki, so that database is the one it actually reads and + # an import into the real ~/.pki/nssdb never reaches it. Write both. + _flatdb="" + if [ "$BROWSER" = "flatpak" ] && [ -n "$FLATPAK_APP" ]; then + _flatdb="$HOME/.var/app/$FLATPAK_APP/.pki/nssdb" + fi + for _nssdb in "$HOME/.pki/nssdb" ${_flatdb:+"$_flatdb"}; do _ready=0 # Each word below is a literal path or a glob result, so this stays @@ -601,9 +615,10 @@ sync_moshpit_trust() { fi if certutil -d "sql:$_nssdb" -A -t "$_flag" -n "$_nick" -i "$_cert" >/dev/null 2>&1; then - echo "TronBrowser: trusted $_nick for Moshpit HTTPS." >&2 + echo "TronBrowser: trusted $_nick for Moshpit HTTPS ($_nssdb)." >&2 fi done + done return 0 } diff --git a/docs/moshpit-pit-toggle.md b/docs/moshpit-pit-toggle.md index 197a736..fea1596 100644 --- a/docs/moshpit-pit-toggle.md +++ b/docs/moshpit-pit-toggle.md @@ -1,6 +1,6 @@ # 🤘 Pit toggle — Moshpit names for one browser session -**Status:** shipped with the AI-sidebar extension + `tron-tor-helper` 3.4.0 +**Status:** shipped with the AI-sidebar extension + `tron-tor-helper` 3.4.1 **Owner:** desktop (`apps/desktop`) **Scope:** resolve Moshpit names in the running browser with one click. Not a replacement for `moshcode dns enable`, which does it for the whole machine. @@ -79,6 +79,14 @@ with root. The pit toggle does the no-root equivalent for this browser: 4. All of this happens before the SOCKS reply, so the browser's TLS handshake that follows already finds the certificate trusted. +The import goes into every database the engine might read: `~/.pki/nssdb`, +the database the launcher names for the engine it started, and any +`~/.var/app/*chromium*/.pki/nssdb`. That last part matters: the Flathub +ungoogled-chromium is sandboxed with `--persist=.pki`, so inside it `~/.pki` +is `~/.var/app/io.github.ungoogled_software.ungoogled_chromium/.pki`, and an +import into the real `~/.pki/nssdb` never reaches it (the launcher's Local CA +sync had the same blind spot and now writes both). + Linux only for now (Chromium on macOS reads the keychain, which needs an interactive prompt), and it needs `certutil` (Debian/Ubuntu `libnss3-tools`, Fedora `nss-tools`, Arch `nss`); `install.sh` installs it on machines that have