diff --git a/apps/desktop/extensions/ai-sidebar/sidepanel.js b/apps/desktop/extensions/ai-sidebar/sidepanel.js
index bd2fdbc..c898a12 100644
--- a/apps/desktop/extensions/ai-sidebar/sidepanel.js
+++ b/apps/desktop/extensions/ai-sidebar/sidepanel.js
@@ -320,8 +320,10 @@ async function togglePit() {
const trust = res && res.trust;
const httpsTip = !trust
? ''
- : trust.available
- ? 'https:// on a pit name is trusted per name on first use, when the registry publishes its pin.'
+ : trust.available && trust.relaunch
+ ? 'https:// on a pit name: the first visit records its key (when the registry publishes a matching pin), and it loads after the next TronBrowser restart — this engine only takes pins at start.'
+ : trust.available
+ ? 'https:// on a pit name is trusted per name on first use, when the registry publishes its pin.'
: trust.why === 'no-certutil'
? 'https:// on a pit name will warn until certutil is installed (Debian/Ubuntu: libnss3-tools, Fedora: nss-tools, Arch: nss).'
: 'https:// on a pit name will warn on this platform; run moshcode dns enable for the certificate.';
diff --git a/apps/desktop/launcher/tron-tor-helper b/apps/desktop/launcher/tron-tor-helper
index ffb3d3d..f79e72c 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.1"
+HELPER_VERSION = "3.4.2"
_lock = threading.Lock()
_proc = None # the running tor subprocess (or None)
_ready = False # True once tor reported Bootstrapped 100%
@@ -347,13 +347,28 @@ 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.
+# Chromium opens ONE of two databases per home: the legacy ~/.pki/nssdb, or
+# since M146 ${XDG_DATA_HOME:-~/.local/share}/pki/nssdb — and which one a given
+# build picks has changed between versions (bonita's Flatpak 152 read the XDG
+# one while ~/.pki/nssdb existed beside it). A Flatpak Chromium adds a twist:
+# it is sandboxed with `--persist=.pki` and XDG_DATA_HOME=~/.var/app//data,
+# so its two candidates are ~/.var/app//.pki/nssdb and
+# ~/.var/app//data/pki/nssdb, and the real ~/.pki is invisible to it.
+# Rule: for each home, write every candidate that already exists; create the
+# legacy one only when neither does (creating ~/.pki/nssdb next to a populated
+# XDG database would flip a newer Chromium onto an empty store). The launcher
+# names the engine's own candidates in TRON_PIT_NSSDB_EXTRA (colon separated).
PIT_NSSDB_EXTRA = os.environ.get("TRON_PIT_NSSDB_EXTRA", "")
+# What the launcher started: "flatpak" or "native". The Flathub ungoogled-chromium
+# ignores NSS user trust (verified on bonita: the leaf sat in the very database
+# strace showed it opening, and Chromium still said "No matching issuer found"),
+# but it honours --ignore-certificate-errors-spki-list. So every pin this helper
+# accepts is also written to PIT_PINS_FILE, and for a Flatpak engine the launcher
+# passes those pins on the command line at the next start. A name first trusted
+# mid-session therefore needs one relaunch on a Flatpak engine; the sidebar says so.
+PIT_ENGINE = os.environ.get("TRON_PIT_ENGINE", "native")
PIT_CERT_DIR = os.environ.get("TRON_PIT_CERT_DIR", os.path.expanduser("~/.tronbrowser/pit-certs"))
+PIT_PINS_FILE = os.environ.get("TRON_PIT_PINS_FILE", os.path.join(PIT_CERT_DIR, "pins.txt"))
_trust_lock = threading.Lock()
_trust_seen = {} # name -> (ok, why); retried after a failure only once the pit restarts
@@ -435,28 +450,58 @@ def _safe_name(name):
return re.sub(r"\.{2,}", ".", re.sub(r"[^a-z0-9.-]", "", name.lower())).strip(".-")
+def remember_pin(name, pin):
+ """Append `name pin` to the pins file the launcher reads (once per pair)."""
+ try:
+ os.makedirs(os.path.dirname(PIT_PINS_FILE), mode=0o700, exist_ok=True)
+ have = set()
+ if os.path.exists(PIT_PINS_FILE):
+ with open(PIT_PINS_FILE) as f:
+ have = {line.strip() for line in f}
+ line = "%s %s" % (name, pin)
+ if line not in have:
+ with open(PIT_PINS_FILE, "a") as f:
+ f.write(line + "\n")
+ except OSError as exc:
+ log("pit: could not record the pin for %s: %s" % (name, exc))
+
+
def trust_available():
- """Can this machine take a per-name import at all? {available, why}."""
+ """Can this machine take a per-name import at all? {available, why}.
+ `relaunch` is true where the engine only honours pins given at start."""
if platform.system() != "Linux":
- return {"available": False, "why": "unsupported-platform"}
+ return {"available": False, "why": "unsupported-platform", "engine": PIT_ENGINE}
if not shutil.which("certutil"):
- return {"available": False, "why": "no-certutil"}
- return {"available": True, "why": "certutil", "nssdbs": pit_nssdbs()}
+ return {"available": False, "why": "no-certutil", "engine": PIT_ENGINE}
+ return {"available": True, "why": "certutil", "engine": PIT_ENGINE,
+ "relaunch": PIT_ENGINE == "flatpak", "nssdbs": pit_nssdbs()}
+
+
+def _nssdb_groups():
+ """[[candidate, ...], ...] — one group per home a Chromium might use."""
+ xdg = os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share")
+ groups = [[PIT_NSSDB, os.path.join(xdg, "pki", "nssdb")]]
+ extra = [d for d in PIT_NSSDB_EXTRA.split(":") if d]
+ if extra:
+ groups.append(extra)
+ for appdir in sorted(glob.glob(os.path.expanduser("~/.var/app/*"))):
+ if "chromium" in os.path.basename(appdir).lower() and os.path.isdir(appdir):
+ groups.append([os.path.join(appdir, ".pki", "nssdb"),
+ os.path.join(appdir, "data", "pki", "nssdb")])
+ return groups
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)
+ """Every NSS database a Chromium on this machine might read: per home, the
+ candidates that already exist, else the first one (to be created)."""
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)
+ for group in _nssdb_groups():
+ paths = [os.path.abspath(os.path.expanduser(p)) for p in group]
+ existing = [p for p in paths if os.path.exists(os.path.join(p, "cert9.db"))]
+ for p in existing or paths[:1]:
+ if p not in seen:
+ seen.add(p)
+ out.append(p)
return out
@@ -529,6 +574,7 @@ def _ensure_leaf_trust(name, ip):
if is_ca:
log("pit: https for %s: certificate is CA:TRUE — refusing to trust a key that could vouch for any name" % name)
return False, "ca-true"
+ remember_pin(name, pin)
try:
os.makedirs(PIT_CERT_DIR, mode=0o700, exist_ok=True)
cert_file = os.path.join(PIT_CERT_DIR, "moshpit-%s.crt" % name)
diff --git a/apps/desktop/launcher/tronbrowser b/apps/desktop/launcher/tronbrowser
index 08bf349..7638bbe 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.1
+ HELPER_VERSION=3.4.2
(
_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,14 +187,18 @@ 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.
+ # A Flatpak engine reads one of ~/.var/app//.pki/nssdb (its
+ # --persist=.pki) or ~/.var/app//data/pki/nssdb (its XDG_DATA_HOME,
+ # Chromium's default since M146) — never the real ~/.pki. Name both; the
+ # helper writes whichever exist.
_pit_db_extra=""
if [ "$BROWSER" = "flatpak" ] && [ -n "$FLATPAK_APP" ]; then
- _pit_db_extra="$HOME/.var/app/$FLATPAK_APP/.pki/nssdb"
+ _pit_db_extra="$HOME/.var/app/$FLATPAK_APP/.pki/nssdb:$HOME/.var/app/$FLATPAK_APP/data/pki/nssdb"
fi
+ _pit_engine=native
+ [ "$BROWSER" = "flatpak" ] && _pit_engine=flatpak
exec env TRON_TOR_DATA="$DATA/tor" TRON_TOR_BIN_DIR="$DIR" TRON_TOR_PIDFILE="$_pf" \
- TRON_PIT_NSSDB_EXTRA="$_pit_db_extra" \
+ TRON_PIT_NSSDB_EXTRA="$_pit_db_extra" TRON_PIT_ENGINE="$_pit_engine" \
python3 "$DIR/tron-tor-helper"
fi
) >>"$DATA/tor-helper.log" 2>&1 &
@@ -564,14 +568,28 @@ sync_moshpit_trust() {
if [ "$(uname -s)" != "Linux" ]; then return 0; fi
if [ "${TRONBROWSER_NO_MOSHPIT_TRUST:-0}" = "1" ]; then return 0; fi
- # 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=""
+ # Chromium opens ONE of two databases per home — the legacy ~/.pki/nssdb or,
+ # since M146, ${XDG_DATA_HOME:-~/.local/share}/pki/nssdb — and which one a
+ # build picks has changed between versions. A Flatpak engine is sandboxed
+ # with --persist=.pki and XDG_DATA_HOME=~/.var/app//data, so its two are
+ # ~/.var/app//.pki/nssdb and ~/.var/app//data/pki/nssdb, and the
+ # real ~/.pki is invisible to it. Write every candidate that already exists;
+ # create the legacy one only when neither does (a fresh ~/.pki/nssdb beside a
+ # populated XDG database would flip a newer Chromium onto an empty store).
+ _xdgdb="${XDG_DATA_HOME:-$HOME/.local/share}/pki/nssdb"
+ _dbs=""
+ if [ -f "$_xdgdb/cert9.db" ]; then _dbs="$_xdgdb"; fi
+ if [ -f "$HOME/.pki/nssdb/cert9.db" ] || [ -z "$_dbs" ]; then _dbs="$HOME/.pki/nssdb${_dbs:+ $_dbs}"; fi
if [ "$BROWSER" = "flatpak" ] && [ -n "$FLATPAK_APP" ]; then
- _flatdb="$HOME/.var/app/$FLATPAK_APP/.pki/nssdb"
+ _fa="$HOME/.var/app/$FLATPAK_APP"
+ _fdbs=""
+ if [ -f "$_fa/data/pki/nssdb/cert9.db" ]; then _fdbs="$_fa/data/pki/nssdb"; fi
+ if [ -f "$_fa/.pki/nssdb/cert9.db" ] || [ -z "$_fdbs" ]; then _fdbs="$_fa/.pki/nssdb${_fdbs:+ $_fdbs}"; fi
+ _dbs="$_dbs $_fdbs"
fi
- for _nssdb in "$HOME/.pki/nssdb" ${_flatdb:+"$_flatdb"}; do
+ # $_dbs is a space-separated list of paths under $HOME; a home with a space
+ # in it is not something the rest of this launcher survives either.
+ for _nssdb in $_dbs; do
_ready=0
# Each word below is a literal path or a glob result, so this stays
@@ -665,7 +683,25 @@ esac
# bundle one, but the kill switch is not ours to flip on their behalf.
# (Ungoogled keeps MV2, but force it off everywhere to be safe.)
MV2_KEEP="ExtensionManifestV2Disabled,ExtensionManifestV2Unsupported,ExtensionManifestV2DeprecationWarning,ExtensionManifestV2DeprecationUnsupported"
-FLAGS="--user-data-dir=$DATA --class=TronBrowser --no-first-run --no-default-browser-check --no-pings --disable-background-networking --disable-breakpad --disable-domain-reliability --disable-sync --disable-features=Translate,OptimizationHints,InterestFeedContentSuggestions,$MV2_KEEP$GPU_OFF_FEATURES --load-extension=$EXT"
+# Flatpak engine only: the Flathub ungoogled-chromium ignores NSS user trust (the
+# pit helper's per-name import sat in the very database strace showed it opening,
+# and Chromium still answered "No matching issuer found"), but it honours pins
+# given on the command line. The helper records every pin it accepted — a key the
+# registry vouches for, for that name — in $HOME/.tronbrowser/pit-certs/pins.txt;
+# hand them over here. Chromium shows a one-line "unsupported command-line flag"
+# bar at start when this switch is present, so it is only passed when there is at
+# least one pin, i.e. on a machine that has used https on a pit name. Native
+# engines honour NSS and get nothing here.
+pit_spki_flag() { # pins_file -> prints the switch, or nothing
+ [ -s "$1" ] || return 0
+ _pins="$(awk 'NF >= 2 && $2 !~ /[^A-Za-z0-9+\/=]/ { print $2 }' "$1" | sort -u | paste -sd, -)"
+ [ -n "$_pins" ] && printf -- '--ignore-certificate-errors-spki-list=%s' "$_pins"
+}
+PIT_SPKI_FLAG=""
+if [ "$BROWSER" = "flatpak" ]; then
+ PIT_SPKI_FLAG="$(pit_spki_flag "$HOME/.tronbrowser/pit-certs/pins.txt")"
+fi
+FLAGS="--user-data-dir=$DATA --class=TronBrowser --no-first-run --no-default-browser-check --no-pings --disable-background-networking --disable-breakpad --disable-domain-reliability --disable-sync --disable-features=Translate,OptimizationHints,InterestFeedContentSuggestions,$MV2_KEEP$GPU_OFF_FEATURES --load-extension=$EXT${PIT_SPKI_FLAG:+ $PIT_SPKI_FLAG}"
# Make the tab-strip audio indicator a clickable mute/unmute control. Upstream
# media::kEnableTabMuting is DISABLED_BY_DEFAULT and stock Chrome only turns it
diff --git a/docs/moshpit-pit-toggle.md b/docs/moshpit-pit-toggle.md
index fea1596..ff47038 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.1
+**Status:** shipped with the AI-sidebar extension + `tron-tor-helper` 3.4.2
**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,13 +79,34 @@ 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).
+Chromium opens one of two databases per home: the legacy `~/.pki/nssdb`, or
+since M146 `${XDG_DATA_HOME:-~/.local/share}/pki/nssdb`, and which one a build
+picks has changed between versions. A Flatpak engine is sandboxed with
+`--persist=.pki` and `XDG_DATA_HOME=~/.var/app//data`, so its two
+candidates are `~/.var/app//.pki/nssdb` and
+`~/.var/app//data/pki/nssdb`, and the real `~/.pki` is invisible to it.
+The helper and the launcher's Local CA sync therefore write every candidate
+that already exists for each home (the real one, the engine's, and any
+`~/.var/app/*chromium*`), and create the legacy one only when none exists,
+so a browser is never flipped onto a fresh empty store. Found on bonita:
+Flatpak ungoogled-chromium 152 read `data/pki/nssdb` while `.pki/nssdb`
+existed beside it, and Chromium's net log said "No matching issuer found"
+until that database held the leaf too.
+
+**Flatpak engines do not honour NSS user trust at all.** Found on bonita with
+the Flathub ungoogled-chromium 152: `strace` showed Chromium opening the very
+database that held the leaf (peer and anchor trust both tried), single-process
+and no-sandbox made no difference, and its net log still said "No matching
+issuer found". What that build does honour is
+`--ignore-certificate-errors-spki-list`, Chromium's own per-key allowance. So
+the helper also records every pin it accepts in
+`~/.tronbrowser/pit-certs/pins.txt`, and for a Flatpak engine the launcher
+passes those pins on the command line at start. Two consequences: a name first
+trusted mid-session loads over https after one relaunch (the sidebar says so),
+and Chromium shows its one-line "unsupported command-line flag" bar at start on
+a machine that has such pins. Native engines take the NSS path and get neither.
+The clean way out is a Moshpit CA on the registry side, or shipping the
+portable ungoogled-chromium as TronBrowser's own engine, which honours NSS.
Linux only for now (Chromium on macOS reads the keychain, which needs an
interactive prompt), and it needs `certutil` (Debian/Ubuntu `libnss3-tools`,