git.lucas.co / cce-display-manager
login greeter
git clone https://git.lucas.co/cce-display-manager.git

scripts/cce-keyring-selftest (6.1K)

  1 #!/usr/bin/env bash
  2 # cce-keyring-selftest — did this login's keyring chain work?
  3 #
  4 # Written 2026-08-16 for the KeePassXC unlock chain; rewritten 2026-08-31 for
  5 # the design that replaced it (cce-display-manager@81450ce): gnome-keyring owns
  6 # org.freedesktop.secrets and is started ALREADY UNLOCKED by
  7 # cce-gnome-keyring-start, which unseals the keyring password from the TPM
  8 # (tpm2-tools) and feeds it to the daemon's stdin — one process, wired in by
  9 # the drop-in ~/.config/systemd/user/gnome-keyring-daemon.service.d/
 10 # tpm-unlock.conf. Login is by fingerprint, so PAM never sees a password: the
 11 # TPM seal is the only unlock path, which is why it deserves a self-test.
 12 #
 13 # Run it after logging in. The journal half is durable, but the compositor log
 14 # lives in the per-user runtime dir (moved out of /tmp in cce@522ce13), which
 15 # is cleared at logout — so run this before logging out; afterwards the
 16 # barrier half of the answer is simply gone. The /tmp fallback covers a
 17 # session started by a pre-move startcce.
 18 CCE_LOG="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/cce/cce.log"
 19 [ -f "$CCE_LOG" ] || CCE_LOG=/tmp/cce.log
 20 
 21 pass=0 fail=0
 22 ok()   { printf '  \033[32mPASS\033[0m  %s\n' "$1"; pass=$((pass+1)); }
 23 bad()  { printf '  \033[31mFAIL\033[0m  %s\n' "$1"; fail=$((fail+1)); }
 24 info() { printf '        %s\n' "$1"; }
 25 
 26 # Everything must be scoped to THIS login, not `journalctl -b`: one boot spans
 27 # many sessions here (the machine stays up for days), and boot-wide counts made
 28 # a healthy login look like 32 failed claude-desktop launches.
 29 #
 30 # Find the compositor by /proc/<pid>/exe basename, never comm or a pgrep
 31 # pattern: it is launched through the `cce` symlink, so its comm is "cce", but
 32 # a `make run` leftover has comm "cce-fx".
 33 compositor_pid() {
 34     local p exe
 35     for p in /proc/[0-9]*; do
 36         exe=$(readlink "$p/exe" 2>/dev/null) || continue
 37         exe=${exe% (deleted)}
 38         [ "${exe##*/}" = cce-fx ] && { printf '%s\n' "${p#/proc/}"; return 0; }
 39     done
 40     return 1
 41 }
 42 
 43 if cce_pid=$(compositor_pid); then
 44     SINCE=$(date -d "$(ps -o lstart= -p "$cce_pid")" '+%Y-%m-%d %H:%M:%S')
 45 else
 46     SINCE=$(date -d '-10 minutes' '+%Y-%m-%d %H:%M:%S')
 47 fi
 48 jl() { journalctl --since "$SINCE" --no-pager "$@" 2>/dev/null; }
 49 
 50 echo "cce keyring self-test — session (compositor) started $SINCE"
 51 echo
 52 
 53 # 1. Is the Secret Service actually open right now?
 54 locked=$(busctl --user get-property org.freedesktop.secrets \
 55     /org/freedesktop/secrets/aliases/default \
 56     org.freedesktop.Secret.Collection Locked 2>/dev/null)
 57 [ "$locked" = "b false" ] && ok "keyring is unlocked" || bad "keyring is ${locked:-unreachable}"
 58 
 59 # 2. Is the daemon running from its unit? The unit is the only start path that
 60 #    goes through cce-gnome-keyring-start, i.e. the only one that unlocks.
 61 if systemctl --user is-active --quiet gnome-keyring-daemon.service; then
 62     ok "gnome-keyring-daemon.service active"
 63     kstart=$(systemctl --user show gnome-keyring-daemon.service \
 64         -p InactiveExitTimestamp --value)
 65     [ -n "$kstart" ] && info "unit up since ${kstart#* }"
 66 else
 67     bad "gnome-keyring-daemon.service not active"
 68 fi
 69 
 70 # 3. Does the UNIT's daemon own the bus name? `gnome-keyring-daemon --unlock`
 71 #    outside the unit daemonizes into a second, LOCKED daemon, and the stock
 72 #    D-Bus activation Exec would spawn one too (see cce-gnome-keyring-start and
 73 #    the dbus/ shadow). Two daemons racing for org.freedesktop.secrets is the
 74 #    nondeterminism that broke the old chain — so check the owner's cgroup,
 75 #    not just that the name is taken. (Cgroup, not MainPID: the unit's main
 76 #    process is the start script's shell; the daemon is its child.)
 77 owner=$(busctl --user status org.freedesktop.secrets 2>/dev/null | sed -n 's/^PID=//p')
 78 if [ -z "$owner" ]; then
 79     bad "org.freedesktop.secrets has no owner"
 80 elif grep -q 'gnome-keyring-daemon\.service' "/proc/$owner/cgroup" 2>/dev/null; then
 81     ok "org.freedesktop.secrets owned by the unit's daemon (pid $owner)"
 82 else
 83     bad "org.freedesktop.secrets owner (pid $owner) is OUTSIDE gnome-keyring-daemon.service — rogue second daemon?"
 84 fi
 85 
 86 # 4. Did the TPM unseal go cleanly? cce-gnome-keyring-start retries transient
 87 #    TPM contention itself and only logs after giving up, so any hit here is a
 88 #    real failure — and with Restart=no (deliberate: a restart loop wedged the
 89 #    login once) a failed start stays failed quietly until something asks.
 90 if jl --user -u gnome-keyring-daemon.service | grep -q 'TPM unseal failed'; then
 91     bad "TPM unseal failed this session (journalctl --user -u gnome-keyring-daemon.service)"
 92 else
 93     ok "no TPM unseal failures this session"
 94 fi
 95 
 96 # 5. Did the compositor barrier engage, and for how long?
 97 bline=$(grep -h -E 'Keyring (unlocked after|still locked)|No Secret Service' "$CCE_LOG" 2>/dev/null | tail -1)
 98 case "$bline" in
 99     *"Keyring unlocked after"*) ok "barrier released: ${bline##*] }" ;;
100     *"still locked"*)           bad "barrier hit its timeout: ${bline##*] }" ;;
101     *"No Secret Service"*)      bad "no Secret Service seen: ${bline##*] }" ;;
102     *) info "no barrier line in $CCE_LOG (no gated app restored?)" ;;
103 esac
104 
105 # 6. The actual question: is claude-desktop alive and loaded? The failing one
106 #    died at 73MB; a working one is hundreds of MB.
107 mainpid=$(pgrep -f 'claude-desktop --password-store' | head -1)
108 if [ -n "$mainpid" ]; then
109     rss=$(awk '/VmRSS/{print $2}' "/proc/$mainpid/status" 2>/dev/null)
110     rssmb=$(( ${rss:-0} / 1024 ))
111     if [ "$rssmb" -gt 300 ]; then
112         ok "claude-desktop running and loaded (${rssmb}MB, pid $mainpid)"
113     else
114         bad "claude-desktop running but only ${rssmb}MB — may be stalled on the keyring"
115     fi
116 else
117     bad "claude-desktop not running"
118 fi
119 
120 # 7. Did it need a relaunch? More than one launch this session = the restored
121 #    one died and had to be started again by hand — the original symptom.
122 scopes=$(jl --user | grep -c 'Started app-claude-desktop-.*\.scope')
123 if [ "$scopes" -le 1 ]; then
124     ok "claude-desktop started once this session ($scopes launch)"
125 else
126     bad "claude-desktop launched $scopes times this session — first attempt(s) died"
127 fi
128 
129 echo
130 printf '%d passed, %d failed\n' "$pass" "$fail"
131 [ "$fail" -eq 0 ]