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.
119 lines
6.3 KiB
Markdown
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.
|