From 9d8f3314c06d1af066cffd4d60964fb94646dcf8 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 1 Sep 2026 15:22:35 +0000 Subject: [PATCH] fix(launcher): trust Moshpit certificates in the store Chromium reads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `curl https://chovy.hacker` returns the page while the same URL in TronBrowser fails with ERR_CERT_AUTHORITY_INVALID. Not a Moshpit regression and not a resolver problem: the two read different trust stores. curl/OpenSSL reads /etc/ssl/certs, which is the only store Moshpit writes. Chromium on Linux takes user-added trust from the per-user NSS database at ~/.pki/nssdb and nowhere else — a path keyed off $HOME, not off --user-data-dir, so no TronBrowser profile could ever have carried it and reinstalling never helped. The launcher now mirrors whatever Moshpit installed into ~/.pki/nssdb on every start. Every start rather than once at install, because names get pointed and trusted long after the browser is installed; an entry already present is skipped by nickname, so the steady state costs one `certutil -L`. The trust flag is the part that is easy to get wrong. The Moshpit Local CA is CA:TRUE and goes in as an anchor ("C,,"); a per-name origin leaf is CA:FALSE, self-signed, and must go in as a trusted peer ("P,,"). Importing a leaf as "C,," fails, because Chromium will not anchor a CA:FALSE certificate — the same wall the old CA:TRUE origin certificates hit from the other side. basicConstraints is read rather than guessed from the filename, since getting it backwards fails exactly like doing nothing. The Local CA keeps the nickname `moshcode dns enable` uses, so a database it already wrote is recognised as done. install.sh installs certutil (libnss3-tools / nss-tools / nss / mozilla-nss-tools) on install and upgrade, but only on a machine that actually has Moshpit certificates — a package nobody needs is not ours to install. That is the one part of the job needing root, and without it the launcher can only print a command for the user to run. Also guards both ensure_ calls with `|| true`. They return 1 when they could not install their tool and the script runs under `set -eu`, so on a machine with no supported package manager `ensure_tor` was aborting an install that had already put the browser on disk. Verified on Linux against real Moshpit certificates: a Chromium whose NSS database was built only by this code loads https://chovy.hacker and https://seo.rank at 200, where the same Chromium with an empty database fails ERR_CERT_AUTHORITY_INVALID. Import is idempotent, the certutil-missing path prints the hint and writes no database, the opt-out (TRONBROWSER_NO_MOSHPIT_TRUST=1) returns early, and the full launcher still reaches the browser with rc=0. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01P2zwGvXnFseRtrDWH9khxn --- apps/desktop/launcher/tronbrowser | 96 +++++++++++++++++++++++++++++++ apps/web/public/install.sh | 57 +++++++++++++++++- 2 files changed, 150 insertions(+), 3 deletions(-) diff --git a/apps/desktop/launcher/tronbrowser b/apps/desktop/launcher/tronbrowser index 2f831fc..697c538 100755 --- a/apps/desktop/launcher/tronbrowser +++ b/apps/desktop/launcher/tronbrowser @@ -462,6 +462,102 @@ if [ "$TOR" != "1" ] && [ -z "$MAC_APP" ] && [ -f "$DIR/tron-pwa" ]; then fi fi +# --- Moshpit names over HTTPS ---------------------------------------------- +# Chromium does not read /etc/ssl/certs. On Linux the only user-added trust it +# honours lives in the per-user NSS database at ~/.pki/nssdb — a path keyed off +# $HOME, not off --user-data-dir, so no TronBrowser profile can carry it and no +# amount of reinstalling puts it there. +# +# Moshpit installs its certificates into the OpenSSL store instead. That is why +# `curl https://chovy.hacker` returns the page while the same URL here fails +# with ERR_CERT_AUTHORITY_INVALID: the two are reading different stores. It +# reads as a TronBrowser bug, and there is nothing the user can do about it from +# inside the browser, so mirror whatever Moshpit installed into the store +# Chromium actually reads. +# +# Two certificate shapes, and the trust flag differs: +# * the Moshpit Local CA (CA:TRUE) — an anchor, "C,,". This is what a machine +# behind the pinned-TLS proxy is served. +# * a per-name origin leaf (CA:FALSE, self-signed, SAN = the name) — "P,,", +# a trusted peer. Importing one as "C,," fails: Chromium will not anchor a +# CA:FALSE certificate. Reading basicConstraints beats guessing from the +# filename, because getting this backwards fails exactly like doing nothing. +# +# Runs on every start rather than once at install. Names get pointed and trusted +# long after the browser is installed, and re-importing one already in the +# database is skipped by nickname, so the steady state costs a `certutil -L`. +# Set TRONBROWSER_NO_MOSHPIT_TRUST=1 to opt out entirely. +# +# The Local CA keeps the nickname `moshcode dns enable` gives it, so a database +# it already wrote is recognised as done rather than imported a second time. +moshpit_nickname() { # cert_file + case "$1" in + */moshpit-local-ca.crt|*/.moshpit/ca/ca.crt) printf 'Moshpit Local CA' ;; + *) + _base="$(basename "$1" .crt)" + printf 'moshpit %s' "${_base#moshpit-}" + ;; + esac +} + +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" + _ready=0 + + # Each word below is a literal path or a glob result, so this stays + # whitespace-safe; an unmatched glob arrives as the pattern itself and fails + # the -f test. + for _cert in /usr/local/share/ca-certificates/moshpit-*.crt \ + /etc/ca-certificates/trust-source/anchors/moshpit-*.crt \ + /etc/pki/ca-trust/source/anchors/moshpit-*.crt \ + "$HOME/.moshpit/ca/ca.crt"; do + if [ ! -f "$_cert" ]; then continue; fi + + # Deferred until we know there is something to import: a machine that never + # heard of Moshpit should not be told to go install anything. + if [ "$_ready" = "0" ]; then + if ! command -v certutil >/dev/null 2>&1; then + echo "TronBrowser: Moshpit certificates are installed, but 'certutil' is not." >&2 + echo " Chromium reads its own trust store, so .hacker/.rank names fail TLS here." >&2 + echo " Debian/Ubuntu: sudo apt install libnss3-tools Fedora: sudo dnf install nss-tools" >&2 + echo " Arch: sudo pacman -S nss Then start TronBrowser again." >&2 + return 0 + fi + if [ ! -f "$_nssdb/cert9.db" ]; then + mkdir -p "$_nssdb" 2>/dev/null || return 0 + certutil -d "sql:$_nssdb" -N --empty-password >/dev/null 2>&1 || return 0 + fi + _ready=1 + fi + + _nick="$(moshpit_nickname "$_cert")" + if certutil -d "sql:$_nssdb" -L -n "$_nick" >/dev/null 2>&1; then continue; fi + + _flag="P,," + if command -v openssl >/dev/null 2>&1; then + if openssl x509 -noout -text -in "$_cert" 2>/dev/null | grep -q 'CA:TRUE'; then + _flag="C,," + fi + else + # No openssl: the Local CA is the only anchor Moshpit ships, and it is the + # one file we can identify by name alone. + case "$_nick" in "Moshpit Local CA") _flag="C,," ;; esac + 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 + fi + done + return 0 +} + +# Best-effort and never fatal: a trust store we could not write must not stop +# the browser from opening. +sync_moshpit_trust || true + # --- GPU backend ----------------------------------------------------------- # A crashing GPU process does not look like a crash. The window stays up and the # page keeps whatever it had already rasterized — a logo, a header — while diff --git a/apps/web/public/install.sh b/apps/web/public/install.sh index 645976d..fc30847 100755 --- a/apps/web/public/install.sh +++ b/apps/web/public/install.sh @@ -642,6 +642,51 @@ ensure_tor() { return 1 } +# Chromium takes user-added trust only from the per-user NSS database at +# ~/.pki/nssdb, never from /etc/ssl/certs. The launcher mirrors Moshpit's +# certificates into it on every start so `.hacker` / `.rank` names load over +# HTTPS — but writing an NSS database needs `certutil`, and that is the one part +# of the job that needs a package installed. Doing it here is the difference +# between the launcher fixing the problem and the launcher printing a command +# for the user to run, which is the manual step this is meant to remove. +# +# Only on a machine that actually has Moshpit certificates: a package nobody +# needs is not ours to install. Best-effort throughout — the launcher explains +# what to do if this cannot get there. +ensure_certutil() { + # macOS Chromium reads the system keychain, which `moshcode dns enable` + # already writes. There is no NSS database in the picture at all. + [ "$(uname -s)" = "Linux" ] || return 0 + command -v certutil >/dev/null 2>&1 && return 0 + + _have_moshpit=0 + for _c in /usr/local/share/ca-certificates/moshpit-*.crt \ + /etc/ca-certificates/trust-source/anchors/moshpit-*.crt \ + /etc/pki/ca-trust/source/anchors/moshpit-*.crt \ + "$HOME/.moshpit/ca/ca.crt"; do + if [ -f "$_c" ]; then _have_moshpit=1; break; fi + done + [ "$_have_moshpit" = "1" ] || return 0 + + info "Setting up certutil (so Moshpit names load over HTTPS)…" + uid="$(id -u 2>/dev/null || echo 0)" + SUDO="" + if [ "$uid" -ne 0 ] && command -v sudo >/dev/null 2>&1 && { [ -t 1 ] || [ -t 2 ]; }; then SUDO="sudo"; fi + if [ "$uid" -eq 0 ] || [ -n "$SUDO" ]; then + # The tool is the same everywhere; only the package carrying it differs. + if command -v apt-get >/dev/null 2>&1; then $SUDO apt-get update -y >/dev/null 2>&1; $SUDO apt-get install -y libnss3-tools >/dev/null 2>&1 || true + elif command -v dnf >/dev/null 2>&1; then $SUDO dnf install -y nss-tools >/dev/null 2>&1 || true + elif command -v yum >/dev/null 2>&1; then $SUDO yum install -y nss-tools >/dev/null 2>&1 || true + elif command -v pacman >/dev/null 2>&1; then $SUDO pacman -Sy --noconfirm nss >/dev/null 2>&1 || true + elif command -v zypper >/dev/null 2>&1; then $SUDO zypper --non-interactive install mozilla-nss-tools >/dev/null 2>&1 || true + elif command -v apk >/dev/null 2>&1; then $SUDO apk add nss-tools >/dev/null 2>&1 || true + fi + command -v certutil >/dev/null 2>&1 && return 0 + fi + warn "Couldn't install certutil automatically. Moshpit names (.hacker, .rank) will fail TLS in TronBrowser until it is installed (e.g. 'sudo apt install libnss3-tools' / 'sudo pacman -S nss')." + return 1 +} + do_install() { need uname asset="$(detect_asset)" @@ -692,7 +737,11 @@ DESKTOP command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$apps_dir" 2>/dev/null || true ensure_browser - ensure_tor # so the in-browser 🧅 Tor toggle works out of the box + # `|| true` because this script runs under `set -eu`: both of these return 1 + # when they could not install their tool, and a missing nice-to-have must not + # abort an install that has already put the browser on disk. + ensure_tor || true # so the in-browser 🧅 Tor toggle works out of the box + ensure_certutil || true # so Moshpit names load over HTTPS on first launch brand_macos_icon "$(dirname "$bin")/tronbrowser.png" info "Installed TronBrowser $tag to $APP_DIR" @@ -791,8 +840,9 @@ do_upgrade() { [ -n "$latest" ] || err "could not resolve the latest release of $REPO" if [ "$current" = "$latest" ] && [ "${TB_FORCE:-0}" != "1" ]; then info "TronBrowser is already up to date ($current)." - ensure_browser # still make sure Ungoogled Chromium is installed - ensure_tor # and that Tor is available for the toggle + ensure_browser # still make sure Ungoogled Chromium is installed + ensure_tor || true # and that Tor is available for the toggle + ensure_certutil || true # and that Moshpit trust can be written brand_macos_icon "$(find "$APP_DIR" -maxdepth 3 -name tronbrowser.png 2>/dev/null | head -n1)" # re-apply icon (Chromium updates reset it) info "Re-install anyway with: TB_FORCE=1 tron upgrade" return @@ -851,6 +901,7 @@ case "$cmd" in esac ;; remove|uninstall) do_remove ;; ensure-tor) ensure_tor ;; + ensure-certutil) ensure_certutil ;; version|--version|-v) do_version ;; help|--help|-h) usage ;; *) err "unknown command: $cmd (try 'help')" ;;