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`,