Chatty superseded by souveraine's Matrix sensorium. libcmatrix unused. qtpim contacts work stalled. All repos archived on Gitea. - STATE.md: remove component sections, update secrets consumers, decisions - Mark deprecated docs with headers - Update INVENTORY.md Tier 3 entries as deprecated - Update secrets.md consumer list
91 lines
4.8 KiB
Markdown
91 lines
4.8 KiB
Markdown
# souveraine-secrets
|
|
|
|
User-tier Secret Service daemon. Implements `org.freedesktop.secrets`
|
|
(`org.freedesktop.Secret.Service`) on the session D-Bus so libsecret clients
|
|
— souveraine-player, culver, 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 one app'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.
|