Watch
1
0
Fork
You've already forked SouveraineOS
0

souveraine-components: per-binary docs + RedFlag-shaped audit tracker

One page each for machined, secrets, sessiond — what each owns, what
crosses its boundary, and the open gaps before it reaches the RedFlag
capability-token bar. Gaps mapped to concrete RedFlag primitives (token,
closure hash, canonical message, KeyID, independent verifier, binary
integrity, eBPF) with file:line anchors, not doctrine-summary hand-waving.
audit-status.md rolls up priority: P0 unlock/handoff attestation, P1
machined token issuance, P2 secrets caller identity, P3 per-agent UIDs,
P4 binary watchdog shared-deferred with RedFlag SEC-022.
This commit is contained in:
Fimeg 2026-07-16 21:48:13 -04:00
commit 4235848bee
5 changed files with 427 additions and 0 deletions

View file

@ -0,0 +1,45 @@
# Souveraine components — substrate binaries
One page per secure Rust binary in the Souveraine substrate. These are the
system- and user-tier daemons that hold authority the shell and agents never
touch directly: machine identity, the secret service, and the session
authority. Each page names what the binary owns, what crosses its boundary,
and the open gaps before it reaches the RedFlag security bar.
The family is small on purpose. Adding a binary here means it holds
authority no other component should hold — signing keys, unlock verbs,
secret material. If a daemon does not meet that bar, it is not a component,
it is a library.
## Members
| Binary | Tier | Owns | Page |
| --- | --- | --- | --- |
| `souveraine-machined` | system | machine Ed25519 seed | [machined.md](machined.md) |
| `souveraine-secrets` | user (session) | `org.freedesktop.secrets` backed by the seed lineage | [secrets.md](secrets.md) |
| `souveraine-sessiond` | user (session) | ext-session-lock; the unlock verb | [sessiond.md](sessiond.md) |
## The security bar these are built toward
Every component is on a path to the same posture RedFlag already embodies
(SESSION-AUTHORITY-DOCTRINE §10/§11, SECURITY-AUDIT P1P7):
- **Binaries verify each other.** A component that asks another for an
authority action (sign, unlock, release a secret) must present an
attestation the target checks — a hash or capability token resolved over a
closure, not a name in a config file.
- **Capability tokens, not installation.** Authority rides the federated
seed identity (P7), not "this binary is in /usr/local/bin." The kernel
stops are defense-in-depth, not the gate (RedFlag constraint #5).
- **One authority.** Session authority and binary authority are the same
authority (§11). sessiond verifies capability tokens on the things that ask
it to unlock or step up; machined signs them; secrets releases against
them.
- **Seeds are preconditions.** No daemon creates identity. A missing seed
fails loudly; generation is a deliberate, guarded CLI action.
Where a component has not yet reached that bar, the gap is recorded in its
page under **Audit gaps** and rolled up in [audit-status.md](audit-status.md).
The intent is that the audit doc is the live tracker: when a gap closes, the
line moves there, and the component page's Audit gaps section is updated to
match.

View file

@ -0,0 +1,88 @@
# Component audit status — toward the RedFlag security bar
Live tracker. Each component page carries an **Audit gaps** section with the
detail; this page is the rollup: which RedFlag primitive each gap maps to,
and where it sits in priority. When a gap closes, move it here to "Closed"
and update the component page.
The bar is read from RedFlag's source, not the doctrine summary. The five
primitives that matter, with anchors into `~/Projects/RedFlag`:
| Primitive | What it is in RedFlag | Anchor |
| --- | --- | --- |
| **Capability token** | Ed25519-signed, forward-only (`Version`), operation-scoped, expiring authorization | `agent/internal/capability/token.go` |
| **Closure hash** | `sha256` over sorted/dedup `name@version#sha256` — binds token to exactly what it authorizes | `Token.ClosureHash()` |
| **Canonical message** | Deterministic field-delimited string that is signed/verified | `Token.CanonicalMessage()` |
| **KeyID fingerprint** | `hex(sha256(pubkey)[:16])` — binds a signature to a pinned key | `KeyIDFor()` |
| **Independent verifier** | The privileged executor verifies the token itself; never trusts the relay's claim | `helper/src/main.rs`, token-executor mode |
| **Binary integrity** | SHA-256 compare (`verify-binary`), exit-on-mismatch | `helper/src/main.rs:2226-2283` |
| **Kernel stop (defense-in-depth)** | eBPF on `execve`, denies outside a valid token context; not the gate | `docs/tasks/KERN-001` |
| **Least-privilege executor** | network-less, single-op, exact-match sudoers | `helper/src/main.rs` |
Two things to internalize from RedFlag's own status, because they set
expectations for Souveraine:
1. **Binary mutual attestation is deliberately unwired in RedFlag too**
(SEC-022: "kill-on-mismatch and phone-home are deliberately not wired").
The standing guarantee in both systems is DAC — `root`/owner-owned
binaries the running user cannot overwrite. The watchdog raises the cost
on top of that; it is not the gate.
2. **The token is the universal primitive, not just for installs.** RedFlag's
ARCH-001 direction is "every fleet operation is a capability" — nothing
runs on standing authority. Souveraine's equivalent: every authority verb
(sign, unlock, handoff, release-secret, step-up) should be a capability,
not a "the caller is local so it's fine."
## Priority order
### P0 — the unlock/handoff verb has no attestation
**Component:** sessiond. **Gap:** `shell_ready` / `lock` honored on any
same-uid caller; no token. **Primitive missing:** capability token +
independent verifier. **Why P0:** unlock-adjacent verbs are the highest-value
authority in the system; a same-uid process that reaches the socket can
induce a handoff or retake. **Closes when:** the shell presents a
machined-signed token (operation-scoped, short expiry, closure carrying the
shell binary hash) that sessiond verifies against the pinned machine pubkey
before honoring `shell_ready`. Depends on machined issuing tokens (P1).
### P1 — machined signs but does not issue tokens
**Component:** machined. **Gap:** signs raw `domain:payload` bytes; no
operation, expiry, closure, or KeyID. **Primitive missing:** capability
token (full), closure hash, canonical message, KeyID. **Why P1:** it is the
root authority; every other attestation depends on it minting real tokens.
**Closes when:** machined issues forward-only, expiring, operation-scoped
tokens over a canonical message that includes the authorized target (verb +
binary hash), advertises a KeyID fingerprint, and the signing/verification
logic is byte-identical across mint and verifier (RedFlag invariant:
`agent/internal/capability/token.go``helper/src/main.rs`).
### P2 — secrets releases on D-Bus policy, not caller identity
**Component:** secrets. **Gap:** any session-bus process can `GetSecrets`;
no per-caller capability, no item-scoping, no `SO_PEERCRED`. **Primitive
missing:** capability token with closure = item path; independent verifier.
**Why P2:** the data is highest-sensitivity but the trust boundary (the
unlocked user session) is currently accepted by design; closes when personal-
tier credentials require a step-up token (§10) honored by secrets, not just
session-unlocked.
### P3 — caller identity is uid 1000 for everything
**Cross-cutting.** **Gap:** `SO_PEERCRED` is logged (machined) or unread
(sessiond); every substrate caller is the same UID, so the audit trail
cannot distinguish Annie from a curl script. **Primitive it blocks:**
`AgentID`-equivalent — the token's "who is asking" field. **Closes when:**
per-agent UNIX accounts land (services-infrastructure review), making
`SO_PEERCRED` kernel-backed caller identity that the token binds to. This is
the enabler for P0/P1/P2, tracked separately because it is account
provisioning, not crypto.
### P4 — binary integrity watchdog (shared with RedFlag)
**All components.** **Gap:** no self/peer SHA-256 verification. **Primitive
missing:** `verify-binary` equivalent, wired. **Status:** deliberately
deferred, same as RedFlag SEC-022. DAC (`root`/owner-owned, non-overwritable)
is the standing guarantee. When the capability token lands (P1), the token's
closure should carry each component's expected hash so a swapped binary
cannot replay a valid old token — that is the point at which the watchdog
becomes load-bearing rather than ornamental.
## Closed
(none yet — component family documented 2026-07-16, all gaps open.)

View file

@ -0,0 +1,102 @@
# souveraine-machined
System-tier machine identity daemon. The root of the authority family — it
holds the machine's Ed25519 seed and hands out signatures, never key
material. Every other component that needs to prove machine identity asks
this one to sign.
Source: `src/bin/souveraine-machined.rs`, `src/machined/{protocol,server,signer,client}.rs`.
RedFlag analog: the server's `SigningService` (authority that mints and
signs) in `server/internal/services/capability_minter.go`.
## What it owns
- The machine Ed25519 seed at `/var/lib/souveraine/seed-id`.
- A Unix socket at `/run/souveraine/machined.sock` (group `souveraine`,
group-readable/writable).
The seed is a **precondition, never created by the daemon**. If no seed
exists, startup fails loudly. Identity creation lives in the deliberate,
guarded CLI verb `souveraine machine init`. Same doctrine as secrets and
sessiond; see `SeedId::load` in `core/identity/seed.rs`.
## What crosses the boundary
Requests are JSON-lines over the socket. Two verbs:
- **pubkey** — the machine's public key (safe to share).
- **sign { domain, payload_hex }** — a signature over domain-separated
bytes. The signed material is `{SIGNING_CONTEXT}:{domain}:{payload}`,
never caller-supplied raw. `domain` is restricted to ASCII
alnum/`-`/`_`/`.`/`,`, max 64 chars; the daemon refuses anything else.
The private key never leaves the process. Callers get signatures and the
public key — that is the entire surface.
## Identity of the caller
`SO_PEERCRED` is read on every connection; uid/gid/pid are logged on every
sign request and every refusal (`src/machined/server.rs:25-43`). Today this
is **observability, not enforcement** — any process in the `souveraine`
group can request a signature. That is the central audit gap (below).
## Today's security posture
- **Domain separation** prevents the daemon from being turned into an oracle
for arbitrary signatures: a signature under one domain does not validate
in another. This is the same principle as RedFlag's `CanonicalMessage`
(`agent/internal/capability/token.go`) — the signed bytes are a fixed,
field-delimited string, never caller-controlled in shape.
- Seeds are never daemon-generated; no background process can quietly mint
a machine identity.
- The socket is group-scoped, so membership in `souveraine` is the coarse
gate.
## Audit gaps (toward the RedFlag bar)
The target posture is RedFlag's capability-token model, read from its
source (not the doctrine summary):
- **Capability token = Ed25519-signed, forward-only, scoped, expiring.**
RedFlag's `Token` carries `Version` (forward-only — new versions add
fields, never reinterpret), `Operation`, `IssuedAt`/`NotBefore`/`ExpiresAt`,
and a `Signature` (`agent/internal/capability/token.go`). machined today
signs caller payloads but **does not issue scoped, expiring tokens** — it
signs raw `domain:payload` bytes with no operation, no expiry, no closure.
Gap: callers cannot present a *machined-issued* token to a verifier,
because machined mints signatures, not tokens.
- **Closure hash binds the token to exactly what it authorizes.** RedFlag's
`ClosureHash` is `sha256` over sorted, de-duplicated `name@version#sha256`
lines, so neither order nor duplicates change the digest. The Souveraine
analog for "what is this signature authorizing" does not exist yet — the
`payload_hex` is opaque to the daemon. When tokens land, the authorized
*thing* (a verb, a target binary hash, a session) must be inside the
canonical message the way RedFlag's closure is.
- **KeyID binds a signature to a key fingerprint.** RedFlag's `KeyIDFor` is
`hex(sha256(pubkey)[:16])`. machined exposes the pubkey but does not
advertise a fingerprint verifiers can pin; a rotating seed has no
discoverable, comparable identity today.
- **The verifier must be independent.** In RedFlag the privileged executor
(`helper/`) verifies the token itself — it does not trust the agent's
claim. Souveraine's analog: whatever consumes a machined signature must
verify it against the pinned pubkey, not trust the relay. No such verifier
is wired yet.
- **`SO_PEERCRED` is logged, not enforced.** Logging is necessary for audit
(P3: use is visible) but not sufficient as a gate. Per-agent UIDs (the
services-infrastructure review) make `SO_PEERCRED` kernel-backed *caller*
identity, which is what the token's `AgentID` field is in RedFlag — but
today every caller is uid 1000.
## Binary integrity of machined itself
RedFlag's binary-attestation primitive is `verify-binary <path>
<expected_sha256>` (`helper/src/main.rs:608-667`) — SHA-256 compare, exit
`EXIT_INTEGRITY=21` on mismatch. It is **deliberately not wired** in RedFlag
either (SEC-022: "kill-on-mismatch and phone-home are deliberately not
wired"). machined has no self- or peer-integrity check today. When this
lands, the same SHA-256-compare shape applies, and — as in RedFlag — the
DAC/sudoers layer (`root:root 0755`, non-overwritable by the running user)
is the integrity guarantee that holds *until* the watchdog is wired.
See [audit-status.md](audit-status.md) for where these land in priority
order.

View file

@ -0,0 +1,91 @@
# souveraine-secrets
User-tier Secret Service daemon. Implements `org.freedesktop.secrets`
(`org.freedesktop.Secret.Service`) on the session D-Bus so libsecret clients
— Chatty/libcmatrix, and our own `keyring-core` credential store — get a
secrets backend without gnome-keyring or KWallet.
Source: `src/bin/souveraine-secrets.rs`, `src/secrets/{service,collection,item,store,session,session_object,transport,dh,types,error}.rs`.
## The load-bearing choice
The store is keyed from the machine's existing Ed25519 `SeedId`, **not** a
passphrase-unlocked keyring. Concretely (`src/secrets/store.rs`): the seed
never leaves the process and is never used directly as the storage key.
Instead the daemon signs a fixed domain-separation string and runs the
(deterministic) Ed25519 signature through HKDF-SHA256 to derive a dedicated
AES-256 key. That keeps "sign federation events" (machined's domain) and
"encrypt local secrets" as **separate derived keys from one root**, rather
than reusing the same key material for two purposes.
The seed is a **precondition, never created by the daemon**. No seed at
`~/.souveraine/seed-id` → loud startup failure. Generation lives in the
guarded CLI verb `souveraine identity generate`.
## Why everything is "permanently unlocked"
Because the seed identity is resident in the process for its whole lifetime,
there is no separate "unlock the keyring" step. `OpenSession` always
succeeds immediately; `Unlock` is a no-op (`src/secrets/service.rs`). The
protection is not a passphrase gate at the D-Bus layer — it is that the
encrypted blob on disk is useless without the seed, and the seed lives in
machined's domain.
## What crosses the boundary
The Secret Service protocol's own crypto is implemented faithfully so
libsecret clients interoperate:
- **Key exchange** (`dh.rs`): `dh-ietf1024-sha256-aes128-cbc-pkcs7`
classic (non-EC) DH over RFC 2409 Second Oakley Group (MODP 1024-bit),
shared secret through HKDF-SHA256 → 16-byte AES-128 session key. Mirrors
libsecret's `secret-session.c` byte-for-byte.
- **Transport** (`transport.rs`): AES-128-CBC + PKCS7, IV = 16 fresh random
bytes per secret, wire shape `(session_path, iv, ciphertext, content_type)`.
- **Algorithms**: AES negotiated first, `plain` fallback only if the daemon
returns `NotSupported` — covering every real libsecret client (`session.rs`).
## Audit gaps (toward the RedFlag bar)
secrets is the furthest of the three from the RedFlag posture, because its
threat model is currently "the user's session is the trust boundary" rather
than "every caller is attested." Read against RedFlag's primitives:
- **No caller attestation on `GetSecret`.** Any process on the session bus
that can talk `org.freedesktop.secrets` can read items once the collection
is open. RedFlag's answer to "who is asking" is the capability token's
`AgentID` + `Operation` fields, verified independently by the executor.
secrets has no equivalent: there is no per-caller capability on
`GetSecrets`, no `SO_PEERCRED`-backed caller identity, no "this token
authorizes reading *this* item." The D-Bus policy is the only gate, and
D-Bus policy is installation-based, not identity-based (exactly what P7
rejects).
- **No closure/scoping.** RedFlag's token binds to a closure hash — the
secret is only released for the exact artifact set authorized. A secrets
equivalent would bind a token to a specific item or collection path, so a
token minted for Chatty's Matrix credential cannot read the email
credential. Nothing like this exists today.
- **`Unlock` is a no-op.** This is intentional given the seed-rooted design,
but it means there is no step-up / freshness at the secrets layer. Step-up
lives in sessiond (the `souveraine-stepup` PAM service); secrets should
eventually honor a step-up token before releasing personal-tier
credentials, not rely on "the session is already unlocked."
- **KeyID/fingerprint not pinned.** As with machined, the seed identity has
no advertised fingerprint (`hex(sha256(pubkey)[:16])`) that a caller or
auditor pins. A seed rotation silently changes what "the keyring" is.
- **Binary integrity.** Same as machined and RedFlag SEC-022: no
self/peer hash verification. DAC (`root`/owner-owned, non-overwritable)
is the standing guarantee.
## Federation note
Because the root is the machine seed, and seeds federate (FEDERATION.md),
there is a deliberate design tension to resolve in the review: a secret
encrypted under machine A's derived key is not readable on machine B even
though both share the federated *agent* identity. The seed-per-machine vs
seed-per-agent question (machined owns the *machine* seed; agents have
their own seed lineage) is exactly where secrets' trust model is decided.
Recorded here, not answered — it belongs in the services-infrastructure
review.
See [audit-status.md](audit-status.md) for priority.

View file

@ -0,0 +1,101 @@
# souveraine-sessiond
User-tier session authority daemon. Owns the ext-session-lock-v1 surface
and the unlock verb — the one thing in the system that can release the
session lock. The shell is a *client* of this authority, not the authority
itself.
Source: `src/bin/souveraine-sessiond.rs`, `src/sessiond/{protocol,server,lock,draw,auth}.rs`.
Companion design: `docs/session-authority-boot-order.md` (Phase C),
`docs/SESSION-AUTHORITY-DOCTRINE.md` §11.
## What it owns
- The ext-session-lock-v1 client that holds the session locked at boot,
before the shell exists, and after the shell dies.
- The PAM conversation for unlock — hand-rolled libpam FFI (`auth.rs`),
no wrapper crate (fewer deps for on-device builds).
- A Unix socket at `$XDG_RUNTIME_DIR/souveraine/sessiond.sock`, same-user
today (`0600`), machined-shaped JSON-lines protocol.
## The zero-gap handoff (load-bearing)
A lock client that drops its Wayland connection **without unlocking** leaves
the compositor holding the session locked. With Hyprland's
`misc:allow_session_lock_restore`, the next client inherits that locked
state. sessiond uses this for two transitions, both fail-closed:
1. **Boot handoff.** sessiond takes the lock first (splash dissolves onto a
locked panel), quickshell starts ~3s later, the bridge calls
`shell_ready`, sessiond drops its connection, the shell's lock inherits
— no unlocked instant between them.
2. **Crash retake.** The bridge connection is the shell's heartbeat. EOF on
that connection = shell dead. sessiond retakes the lock
(`allow_session_lock_restore`) and renders its own spartan PIN surface.
A shell crash while locked is no longer an ssh-only recovery.
The retake is self-correcting: if sessiond tries to lock against a *live*
shell lock, the compositor yields Finished/Denied — no deadlock.
## What crosses the boundary
JSON-lines over the socket (`protocol.rs`). Request ops: `status`,
`shell_ready`, `locked_ack`, `lock`. Phases: `holding`, `awaiting_shell_lock`,
`released`, `idle`. The `shell_ready` connection *is* the heartbeat; its EOF
is the retake trigger. Responses are guarded `ok`/`reason` shaped, same
convention as machined.
Unlock is never an RPC verb. The only path to unlock is the PAM
conversation on sessiond's own lock surface.
## Today's security posture
- **Lock-before-shell.** No desktop surface (NM dialog, bar, background)
can precede the lock at boot — the boot fail-open hole that motivated
this daemon.
- **Lock-past-shell-death.** Shell crash while locked retakes to a PIN, not
a locked brick.
- **PAM is the unlock path.** `auth include login` (same credentials as
console login), unprivileged via `unix_chkpwd`. No bypass verb.
- **Fail-closed default.** Default behavior on start is to lock; `--no-initial-lock`
is the explicit opt-out for dev.
## Audit gaps (toward the RedFlag bar)
sessiond is the component that most directly maps onto RedFlag's "capability
token over a resolved closure, signed by an authority, verified by the
executor" shape (SESSION-AUTHORITY-DOCTRINE §10), because *unlock is
exactly the kind of verb that should require a token*. Today it does not:
- **`shell_ready` has no caller attestation.** Any same-uid process that
can reach the socket can call `shell_ready` and induce a handoff, or call
`lock`. RedFlag's analog: the privileged executor verifies the token's
signature, `AgentID`, `Operation`, expiry, and closure hash *before*
acting — it does not trust "a process asked." sessiond trusts the
heartbeat's mere existence. The fix shape: the shell presents a
machined-signed capability token (operation=`session_handoff`, expiry
short, closure = the shell binary hash) that sessiond verifies against
the pinned machine pubkey before honoring `shell_ready`.
- **Inhibit / step-up verbs (§6/§10) are stubs.** sessiond is the natural
home for the `souveraine-stepup` PAM service and the portal Inhibit
backend, and for verifying capability tokens on session verbs. These are
named in the doctrine but not implemented; the socket protocol reserves
the space.
- **`SO_PEERCRED` not yet read.** machined logs uid/gid/pid on every
request; sessiond does not yet. Same-user-only today (`server.rs`
comment: "widens to a group grant deliberately when agents get accounts").
Per-agent UIDs (services review) turn this into kernel-backed caller
identity — the substrate of the capability gate.
- **Binary integrity.** As with the other two and RedFlag SEC-022: no
self/peer hash check. DAC is the standing guarantee. When the capability
token lands, the token's closure should carry sessiond's *own* expected
hash so a swapped binary cannot present a valid old token — RedFlag's
`ClosureEntry.SHA256` shape pointed inward.
- **eBPF (KERN-001 shape).** The lock gate's kernel-level stop is
ext-session-lock-v1 itself (a Wayland protocol the compositor enforces),
which is already a kernel-adjacent guarantee stronger than RedFlag's
eBPF-scaffold-for-execve. So sessiond's "defense-in-depth kernel stop"
is largely already present via the compositor; the gap is the
*token gate* on top, not the kernel enforcement underneath.
See [audit-status.md](audit-status.md) for priority.