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

202 lines
10 KiB
Markdown

# 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 gates** — `abort_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.md` — `session.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*
`PamContext` → `souveraine-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 (`StepUpAuth` →
`LockContentPolicy.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.