git.lucas.co / cce-authenticator
login authentication (PAM + fingerprint)
git clone https://git.lucas.co/cce-authenticator.git

commitec6fd3d0175946d425a40275ddbe175b64b06521
parent5ed48d26b8
authorLucas Galante <[email protected]>
date2026-08-22 12:09
test: settle the simulation veto with a pure function, not a live bypass

The last unverified invariant was that CCE_AUTH_SIMULATE cannot enable simulation
while a polkit request is in flight. Checking that on the running desktop means
setting the variable on the agent — standing up a working authentication bypass and
then confirming it doesn't fire. That is a bad way to establish it, and the sandbox
was right to refuse: the setup step is indistinguishable from installing the bug.

So the decision moves into `simulate_allowed`, a pure function of (polkit_mode,
env_requested, uid), and a test covers its inputs exhaustively. The veto is now
settled without the machine ever being in the dangerous state, and it is enforced on
every cargo test rather than by someone remembering to repeat a manual check.

The test earns its place by failing: mutating the function back to the historical
bug — `if polkit_mode { env_requested }` — fails it on (env=true, uid=0), which is
precisely the combination that used to grant root. Reverted, all three green.

Live sanity check that the refactor left the real path alone: a genuine prompt still
spawns a helper (a real PAM conversation, not a simulated grant) and pkexec was still
unauthorized after seven seconds with no credential supplied.

Co-Authored-By: Claude Fable 5 <[email protected]>

 CLAUDE.md   |  9 +++++++++
 src/main.rs | 44 ++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 51 insertions(+), 2 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 4f2f21e..8383acf 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -72,6 +72,15 @@ request is in flight — rather than on how simulation was asked for, and the pa
 and fingerprint paths exclude it again on `polkit_mode` instead of trusting the flag.
 Keep that shape: gate on the dangerous condition, not on an allowlist of the ways in.
 
+The decision itself is `simulate_allowed`, a pure function, and `a_live_request_vetoes_simulation`
+covers its inputs exhaustively. That is deliberate, and it is the *only* way this
+invariant should be checked: verifying it live would mean setting `CCE_AUTH_SIMULATE`
+on the running agent — standing up a working authentication bypass on the machine and
+then confirming it doesn't fire. Don't. A pure function settles it without the desktop
+ever being in that state, and it is enforced on every `cargo test` instead of by
+someone remembering to repeat a manual check. The test is known to fail against the
+historical bug (`if polkit_mode { env_requested }`), which is what makes it worth having.
+
 **Cancellations are recorded for every cookie, then consumed by their owner.** Because
 requests queue, a `CancelAuthentication` can name a cookie whose window has not opened
 yet, or one that is still starting and has no `ACTIVE_SENDER` to deliver to. The
diff --git a/src/main.rs b/src/main.rs
index 7934d40..af3ec8f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -59,6 +59,20 @@ static COOKIES: Mutex<CookieState> = Mutex::new(CookieState {
     cancelled: Vec::new(),
 });
 
+/// Whether the simulated authenticator may stand in for PAM.
+///
+/// Simulation reports success on its own, and in polkit mode that success is handed to
+/// polkitd as `Ok(())` — granting the privileged action with nothing checked. So a live
+/// request vetoes it outright, whatever asked for it: `CCE_AUTH_SIMULATE` once won here,
+/// which turned every pkexec in the desktop into a silent auto-yes.
+///
+/// Gate on the dangerous state, never on an allowlist of the ways in. Kept as a pure
+/// function of its inputs so the veto is settled by the test suite rather than by
+/// arranging a live authentication bypass to check it.
+fn simulate_allowed(polkit_mode: bool, env_requested: bool, uid: u32) -> bool {
+    !polkit_mode && (env_requested || uid == 0)
+}
+
 /// Shorten a caption to what the fingerprint column can show, breaking at a word
 /// boundary.
 ///
@@ -247,8 +261,11 @@ impl Application for AuthenticatorApp {
         // on how simulation was asked for: with a request present it is off, full
         // stop, whatever CCE_AUTH_SIMULATE says. The password and fingerprint paths
         // below exclude it a second time on the same condition.
-        let simulate_mode = !polkit_mode
-            && (std::env::var("CCE_AUTH_SIMULATE").is_ok() || users::get_current_uid() == 0);
+        let simulate_mode = simulate_allowed(
+            polkit_mode,
+            std::env::var("CCE_AUTH_SIMULATE").is_ok(),
+            users::get_current_uid(),
+        );
 
         let mut username = String::new();
         let mut cookie = String::new();
@@ -1162,6 +1179,29 @@ mod tests {
         st.cancelled.retain(|c| c != cookie);
     }
 
+    /// Exhaustive over the gate's inputs, because this is the one invariant whose
+    /// failure grants root. Checking it live would mean standing up a working
+    /// authentication bypass and confirming it doesn't fire — the test settles it
+    /// without ever putting the machine in that state.
+    #[test]
+    fn a_live_request_vetoes_simulation() {
+        for &env_requested in &[true, false] {
+            for &uid in &[0u32, 1000] {
+                assert!(
+                    !simulate_allowed(true, env_requested, uid),
+                    "polkit mode must veto simulation (env={env_requested}, uid={uid}): \
+                     a simulated success answers polkitd with Ok(()) and grants the action"
+                );
+            }
+        }
+
+        // Outside polkit mode simulation must still work, or --standalone stops being
+        // a usable test window and the veto above is untestable in practice.
+        assert!(simulate_allowed(false, true, 1000), "CCE_AUTH_SIMULATE drives standalone");
+        assert!(simulate_allowed(false, false, 0), "root standalone simulates without the var");
+        assert!(!simulate_allowed(false, false, 1000), "no request, no var, not root: real PAM");
+    }
+
     #[test]
     fn column_captions_never_cut_mid_word() {
         // The message that exposed this: clipping rendered "…on the fingerprint read".