Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/tasks/12-gaze-reference-face-auth.md
Fimeg 7ec2479f83 blueline: consolidate wifi bring-up, adopt cellular/MMS path, task audit
wifi: bringup unit now carries the union ordering (slpi/adsp for TZ MSA,
rmtfs+tqftpserv for firmware serving — the 2026-07-20 boot proved the old
load unit's ordering incomplete) and supersedes blueline-wifi-load;
msa-release adopted into the repo (shutdown unload that keeps warm
reboots clean).

cellular: adopt blueline-clat service+script. clat script now pins a host
route to the Fido MMS proxy via the CLAT — with wifi up the wlan default
beat the clat default and MMS died off-carrier. mmsd config contract
documented (MMS_APN must match the bearer APN ltemobile.apn; shipped as
netsvcs which matches nothing).

tasks: retire 06 qtpim (no longer the contacts path), archive 10 (done),
index catches up on 11 (archived); 01 re-pointed at a contacts-store
decision.
2026-07-20 22:21:15 -04:00

10 KiB

TASK 12 — Face auth as a capability factor (Gaze is the reference, not the dep)

Status: open. Reference cloned to ~/Projects/gaze (GunduLabs/gaze @ c9c8ffb, MIT, v0.2.6, 2026-07-18). This doc scopes the SouveraineOS-owned face-auth factor modeled on it. We own the source — anything touching the permission/capability gating is in-tree, not vendored.

Goal

A first-class face-auth factor that plugs into the existing capability tier model as a step-up / unlock credential, symmetric with fingerprint and PIN. Gaze proves the pipeline is real and fully on-device; we build the SouveraineOS-native equivalent against the primitives we already have (PamContext, StepUpAuth, LockContext, LockContentPolicy), not a wrapper around gazed.

Two placements, one mechanism:

  1. Unlock. session.unlock() is already gated on LockContext PIN/PAM (TRUST-BOUNDARY-MATRIX row). Face becomes a PAM factor behind that same gate — a face mint unlocks, exactly as a fingerprint mint would.
  2. Step-up / in-place reveal. The doctrine's open slot: PamContext against souveraine-stepup "accepting fingerprint mints a short-lived grant," and LockContentPolicy.allowsOnLock() gains its third condition (|| hasFreshGrant('read')). Face is that same mint for a second factor type — reveal message/notification bodies on the lock surface without unlocking the session. (SESSION-AUTHORITY-DOCTRINE §2, currently "gated on the fingerprint sensor coming online (not yet true on blueline)." Face is unblocked by any working front camera; it does not wait on the FP driver.)

Face auth is not a new trust mechanism. It is one more factor behind the two existing gates. The doctrinal guarantees are unchanged: step-up never unlocks the session, never accepts an agent-supplied boolean, the grant is short-lived / in-memory / action-family-bound.

Context — what Gaze does (the reference)

Cloned at ~/Projects/gaze. Architecture worth copying, decisions worth re-making:

  • Pipeline (all on-device, no network): Camera → SCRFD detect → align → ArcFace embed → match → MiniFASNet-V2 liveness. Optional IR camera for high-security spoof resistance.
  • Process split (the part to copy structurally):
    • gazed — system daemon, owns the camera and embeddings, exposes com.gundulabs.Gaze on D-Bus. Stateful: claim/release, verify_start/stop, enroll_start/stop, signals face_status / verify_status carrying CaptureStatus (TooDark / etc.) and VerifyResult (Match / NoMatch).
    • pam-gaze — thin C-ABI PAM shim (pam_sm_authenticate).
    • pam-gaze-core — talks to the daemon over zbus, returns Match / NoMatch / Unavailable. The shim is dumb on purpose.
  • Tiered strictness — 5 levels (low/medium/high/maximum/custom) swap detector models (det_500m.onnx vs det_10g.onnx) and recognizer (w600k_mbf vs w600k_r50) + liveness threshold. The model-swap-by-tier idea maps cleanly onto our ambient/personal/stepUp tiers in reverse: higher trust decision → stricter pipeline, not a single global knob.
  • Presence gatesabort_if_ssh and abort_if_lid_closed refuse to fire when no user is physically present. This is the right instinct and aligns with our fail-closed lock policy: no camera / too dark / SSH session → Unavailable → don't unlock, stay secure.
  • Storage (reference, not to copy verbatim): /var/lib/gaze/users, config /etc/gaze/config.toml. We will not inherit this layout — see "What to build."

Files of record in the reference: gaze-core/src/{dbus.rs,config.rs,face.rs,detect.rs,camera.rs}, pam-gaze/src/lib.rs:113 (pam_sm_authenticate), pam-gaze-core/src/lib.rs (D-Bus auth flow, AuthOutcome).

Context — what SouveraineOS already has

  • SESSION-AUTHORITY-DOCTRINE.md §2-3 — PamContext is a standalone connection type (not baked into WlSessionLock); step-up points a second PAM context at /etc/pam.d/souveraine-stepup, accepts password or fingerprint in any order, mints a short-lived grant bound to an action family. Face slots in as a third accepted factor on the same context.
  • SESSION-TRUST-ARCHITECTURE.md — capability tiers ambient / personal / stepUp. personal gate is !screenLocked && !screenLockSecure. stepUp requires a recent grant.
  • TRUST-BOUNDARY-MATRIX.mdsession.unlock() → stepUp, LockContext PIN/PAM; StepUpAuth.requestAuth() → PAM conversation. Both are the insertion points for a face factor.
  • 02-lockscreen-rust-system.md — the glance/lock surface where an in-place reveal (face mint → hasFreshGrant('read')) would show notification/message bodies without unlocking.
  • 11-souveraine-secrets.md — secrets store; face embeddings are a biometric secret and belong in the same trust/at-rest discipline, not a parallel /var/lib tree.

What to build (full plan, not V1/V2)

One owned factor, end to end. No phased feature-stripping — design the final shape, then implement.

  1. In-tree face daemon (the gazed equivalent), SouveraineOS-native.

    • Owns the camera, runs SCRFD/ArcFace/MiniFASNet, stores embeddings.
    • NOT a D-Bus com.gundulabs.Gaze clone. Exposed over the existing Souveraine IPC surface (same bus agents and sessiond use), with a verify_* / enroll_* / status-signal shape derived from Gaze's CaptureStatus + VerifyResult — because that enum pair is well-thought-out and the Unavailable vs NoMatch distinction is load-bearing for fail-closed behavior.
    • Camera/tiling/liveness run as their own service the PAM path can reach; keep the PAM-facing shim dumb (Gaze's split is right).
  2. PAM factor, not a PAM silo. A face mint flows through the existing PamContextsouveraine-stepup and the lock's LockContext, not a new pam-gaze service with its own policy. Add face to the factor list /etc/pam.d/souveraine-stepup already accepts. The unlock path (session.unlock()) and the reveal path (StepUpAuthLockContentPolicy.allowsOnLock() third condition) both consume it identically — one factor, two effects, by virtue of which context it mints into. This is the doctrine's design; we're populating it, not extending it.

  3. Tiered pipeline strictness. Reuse Gaze's model-swap pattern but map it onto our capability tiers at the decision side: a grant that will unlock the session or satisfy stepUp runs the strict pipeline (det_10g + IR-if-available + tight liveness); an ambient read-mint can run the fast pipeline. The mapping is policy; the mechanism is two model sets behind the same daemon.

  4. Presence / fail-closed gates (copy Gaze's instinct, wire to our state). Refuse to capture when:

    • session is SSH/remote (our session authority already knows this),
    • lid closed / no front camera available (device-state manager, TASK-08),
    • liveness fails or capture is TooDark. Every one of these yields Unavailable, never Match. No unlock, no reveal. This matches the suspend-before-lock fail-closed invariant.
  5. Enrollment + storage. Enrollment UI in the shell settings app (SETTINGS-APP-PLAN). Embeddings stored under the souveraine-secrets discipline (TASK-11), at-rest protected, never logged, never transmitted. No /var/lib/gaze parallel tree — biometric data lives where secrets live.

  6. Audit. Face success/failure/unavailable events hit the existing SessionAudit append-only JSONL with hash chain (TRUST-BOUNDARY-MATRIX row "Session audit trail"). A face unlock is a security-relevant event and must be in the same trail as PIN/PAM unlocks — not a separate log.

  7. Models. Ship the ONNX models Gaze uses (SCRFD detector, ArcFace recognizer, MiniFASNet liveness) as packaged assets, same provenance discipline as any other binary blob in the rootfs. Pin versions; do not fetch at runtime.

Acceptance

  • Cold boot to unlock: a face match mints through souveraine-stepup / lock LockContext and unlocks the session, indistinguishable at the capability layer from a PIN or fingerprint unlock. Same audit row shape.
  • On the lock surface (post-TASK-02 glance): a face mint reveals message / notification bodies in place via hasFreshGrant('read') without unlocking the session. Lock is still requested; compositor still secure.
  • SSH session, lid closed, camera gone, too dark, or liveness fail → Unavailable → no unlock, no reveal, no audit "success." Fall through to PIN silently.
  • Enrollment, model swap by tier, and embedding storage live entirely in-tree under the secrets discipline. No gazed process, no /var/lib/gaze, no GunduLabs D-Bus name on the bus.

Do not

  • Do not vendor or wrap gazed. It's a reference. We own the source for anything in the permission-gating path. The PAM shim, the daemon, the policy — all in-tree.
  • Do not make face a new trust mechanism. No new tier, no new grant type. It is a factor behind PamContext / StepUpAuth, exactly as fingerprint is framed in the doctrine. The "two factors" framing is explicitly rejected in SESSION-AUTHORITY-DOCTRINE — don't reintroduce it.
  • Do not gate personal content on face alone. The reveal is hasFreshGrant('read'); whether that grant came from face, fingerprint, or PIN is irrelevant to LockContentPolicy. Keep the policy factor-agnostic.
  • Do not special-case face in the audit trail. Same JSONL, same hash chain, factor recorded as a field — not a parallel log.

Connects to

  • SESSION-AUTHORITY-DOCTRINE §2-3 (the slot this fills — "gated on the fingerprint sensor coming online"; face is unblocked by any camera).
  • TRUST-BOUNDARY-MATRIX (session.unlock, StepUpAuth.requestAuth, Session audit trail rows).
  • TASK-02 lockscreen glance (in-place reveal surface).
  • TASK-08 device-state manager (lid-closed / camera-present gate).
  • TASK-11 souveraine-secrets (embedding at-rest).
  • SETTINGS-APP-PLAN (enrollment UI).
  • Blocker, per-target: laptop front camera is straightforward; Pixel 3 (blueline) front-camera bring-up is not yet solved — face auth is moot on the phone until a userspace camera device exists for the daemon to grab. Not a reason to scope down; a reason to land laptop-first and have the phone light up when the camera does.

Reference provenance

  • Upstream: https://github.com/GunduLabs/gaze, commit c9c8ffb, MIT.
  • Local clone: ~/Projects/gaze (reference only; not a submodule, not a build input).
  • Cite file:line when borrowing a design decision (not code) from it.