Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/SESSION-AUTHORITY-DOCTRINE.md

16 KiB

Session authority — doctrine

The concepts behind the session/trust work, locked down so they stop living in chat logs. This is why; SESSION-TRUST-ARCHITECTURE.md is the contract and TRUST-BOUNDARY-MATRIX.md is the enforcement status. Where this and the code disagree, the code is right — fix one or the other in the same change.

1. Why Linux has no session manager

Unix's security boundary is the UID. Everything running as you is you — your shell, your editor, a binary you curl-piped last Tuesday. There was never a concept of "the session" as an authority separate from "whatever runs as this user," because nothing needed gating from the user's own processes. X11 made it explicit: any authenticated client could do anything, and session management (XSMP) was a save-file, not a boundary.

What did need gating was narrower: things affecting other people on the box. Suspend, poweroff, VT-switch, killing another user's session. That is the entire reason logind's inhibitors and polkit actions cluster there and nowhere else. ConsoleKit (2007) → logind (2011) is the closest thing to a session manager Linux grew, and its job is deliberately small: who is logged in on which seat, and arbitration of the few operations touching shared hardware.

polkit and xdg-desktop-portal look like a permission broker but exist for a different reason: Flatpak. Sandboxed apps have zero ambient access — the first things on Linux that aren't automatically you. Portals are the mediated door. This is why the stack feels redundant and inconsistent: two overlapping trust models bolted together, unified by convention and a shared bus. Unsandboxed apps (still most of the desktop) ignore both and call system().

Wayland quietly pulled session-manager authority into the compositor without naming it that — ext-session-lock-v1 and ext-idle-notify-v1 exist because Wayland's founding move was "the compositor is the sole trusted actor; anything privileged goes through a protocol it can refuse."

The phone did not invent the need for a session manager. It removed the ability to shrug it off — one screen, one battery, no fallback device, and an expectation since Android/iOS that a misbehaving app cannot hold a wake lock forever or walk past the lock screen.

So this is not a redundant permissions layer. It is the thing classical Linux never had: one authority treating compositor state (lock, idle) and system state (suspend, inhibit) as facets of the same session, instead of three subsystems queried separately and reconciled by hand.

2. Capability tiers — one policy system, not three features

The lock screen, media, and the agent are not three subsystems. They are one policy system with tiers. Tier is metadata on the guarded methods that already exist ({minTier: 'stepUp'}), checked at the point that currently checks state().locked. It is not a fourth subsystem.

Tier Requirement Examples
ambient none clock, weather, timers, MPRIS transport, general Q&A that touches neither memory nor sensorium
personal session genuinely unlocked (!screenLocked && !screenLockSecure, live check) messages, calendar, memories, fleet state, conversation history
stepUp fresh re-auth, even when already unlocked sending, pushes, deletes, payments, physical access — and reading personal content on the lock surface

stepUp is a reveal mechanism, not only an action gate

The tier table read naively implies personal content is binary: hidden until the session unlocks. That is wrong for the sole-user phone. Notification bodies, message content, and agent output stay hidden at ambient — but a fingerprint touch on the lock surface should reveal them in place, without unlocking the session.

That means action families include read, not just mutation verbs. On a phone you are the only user of, read is the family that matters most.

Mechanically this needs no new machinery: a PamContext against souveraine-stepup accepting fingerprint mints a short-lived grant, and LockContentPolicy.allowsOnLock() gains a third condition — not just ambient || promotedAmbient, but || hasFreshGrant('read').

Gated on the fingerprint sensor coming online (not yet true on blueline).

Media metadata is a toggle, not a fixed answer

nowPlaying metadata is ambient by default, configurable to personal. Android and iOS both punt this to the user because it genuinely varies by person. Wire it into the tier system; do not special-case it.

Two policy questions, deliberately unanswered

  • Freshness window. Prompting per tagged action is sudo asking per line of a script. Needs a trust window (last-success timestamp vs expiry). The number is a deliberate policy setting, not an implementation accident.
  • In-flight output on lock. If lock fires mid-turn, personal/step-up output is withheld or redacted; only ambient remains. Same rule as notification content — not a separate rule for agent output.

3. Step-up is decoupled from the session lock

PamContext is a standalone connection type, not baked into WlSessionLock. Point a second, narrower context at a custom service (/etc/pam.d/souveraine-stepup, accepting password or fingerprint in any order), run that one conversation, gate that one call.

Step-up never unlocks the session. It never accepts a boolean from an agent as proof. The grant is short-lived, in-memory, bound to an action family, and cleared on lock, session end, PAM failure, and expiry.

4. Never hold state the protocol owns

The rule that keeps this from becoming a second source of truth:

  • locked comes from LockedHint / Lock() / Unlock() — never a hand-tracked bool. A shadow copy can disagree with logind, and anything asking logind directly (loginctl, another client, you at 2am) sees something different than the shell shows.
  • Idle goes through ext-idle-notify-v1 so other apps' inhibitors are visible to us and ours to them. Hand-rolled idle detection is blind to both.
  • Suspend goes through logind's Suspend(), never /sys/power/state — that is where decades of S3-vs-s2idle and firmware-quirk handling live.
  • Auth goes through PAM. It is the one surface everything on the system already agrees on (sudo, sshd, fprintd, login). Reinventing it is the one move that would genuinely be redesigning a Linux fundamental.

Being a client of logind and the Wayland protocols is the same role gnome-session, ksmserver, and swayidle+swaylock have always had. That is a well-trodden category, not a hack on the OS. It stops being that the moment we keep parallel state.

5. Idle belongs to the compositor; logind owns only sleep and shutdown

ext-idle-notify-v1 (IdleMonitor) and idle-inhibit-unstable-v1 (IdleInhibitor) are native Wayland protocol bindings — no external process, no polling. Idle staging becomes several IdleMonitor instances at different timeouts, declared in config and legible to the agent, instead of exec directives parsed out of hypridle.conf.

IdleMonitor.respectIdleInhibitors (default true) already factors active inhibitors into isIdle.

logind's Inhibit() is therefore needed only for sleep and shutdown — the categories genuinely outside compositor authority. That is a far smaller surface than "wrap all of logind."

Caveat before removing the hypridle unit: both protocols must actually be bound on the build. Verify separately on x86 and SDM845 — do not assume parity.

6. The inhibit vocabulary is portal-shaped, not logind-shaped

logind's what is a fixed set: shutdown, sleep, idle, handle-*-key, handle-lid-switch. There is no logout or user-switch in logind — nothing in the kernel or systemd cares whether you are mid-logout.

The four-flag bitmask (1=Logout, 2=UserSwitch, 4=Suspend, 8=Idle) is org.freedesktop.portal.Inhibit — the GNOME session-manager convention, later standardized as the portal interface. It is what browsers actually call when a video call wants to stop the screen locking. Build the agent-facing surface on that semantic (third-party software already speaks it), translate per flag:

Flag Underlying mechanism
IDLE toggle IdleInhibitor.enabled — no process, no fd
SUSPEND fd-holding systemd-inhibit process (§7)
LOGOUT / USER_SWITCH pure shell policy — our own entry points check the registry; nothing to inhibit at OS level

Only expose a kind once it owns a real backend. Accepting a kind we cannot enforce is a lie in a result shape.

Eventually this sets up registering as a real org.freedesktop.portal.Inhibit backend so browsers' "prevent lock during this call" lands in the same registry instead of silently doing nothing. Separate design pass; not folded in now.

7. The fd problem

No generic D-Bus binding in QML, and Inhibit()'s entire contract is "hold this fd open; close it to release." A Process that runs loginctl and exits cannot hold it — the fd dies with the process.

The pattern everything else uses (systemd-inhibit, swayidle): spawn a long-lived wrapper that takes the lock and blocks.

systemd-inhibit --what=sleep --who=souveraine --why="lock before suspend" \
                --mode=delay sleep infinity

The lock is held exactly as long as the QML Process object lives. uninhibit(cookie) is "kill this Process." The cookie is a locally-generated UUID mapped to the Process; systemd owns the fd, we never see it.

If process-per-inhibit proves fragile (e.g. merging N reasons into one D-Bus inhibitor), the next step up is a small long-lived Rust helper owning the fd, taking commands over stdin/socket — the same shape as RedFlag's privileged helper. Reach for it only when the simple model breaks; it is one dependency more.

8. The race that matters — lock before suspend

systemd's own inhibitor docs name this scenario: "a screen lock tool wants to bring up the screen lock right before suspend, and delay the suspend until that's complete."

  1. Take a standing delay-mode sleep inhibitor at shell startup.
  2. Watch PrepareForSleep via a persistent gdbus monitor --system process, parsed line-by-line — not polling. Polling latency eats the delay budget directly.
  3. On PrepareForSleep(true): drive the lock, wait for WlSessionLock.secure, release the delay inhibitor only then.
  4. On PrepareForSleep(false): immediately re-acquire — the old inhibitor is dead and there is a window between wake and next sleep where nothing holds the line.

Budget is InhibitDelayMaxUSec (commonly 5s). Fail closed: if the lock is not secure before the deadline, leave a diagnostic event and do not pretend the session locked. Must be timed on the Pixel 3 before it is a security claim — SDM845 lock-surface creation under load is not laptop timing.

9. Sensor readings are evidence, not fact

The tier system asks "does the user need to authenticate to see this." It never asks "can the system trust this without verification." Those are different questions and the gap between them is a real vector.

Clock time is genuinely ambient — it comes from a kernel authority. Sensor readings are observations: they arrive from hardware through a userspace daemon (iio-sensor-proxy) over the system bus. A compromised proxy or a spoofed signal feeds the shell arbitrary orientation, light, and proximity — and the shell acts on it without question.

This is not a data leak. It is a trust gate opened or closed by a liar. The lock surface decides when to show the credential gate based on lift-to-wake and proximity-out; an adversarial sensor drives that gate at a moment of its choosing. The sensor does not need to beat PAM — it only has to lie to the thing that decides whether PAM is ever asked.

This is the same shape as screenLocked vs screenLockSecure: the reading is not the reality, exactly as the lock request is not the compositor's acknowledgement. And it is the same shape RedFlag already answered — verify the key, not the messenger; keep the trust root off the box the attacker owns.

Consequence for the sensor service: souveraine-surface-003 currently scopes SensorService as a thin bridge that "exposes what the hardware reports, interpretation belongs to consumers." That wires an unverified input straight into a trust gate. Unreconciled — the open question is whether the service publishes raw readings or observations carrying:

  • provenance (which bus name, which hardware path)
  • cross-sensor verification (accelerometer says face-down, light sensor says bright — one of them is lying)
  • confidence (hardware noise is real; smoothing can mask injection)

The tier vocabulary likely needs a row the current matrix has no cell for — working name observed: this came from a sensor; treat it as evidence, not fact.

10. Binary compliance — the policy layer is advisory until it isn't

Everything above is policy: the shell asks, decides, refuses. Policy in userspace is advisory against anything that does not ask. A binary that talks to net.hadess.SensorProxy directly, opens the MPRIS bus, or reads the notification store never touches LockContentPolicy — it simply acts. Same hole as §9: a gate only gates what routes through it.

RedFlag already solved this shape and it points inward without modification: capability tokens over a resolved closure, signed by an authority that lives off the box, verified by a small privileged network-less executor, with kernel enforcement (eBPF / WDAC / ESF) ensuring the executor is the only permitted caller. Applied here: binaries carry an attestation, the session authority is the signer, and things that bypass the tier checks do not run.

RedFlag's load-bearing constraint #5 transfers directly: kernel stops are defense-in-depth, not a prerequisite. The policy layer means something before eBPF lands; eBPF raises the cost of bypass. Partial deployment still moves the device out of the soft-target category.

This is a reaching plan. It is written down because the unification is the point, not the individual pieces.

11. Why unification is paramount

Four faces of one system:

Face Role Where it lives today
Capability tiers the policy LockContentPolicy, StepUpAuth
Capability gate the enforcement RedFlag's helper/ — not yet pointed inward
Seed identity the signer src/core/identity/ (Ed25519, machine key + agent key)
memfs git DAG the audit trail src/core/memory/, every write a commit

Today these are separate bodies of work that share an author's instincts. The thing that makes SouveraineOS an OS rather than a shell is that the session authority and the binary authority are the same authority.

12. Watch list

  • One polkit agent only. Two agents racing the same authentication request is a documented failure mode, not a hypothetical. Audit what the base install pulls in.
  • Stray legacy daemons. A leftover gnome-settings-daemon / *-power-manager fragment watching PrepareForSleep or holding its own inhibitor produces double-locks and races that look like our bug but are two authorities colliding. Audit systemctl --user list-units and the session bus on both machines before trusting the suspend race.
  • Scope portals deliberately. xdg-desktop-portal-wlr already does screenshot/screencast well. Run it underneath; take over only the lock/inhibit-adjacent portal, later, on purpose.
  • Verify protocol availability per target. x86 and SDM845 separately. Both IdleMonitor and IdleInhibitor report unavailable on some builds.
  • SDDM-vs-greetd asymmetry is debt — deferred, not accepted. Laptop on SDDM and phone on greetd is the current state, not the target. The intent is one system across the board; two greeters is a seam that contradicts it. Not today's work, but do not close this as "two legitimate greeters for two form factors" — that framing is wrong and would let the seam calcify.