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.
19 KiB
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.mdis the why of session/binary authority.TRUST-BOUNDARY-MATRIX.mdis the enforcement status of session operations.SESSION-TRUST-ARCHITECTURE.mdis 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:
- 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.
- 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.
- 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.
- 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_SENSORSis 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.
- 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
- 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.rsis 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.