Skip to content

harden: session-bus + liveness fail-closed, zbus tokio pin, root-only D-Bus checks (v0.3.6)#64

Merged
ccross2 merged 1 commit into
mainfrom
security/hardening-batch
Jul 7, 2026
Merged

harden: session-bus + liveness fail-closed, zbus tokio pin, root-only D-Bus checks (v0.3.6)#64
ccross2 merged 1 commit into
mainfrom
security/hardening-batch

Conversation

@ccross2

@ccross2 ccross2 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What problem does this solve?

A security-hardening pass over visaged's authorization and runtime surface. Four issues, all defense-in-depth or fail-open → fail-closed corrections:

  1. The privileged D-Bus methods (Enroll, RemoveModel, ListModels) were root-only by the system-bus policy file alone. A missing, mis-scoped, or overly-permissive org.freedesktop.Visage1.conf — or running on the session bus — could let a non-root caller reach enrollment mutation or the enrollment listing. Verify already validated the caller UID in-process; these did not.
  2. VISAGE_SESSION_BUS=0 enabled session-bus mode (which skips caller-UID validation). The flag was read with env::var(..).is_ok(), so any value — including 0, the natural way to turn it off — enabled it: a fail-open trap.
  3. Passive liveness failed open on thin data. check_landmark_stability returned "live" when fewer than two landmark frames were available, so a match backed by a single detectable landmark frame bypassed the liveness gate.
  4. zbus ran on its default async-io executor while visaged awaits tokio primitives inside #[zbus::interface] handlers — a latent "no reactor running" panic if a reactor-bound tokio call is ever added, and a transitive dep could silently revert the whole process to async-io.

What changed

  • Root-only in-handler check on Enroll/RemoveModel/ListModels (require_root_caller), skipped on the session bus, mirroring Verify.
  • parse_session_bus: only a non-empty, non-"0" value opts in; unset/empty/"0" keep the secure system-bus default. Unit-tested.
  • Liveness fails closed on < 2 landmark frames; surfaced as a rate-limited non-match. frames_per_verify defaults to 3, so a live subject in normal lighting is unaffected.
  • Pinned zbus to tokio (default-features = false, features = ["tokio"]; pam-visage re-adds blocking-api, which runs on zbus's internal tokio runtime). Drops the async-io/smol stack from the tree.
  • AES-256-GCM NIST known-answer test + blob-format guard so a future aes-gcm upgrade can't silently change the on-disk format and orphan enrollments.

No public API or D-Bus wire-format changes (the #[zbus(header)]/#[zbus(connection)] params are injected, not part of the wire signature). Bumps to v0.3.6.

Testing

In the nix devshell: cargo build, cargo test (79 tests, incl. new session-bus, liveness fail-closed, and crypto KAT/blob-format cases), cargo clippy --workspace -- -D warnings, and cargo fmt --all -- --check — all clean. cargo tree -e features confirms zbus no longer pulls async-io.

… D-Bus checks (v0.3.6)

Security hardening batch on visaged's authorization and runtime surface.
No public API or D-Bus wire-format changes.

- Root-only in-handler check on Enroll/RemoveModel/ListModels — defense in
  depth over the system-bus policy file alone (a missing/mis-scoped policy, or
  the session bus, could otherwise widen access). Skipped on the session bus.
- VISAGE_SESSION_BUS=0 no longer enables session-bus mode (which skips D-Bus
  caller-UID validation). The env::var(..).is_ok() parse enabled it on ANY
  value — including "0", the natural way to turn it off — a fail-open trap. Now
  only a non-empty, non-"0" value opts in; unset/empty/"0" keep the secure
  system-bus default.
- Passive liveness fails closed on fewer than 2 landmark frames. It previously
  reported "live" (fail-open), so a match backed by a single detectable
  landmark frame bypassed the gate. frames_per_verify defaults to 3, so a live
  subject in normal lighting is unaffected.
- Pinned zbus to the tokio executor (default-features = false, features =
  ["tokio"]; pam-visage re-adds blocking-api) per zbus's own tokio-integration
  guidance. Removes the async-io / smol executor stack from the dependency tree
  and prevents a transitive dep from silently reverting the process to async-io.
- Added an AES-256-GCM NIST known-answer test plus an on-disk blob-format guard
  (12-byte nonce, ciphertext, 16-byte tag, AEAD tamper rejection) so a future
  aes-gcm upgrade cannot silently change the format and orphan enrollments.

Verified in the nix devshell: cargo build/test (79 tests) / clippy -D warnings
/ fmt all clean; feature audit confirms zbus no longer pulls async-io.

Claude-Session: https://claude.ai/code/session_012wU47GVzskAummZqgH6FgF

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ccross2 ccross2 merged commit 931cd98 into main Jul 7, 2026
3 checks passed
@ccross2 ccross2 deleted the security/hardening-batch branch July 7, 2026 22:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant