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 ]