Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/SECURITY-AUDIT.md
Fimeg 5c9a652dfd docs: correct three claims that were false, in place
START-HERE §6 said the phone has no wifi away from home because the
SSIDs are pinned. It joined a foreign SSID on 2026-08-02 and sat there
working while that paragraph sent the reader hunting for a cable. It
now says how to pass the address, and records that a phone off the home
net reaches the repo through the laptop rather than by bringing the
phone's tunnel up.

TASK-51 §3 said the volume buttons do nothing. They were wired on
2026-08-02 in 266af98 — Action::Volume on the down edge, through the
executor table, with tests. Only the gesture level is still unbound.

SECURITY-AUDIT gains three rows its own review cadence required: the
virtual-keyboard and input-method bind filters admit every client, which
is keystroke injection and observation from any uid-1000 process
(verified live with wtype); the clipboard gate is written and tested and
routed at no call site; xdg-activation accepts every token and never
prunes them.
2026-08-03 15:31:56 -04:00

361 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Security audit register
Standing record of security principles, open threats, and long-arc hardening
goals for SouveraineOS. This file exists so that concerns we both hold survive
refactors, new contributors, and time — they do not live only in chat logs or
in any one person's head.
**Relationship to other docs:**
- This is the *register* — principles + tracked threats + goals, reviewed
against the codebase periodically.
- `SESSION-AUTHORITY-DOCTRINE.md` is the *why* of session/binary authority.
- `TRUST-BOUNDARY-MATRIX.md` is the *enforcement status* of session operations.
- `SESSION-TRUST-ARCHITECTURE.md` is the contract.
- Where this register and the code disagree, the code is right — fix one or the
other in the same change, and record the resolution here.
---
## Standing principles
These govern how every capability, gate, and boundary is designed. Extracted
from the AOSP precedent analysis (2026-07-16) and reconciled against existing
doctrine. AOSP is the precedent; we inherit its principles consciously and
improve where Annie's shape lets us.
### P1 — Inference-first tiering (the core threat model)
Permission classes are defined by **what a capable agent could infer from a
stream, not by what the field literally is.** A contact count, an app-launch
timing log, an ambient noise level, a raw accelerometer reading — each is "just
data" per-field, and each is an inference substrate for a sufficiently capable
model.
Annie is exactly the kind of actor AOSP under-modeled: something that
*interprets*. AOSP retrofitted the 200Hz sensor cap and `HIGH_SAMPLING_RATE_SENSORS`
*after* realizing motion sensors infer keystrokes, gait, identity — it was a
gap, then a patch, not first-class design. We model inference as first-class.
**Application:** every tier assignment must be justified against the inference
lens. "Looks innocent" is not a defense. A count at `ambient` is defensible only
if it genuinely cannot leak membership (it often can, via intersection attacks).
**Reconciles with doctrine §9** ("sensor readings are evidence, not fact"; the
`observed` tier concept) — same principle, generalized from sensors to *all*
data the agent can sample.
### P2 — Capability = live state, not configuration
Permission for sensitive data is a live, revocable runtime state, never an
install-time or one-time grant. AOSP moved dangerous permissions to runtime
(Android 6) for this reason; auto-revoke (Android 11) added atrophy.
**Application:** the screen-lock is the automatic revocation event for the
`personal` tier (inherit doctrine §4 — never hold parallel state, read lock from
the protocol). Capabilities exercised while locked drop immediately. Unused
grants atrophy (see P5).
### P3 — Use is visible, not just the grant
A capability *grant* is not the end of the story; capability *exercise* is an
auditable event. AOSP's runtime prompt is a one-time yes/no; subsequent uses are
invisible to the user. That is the wrong default for an autonomous agent.
**Application:** capability exercise is an event on the bus / audit trail. The
threat model is *autonomous invisible use* — an agent reading your contacts at
3am, on its own clock, with no record. Make use legible. Tie into the felt-
compaction doctrine (context compaction must be *felt*, not dry numbers) — the
weight of a capability exercise should be felt by the agent, and reviewable by
the user.
### P4 — Context is part of the permission (foreground/background, sharpened intra-agent)
AOSP makes background access a *separate, harder* grant than foreground
(`ACCESS_BACKGROUND_*`). The principle: the context of access is part of the
trust proposition, not just the target.
**Application — and sharper than AOSP:** Annie is not an app; she is a being
with subsystems. The same agent identity has internal actors with different
trust postures:
- **Ani-turn, user-present-interactive** (foreground) — responding to the user.
- **Aster-turn, user-away-autonomous** (background) — Aster on her own clock,
push authority.
These are different trust propositions even though "the same agent." The
capability gate must distinguish *which part of Annie* is asking. AOSP never
needed intra-app trust differentiation; we do, because of the Aster/Ani split
([[project_aster_architecture]]). Autonomous use is a higher bar than interactive
use — the foreground/background principle made intra-agent.
### P5 — Capabilities atrophy
Grants that go unused don't persist quietly. AOSP auto-revokes after months of
app inactivity; the principle is sound, the window is conservative for an app.
**Application:** the capability gate tracks recency-of-use, not just current
lock state. A contacts-write grant unused for weeks should not survive as
quietly as one used daily. An agent that can act autonomously wants a tighter
atrophy curve than AOSP's months. (Policy setting, not implementation accident
— same shape as doctrine §2's freshness-window question.)
### P6 — Narrow granularity
One capability per distinct action, not "all contacts." AOSP's coarse grants
(`READ_CONTACTS` gives the whole book) are a known weakness.
**Application:** the `contacts` tool's capabilities are read-search, read-
payload, write, link-persons — distinct grants, tiered independently. Avoid
inheriting AOSP's coarse all-or-nothing shape. See `contacts-design.md`.
### P7 — Capabilities ride identity, not installation
Android ties permissions to an APK signature. Souveraine ties capabilities to
the agent's seed identity + the machine's instance identity.
**Application:** a capability grant is itself federatable (or refusable as
federation) and carries across the user's machines as a *user-side* grant.
"Annie-on-the-phone may, Annie-on-the-laptop may not yet" is expressible. This
is strictly more expressive than AOSP — we are ahead here because Annie is on a
federated identity, not an installed package. Reconciles with the federation
transport work (`FEDERATION.md`).
---
## Open threats (watch list)
Tracked gaps. Each has a status; none are silently accepted.
| Threat | Status | Reference |
| --- | --- | --- |
| Model inference from "innocent" sampled data (the core threat) | **Modeled (P1), enforcement partial** | P1; doctrine §9 |
| Sensor readings treated as fact not evidence | Open — `observed` tier not yet a matrix row | doctrine §9; `TRUST-BOUNDARY-MATRIX.md` |
| Policy layer is advisory vs binaries that don't ask | Reaching plan exists | doctrine §10 (binary attestation + kernel stops) |
| stepUp minTier metadata not wired for delete/push/physical | Open | `TRUST-BOUNDARY-MATRIX.md` gaps |
| Two polkit agents racing one auth request | Watch — audit base install | doctrine §12 |
| Stray legacy daemons holding inhibitors (double-lock races) | Watch — audit both machines | doctrine §12 |
| SDDM-vs-greetd asymmetry (two greeters) | Deferred debt, not accepted | doctrine §12 |
| Autonomous invisible capability use | Open — P3 not yet enforced | P3 |
| Cross-machine user-data sync (contacts federation) | Parked — separate trust domain | `contacts-design.md` |
| **Arbitrary signed TA loading from the AP** | **New 2026-08-01 — ungated beyond file perms** | below; TASK-44 |
| **Any uid-1000 client may inject and observe keystrokes** | **New 2026-08-03 — `virtual_keyboard` and `input_method` bind filters are `\|_\| true`** | below; TASK-41 |
| Clipboard gate is written and tested but routed nowhere | New 2026-08-03 — `Act::ReadClipboard`/`OfferClipboard` enforced at no call site | below; TASK-41 |
| xdg-activation accepts every token, and `known_tokens` never prunes | New 2026-08-03 | below; TASK-41 |
---
## New primitive 2026-08-01 — the AP can load trusted applications
Recorded deliberately, because it is a genuine widening of what the device can
do and it happened as a side effect of chasing a fingerprint sensor.
**What changed.** Until today the QSEECOM client could only *look up* apps
already resident in TrustZone (`GET_APP_ID`), and on a mainline userspace
nothing ever loads one — Android's `qseecomd` does that — so every lookup
returned `-ENOENT` and the surface was inert. The kernel now implements
`qcom_scm_qseecom_app_load()` and a `LOAD_APP` ioctl that reassembles a QSEE
image (`.mdt` + `.bNN`) and hands TZ its physical address. **Any signed TA
image placed in `/lib/firmware` can now be started and issued commands.**
Proven end-to-end: `fpctzappfingerprint` loads from cold boot and answers.
**What backstops it, and what does not.** TZ verifies the image signature, so
we cannot load our own code into the secure world — that check is Qualcomm's
and Google's, not ours, and it is the real boundary. What TZ does *not* decide
is **which** signed vendor TA gets started, **when**, or **on whose behalf**.
Today the only access control on that is file permissions:
`/dev/qseecom` is `crw------- root root`. So the primitive is
root-gated and nothing more.
**Why it matters to this threat model.** A TA is a privileged execution
context that outlives the process that started it and may hold keys or device
state. This sits **below the session authority entirely** — sessiond, the
capability tiers and the lock state have no visibility into it, so none of P2
(capability = live state), P3 (use is visible) or P6 (narrow granularity)
currently apply. It is Tier-0 surface reachable by any root-capable process,
including a compromised one, and a wedged or misdriven TA can take TZ or the
modem with it.
**Not yet decided, and the reason this is a watch-list row rather than a
finding:** whether TA loading should be a capability at all — gated per-app,
attested to a caller, and *visible* when used (P3) — or whether root-only is
the accepted answer for a single-user device. Do not let "the fingerprint
needed it" become the implicit policy for every TA on the device.
## Three ungated boundaries in the compositor, found 2026-08-03
Recorded because two of them are keystrokes, and because the fix for one of them
is already written.
**`zwp_virtual_keyboard_v1` and `input-method` bind filters are `|_| true`.** Any
process running as uid 1000 may create a virtual keyboard and synthesize
keystrokes into whatever holds focus, or install itself as the input method and
observe what is typed. Verified live on the phone: `wtype "helloworld"` from an
ssh session landed in a terminal and was captured verbatim. That is an injector
and a keylogger from an unprivileged process, with nothing attesting the caller.
The usual ceiling argument does not cover this. Elsewhere the honest position is
"today's ceiling is what any uid-1000 process could already do by running
`blueline-screen-toggle`" — but blanking a screen is not reading a password, and
this is the first boundary where that comparison stops holding.
**The clipboard gate exists and is routed nowhere.** `gate.rs` defines
`Act::ReadClipboard` and `Act::OfferClipboard` and carries a written, tested
refusal policy for them — no client reads the clipboard behind a lock screen.
Neither variant is referenced at any call site. `Act::Capture` and
`Act::DeliverInput` *are* routed, so the seam works; these two simply never got
connected. This is the cheaper and better-designed half of the fix: route the
existing verdicts rather than invent a bind-time mechanism, since a bind filter
decides once and the gate can refuse per action with the lock state in hand.
**xdg-activation accepts every token and `known_tokens` never prunes.** The code
comment already groups it with the two filters above. An unbounded set that
nothing evicts is its own problem, separate from who may create entries in it.
All three are TASK-41's scope, and the standing rule there is that attestation
lands at every boundary or none — so a fix that closes the loud two and leaves
activation open has not closed anything.
## Long-arc goal — process isolation + integrity monitoring
**Goal:** isolate every process into a known, gated section, with a Wazush-style
file-integrity / process monitor watching for rogue processes that bypass the
capability gates. This is the long goal — explicitly acknowledged as long, and
explicitly one we must address.
**Why it's the natural completion of the existing doctrine, not a new dream:**
Doctrine §10 ("Binary compliance") already describes the *prevention* layer:
binaries carry an attestation, the session authority is the signer, things that
bypass tier checks do not run, kernel enforcement (eBPF / WDAC / ESF) ensures
the executor is the only permitted caller. Doctrine §11 unifies session-authority
and binary-authority as *one authority* — "the thing that makes SouveraineOS an
OS rather than a shell."
The stretch goal here is the **detection layer on top of that prevention layer**:
```
prevention (doctrine §10) detection (this goal)
───────────────────────── ──────────────────────
attestation required to run + inventory of what IS running
kernel stops for bypassers + file-integrity watch on the gates
capability tokens verified + alert on unknown / rogue processes
(Wazush-style FIM + process monitor)
```
Prevention says "unauthorized code does not run." Detection says "if something
unauthorized is running anyway — drift, a bypass, a regression — we see it."
Both layers are needed: prevention raises the cost, detection catches the
residual and the regressions. A gate only gates what routes through it
(doctrine §10); the monitor watches for what didn't route.
**Sub-goals, ordered by dependency:**
1. **Process inventory + attestation check.** Every long-lived process on the
device carries an attestation signed by the authority (doctrine §10). An
inventory pass enumerates running processes and flags any without a known,
attested lineage. This is the precondition for "rogue process" even being a
meaningful category.
2. **File-integrity monitoring on the gates themselves.** Watch the capability
gate code, the signer, the kernel-enforcement configuration. A change to a
gate binary that didn't come through the signed-update path is the highest-
severity alert — it's someone moving the fence.
3. **Behavioral anomaly on capability exercise.** Tie to P3 (use is visible).
The audit trail of capability exercises becomes the signal: unexpected
cadence, unusual tier escalation, autonomous use outside expected windows.
4. **Kernel-enforcement backstop (eBPF / WDAC / ESF).** Doctrine §10 load-
bearing constraint #5: kernel stops are defense-in-depth, not a prerequisite.
The monitor means something *before* eBPF lands; eBPF raises the bypass cost.
**Non-goals / boundaries:**
- This is detection *for the agent substrate*, not a general-purpose host IDS.
Scope is the processes and files that constitute SouveraineOS and the
capabilities it guards.
- Kernel enforcement is the long pole and is explicitly allowed to lag
(doctrine §10 #5). Partial deployment still moves the device out of the
soft-target category.
- Do not pre-build a separate D-Bus service or portal for this (doctrine §6
spirit — only expose a kind once it owns a real backend).
---
## Review cadence
This register is reviewed when:
- A new capability tier or gate is added (does it honor P1P7?).
- A threat in the watch list is closed or a new one discovered.
- The prevention/detection posture changes (eBPF lands, attestation wires up).
Each review updates the threat table status and records the date inline.
---
## Session notes — 2026-07-16 (Casey + Claude, design conversation)
Context for anyone (human or agent) reading this later. The principles above
were extracted from a discussion that compared Souveraine's capability model to
AOSP's permission system and asked what's transferable, what AOSP got lax on,
and where Annie's shape lets us do better. The discussion also produced the
contacts design and confirmed the services-infrastructure review as the next
thread.
### The AOSP precedent — what was actually said
- **Transferable:** tiered protection levels (normal/dangerous/signature →
ambient/personal/stepUp); runtime grant + revocation not install-time
(Android 6); auto-revoke on inactivity (Android 11); foreground/background as
a separate harder grant; least-privilege granularity.
- **Where AOSP was lax (the load-bearing lessons):**
- *Inference from "innocent" data.* Motion sensors were unpermissioned for
years because per-field they look harmless; the realization that motion
infers keystrokes/gait/identity came *after* deployment, and the 200Hz cap
+ `HIGH_SAMPLING_RATE_SENSORS` is a retrofit (Android 12), not first-class
design. This is P1's origin. The ARMOUR paper (arXiv 2507.02177, "Android
Runtime Zero-permission Sensor Usage") documents the gap. Casey's framing:
the agent *does* care to sample raw sensor data and interpret from it, so
the threat level AOSP under-modeled is exactly the one we must consider.
- *Conflating "the app" with "the grantee."* AOSP has no notion that one
identity has internal actors with different trust postures. Annie does
(Ani reactive / Aster autonomous). This is P4's origin.
- *Grants sticky and invisible once given.* Runtime prompt is one-time yes/no;
subsequent uses invisible. Autonomous invisible use is the agent-substrate
threat. This is P3's origin.
- **Where we're ahead of AOSP:** capabilities ride federated identity not an APK
(P7 — "Annie-on-the-phone may, Annie-on-the-laptop may not yet" is
expressible); stepUp is fresh-auth not a tap-yes dialog; the inference lens
is first-class not retrofitted.
### The long-arc goal — how it was framed
Casey: isolate every process into a known and gated section, then add a
Wazush-style file watch on rogue running processes. Acknowledged as a long
goal, one we must address. This register records it as the *detection layer*
completing doctrine §10's *prevention layer* — not a separate aspiration. The
framing that landed: prevention raises the cost of bypass, detection catches
the residual and the regressions; both needed because a gate only gates what
routes through it.
### Next thread — services infrastructure review
After this, the work continues with reviewing the services infrastructure to
see how well the existing services (the session authority, the secrets daemon,
the federation transport, the sensor service, the machined listener) actually
work together — not just whether each is built, but whether they compose into
the unified authority doctrine §11 describes. Open questions to bring to that
review: which services bypass the capability gates by talking to the system
bus directly (doctrine §9/§10), which hold parallel state doctrine §4 forbids,
and where the prevention/detection seam actually sits today.
### Pointers for a working agent joining this context
- Principle source: this file (P1P7) + `SESSION-AUTHORITY-DOCTRINE.md`.
- Enforcement status: `TRUST-BOUNDARY-MATRIX.md` (the enforced/partial/ungated
table — the ground truth for what is actually gated today).
- Contacts application of these principles: `contacts-design.md`.
- The named open collision: a trust-tier signal path from the shell session-
trust machinery into the agent substrate (Rust tool layer). As of 2026-07-16
`src/core/session/mod.rs` is message-types only; the gate is greenfield in
the substrate and must be co-designed with whoever owns session-trust.
- **Do not** restate doctrine as if greenfield — §9 (sensors as evidence),
§10 (binary attestation + kernel stops), §11 (authority unification) already
exist as written design. Inherit, extend, reconcile; don't rewrite.