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

119 lines
6.3 KiB
Markdown

# 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.