Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/tasks/41-attested-producers.md
Fimeg 0100fa34c8 viewtop: assess denial as the base; the lock model is the one thing we cannot take
Our engine pins are already theirs. The embedder, the eleven-patch
DMSAA/external-texture series, KMS and the GBM atlas path are the
expensive parts and they are readable.

Their lock is a compositor-internal bool with PAM dlopened in-process
and no ext-session-lock served at all. It is invisible to logind and
sessiond, and it gates input against every caller including Annie.
viewtop serves the protocol; sessiond keeps the decision.

Also adds TASK-41 (attested producers), raised out of grip bring-up.
2026-07-28 13:41:37 -04:00

6.3 KiB

TASK 41 — Producers must be attested; one authority per input

Status: open. Raised 2026-07-28 (Casey), out of grip bring-up. Size: design first — this is doctrine §10 pointed at a concrete surface, and the wrong shape here is worse than no shape. Repos: souveraine (sessiond), Pixel3Arch (the producers), RedFlag (the reference implementation).

The hole, demonstrated rather than theorised

Both of these happened during the 2026-07-28 session, neither was challenged, and neither left a mark:

  • qs -c souveraine ipc --any-display call dial open — run from an SSH shell with no session, over the network. The dial opened.
  • sudo /usr/bin/blueline-chre-gesture squeeze — a script written minutes earlier, delivering a gesture as root. The shell routed it to the dial.

Nothing asked who was calling. Gestures.qml's deliver() logs the name and fires the action; sessiond's sensor_input and input ops take a reading from whoever connects to the socket. The producer/consumer split is good design and it is currently held together by convention alone.

This is doctrine §10 in one sentence: a gate only gates what routes through it. We built the routing and never built the gate.

Why grip is the sharp case

TASK-40 already says elmyra_raw is the highest-inference stream on the device — six strain gauges at 100 Hz, a typing side-channel and a grip biometric. Two distinct problems fall out, and they need different answers:

  1. Anyone can read it. Any AP process linking libssc can open elmyra_raw today. Running blueline-edge-sense does not create that exposure; serving the sensor does. Attesting our reader does not close it either — this one needs the SSC subscription itself gated, or the nanoapp path where the raw stream never leaves the SLPI.
  2. Anyone can forge its output. A squeeze is about to become a real input that resets the idle budget, writes the forensic trail, and opens a verb surface. An unattested producer means any process can synthesise user presence and user intent. That is what this task is about.

Keep them separate. (1) is a confinement problem and belongs to TASK-40. (2) is an authentication problem and belongs here.

What "one authority" has to mean

The reference model is RedFlag: capability tokens over a resolved closure, signed by an authority off the box, verified by a small privileged network-less executor, with kernel enforcement ensuring the executor is the only permitted caller. Pointed inward (doctrine §10, §11; session-authority-boot-order.md Phase C), the mapping is:

RedFlag here
signing authority off the box souveraine-machined — owns the machine Ed25519 seed, signs over a group-gated socket, never hands out key material
capability token a grant naming which producer may report which source
privileged executor souveraine-sessiond — already the single authority for device state
kernel enforcement eBPF, later; RedFlag constraint #5 says policy first

So: sessiond accepts input/sensor_input for a given source only from a producer holding a grant for that source, and its own binary carries the same attestation it checks. RefusalCode::NotPermitted already exists and is issued by nothing — doctrine §13 says it was put there so callers would branch on it before tokens landed. This is what makes it start being issued.

The cheap step that is not the answer, and is still worth taking first

SO_PEERCRED on sessiond's socket gives uid/gid/pid of the connecting process for free, and machined already does this. That is enough to refuse a report from a uid that is not the seat user, and to record which pid reported in the trail.

It is not attestation: pid is racy (the process can exec something else), uid says nothing about which binary, and anything running as the seat user passes. It closes the network-shell case and nothing beyond it. Take it as a first commit, name it as partial in the code, and do not let it close this task.

Design questions owed before code

  • Grant granularity. Per producer-binary, or per source? A reader that may report proximity should not thereby be able to report a squeeze.
  • Where the grant lives. machined signs, but something must hold the producer→source table, and it must survive a reprovision — so rootfs-overlay/, and therefore a package, not a live-configured file (TASK-25's whole lesson).
  • What an unattested producer gets. Refusal is obvious for a mutation. But a silently refused sensor report is indistinguishable from a dead sensor, which §10 of DEVICE-STATE-MACHINE spent a whole section making impossible. A refused report must be as loud as a down source.
  • Bootstrap. The producers start before the shell and around the same time as sessiond. A grant check that is not ready yet must fail closed without deadlocking the boot — and "fail closed" during boot means grip is dead until it resolves, which is the correct trade and should be stated, not discovered.
  • The shell side too. Gestures.qml's IPC target and dial's open/close/toggle/entries are the same hole from the other end (TASK-40 §3 already flags entries() as ungated reconnaissance). Quickshell IPC has no peer identity at all, which may mean the shell stops being directly reachable and gestures arrive only via sessiond.

Acceptance

  • sessiond refuses an input/sensor_input report for a source the caller holds no grant for, with not_permitted, and the refusal is in the trail.
  • The grant is verified against a machined-signed attestation of the producer binary, not against uid alone.
  • A refused report is as visible as a down source (§10), never a silent drop.
  • sessiond's own binary carries the attestation it checks.
  • Re-running the two demonstrations at the top of this file fails.

Connects to

Doctrine §9 (evidence not fact), §10 (binary compliance), §11 (one authority), §13 (not_permitted reserved for exactly this); session-authority-boot-order.md Phase C; SECURITY-AUDIT.md (prevention vs detection, and P3); TRUST-BOUNDARY-MATRIX.md; TASK-40 (the confinement half — do not merge them); TASK-13/TASK-36 (the producers this lands on); TASK-25 (anything durable must be packaged); souveraine-components/secrets.md, which records the identical gap on GetSecret and is the second instance of this pattern.