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.
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:
- Anyone can read it. Any AP process linking libssc can open
elmyra_rawtoday. Runningblueline-edge-sensedoes 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. - 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 anddial'sopen/close/toggle/entriesare the same hole from the other end (TASK-40 §3 already flagsentries()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_inputreport for a source the caller holds no grant for, withnot_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.