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.
361 lines
19 KiB
Markdown
361 lines
19 KiB
Markdown
# 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 P1–P7?).
|
||
- 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 (P1–P7) + `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.
|
||
|