Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

### Changed

### Fixed

### Security

## [0.1.0] - 2026-05-17

Initial open-source release.
Expand All @@ -16,14 +26,30 @@ Initial open-source release.
- **Daemon**: Persistent process with model caching, ~200ms warm auth latency
- **CLI**: Unified `facelock` binary — setup, enroll, test, preview, bench, audit, and more
- **Anti-spoofing**: IR camera enforcement, frame variance checks, landmark liveness detection
- **Security**: Constant-time matching (subtle), AES-256-GCM encryption at rest, optional TPM key sealing, SHA256-verified models, persistent rate limiting, D-Bus method-level authorization, hardened PAM env handling
- **D-Bus**: System bus interface (`org.facelock.Daemon`) with deny-all policy and caller UID verification
- **systemd**: Service hardening (ProtectSystem, NoNewPrivileges, InaccessiblePaths)
- **GPU**: Runtime-selectable execution providers (CPU, CUDA, ROCm, OpenVINO) via `execution_provider` config — no compile-time flags
- **Setup wizard**: Interactive model-quality and inference-device selection, streaming download progress bar, only downloads the models actually selected in config
- **Status command**: Reports inference provider and ORT library location, enrolled face count for the current user, security posture (IR enforcement, liveness, `min_auth_frames`), and notification state (`73a5c00`)
- **Models**: Self-hosted ONNX assets distributed via GitHub release downloads (no third-party model fetches in the auth path)
- **Packaging**: deb, rpm, PKGBUILD (`facelock` and `facelock-git`), Nix flake, signed APT repository (TPM `main` + non-TPM `legacy` channels), systemd/D-Bus activation, OpenRC/runit/s6
- **Packaging**: deb, rpm, PKGBUILD (`facelock` and `facelock-git`), Nix flake, signed APT repository with two channels — `main` (TPM-enabled, Debian trixie+ / Ubuntu 25.04+) and `legacy` (non-TPM, Debian bookworm / Ubuntu 24.04) — systemd/D-Bus activation, OpenRC/runit/s6 (`c70999b`)
- **CI/CD**: Build/test/lint pipeline, TPM tests via swtpm, container PAM smoke tests, end-to-end `.deb` and `.rpm` package install validation
- **Documentation**: mdBook, man pages, ADRs, security posture assessment, threat model

### Security

- **Constant-time matching**: Embedding comparison via `subtle` crate to prevent timing side-channels
- **Encryption at rest**: AES-256-GCM software encryption for stored face embeddings
- **TPM key sealing**: Optional TPM-backed key protection for the encryption key
- **Model integrity**: SHA256 verification of ONNX model files at load time
- **Rate limiting**: 5 auth attempts per user per 60 seconds (default), enforced in daemon
- **D-Bus authorization**: Daemon verifies caller UID via `GetConnectionUnixUser` before executing methods
- **Enrollment restriction**: Root-required enrollment enforced in auth paths (`c01a655`)
- **PAM env hardening**: Hardened PAM environment handling to prevent injection (`c01a655`)
- **systemd hardening**: `ProtectSystem=strict`, `NoNewPrivileges`, `InaccessiblePaths`, and related service restrictions

### Fixed

- **PAM install output**: Conditional install messages — suppressed when PAM entry already present (`c12a970`)
- **PAM uninstall**: Uninstall now removes entries from all relevant PAM services, not just the primary one (`c12a970`)
Comment on lines +32 to +53

[0.1.0]: https://github.com/tyvsmith/facelock/releases/tag/v0.1.0
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ Facelock is a Cargo workspace with 11 crates:

| Crate | Type | Purpose |
|-------|------|---------|
| `facelock-core` | lib | Config, types, errors, IPC protocol, traits |
| `facelock-core` | lib | Config, types, errors, D-Bus interface, traits |
| `facelock-camera` | lib | V4L2 capture, auto-detection, preprocessing |
| `facelock-face` | lib | ONNX inference (SCRFD + ArcFace) |
| `facelock-store` | lib | SQLite face embedding storage |
| `facelock-daemon` | lib | Auth/enroll logic, rate limiting, liveness, audit |
| `facelock-cli` | bin | Unified CLI (`facelock` binary, includes `bench` subcommand) |
| `facelock-bench` | bin | Standalone benchmark and calibration utility |
| `pam-facelock` | cdylib | PAM module (libc + toml + serde, zbus only) |
| `pam-facelock` | cdylib | PAM module (libc, toml, serde, zbus only) |
| `facelock-tpm` | lib | Optional TPM encryption |
| `facelock-polkit` | bin | Polkit face authentication agent |
| `facelock-test-support` | lib | Mocks and fixtures for testing |
Expand Down Expand Up @@ -98,7 +98,7 @@ Read `docs/security.md` before implementing any auth-related code. Key rules:
- `security.require_ir` defaults to **true**. Never weaken this default.
- Frame variance checks must remain in the auth path.
- Model files are SHA256-verified at load time.
- IPC messages have size limits (10MB max). Never allocate unbounded buffers.
- IPC messages have size limits enforced by the D-Bus daemon (see `dbus/org.facelock.Daemon.conf`). Never allocate unbounded buffers.
- D-Bus system bus policy restricts daemon access.
- The PAM module logs all auth attempts to syslog.
- Rate limiting is enforced in the daemon (5 attempts/user/60s default).
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Facelock: Face Authentication for Linux

> **v0.1.0-alpha** — Pre-release. Under active development. Functional, daily-driveable, but experimental. APIs will change before 1.0. See [CHANGELOG.md](CHANGELOG.md).
> **v0.1.0** — Stable release. See [CHANGELOG.md](CHANGELOG.md) for details.

A modern face authentication system for Linux PAM. Provides Windows Hello-style facial auth with IR anti-spoofing, configurable as a persistent daemon or daemonless one-shot. All inference runs locally on your hardware -- no cloud services, no network requests, no telemetry. Your biometric data never leaves your machine.

Expand Down Expand Up @@ -113,15 +113,17 @@ facelock audit View structured audit log
## Architecture

```
facelock-core Config, types, errors, D-Bus interface
facelock-core Config, types, errors, D-Bus interface, traits
facelock-camera V4L2 capture, auto-detection, preprocessing
facelock-face ONNX inference (SCRFD detection + ArcFace embedding)
facelock-store SQLite face embedding storage
facelock-daemon Auth/enroll logic, rate limiting, liveness, audit
facelock-cli Unified CLI binary (facelock)
facelock-bench Standalone benchmark and calibration utility
facelock-tpm TPM-sealed key encryption, software AES-256-GCM
facelock-polkit Polkit authentication agent
pam-facelock PAM module (libc + toml + serde + zbus only)
facelock-test-support Mocks and fixtures for testing
```

### Face Recognition Pipeline
Expand Down
107 changes: 101 additions & 6 deletions book/src/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@

All commands are subcommands of the `facelock` binary.

## Global flags

The following flag is accepted by every subcommand (declared `global = true`):

| Flag | Description |
|------|-------------|
| `--config <PATH>` | Override the config file path. Takes precedence over `FACELOCK_CONFIG`. |

## facelock setup

Interactive setup wizard. Walks through camera selection, model quality, inference device (CPU/CUDA), model downloads, encryption, enrollment, and PAM configuration. Can also be run with flags for individual setup tasks.
Interactive setup wizard. Walks through camera selection, model quality, inference device (CPU / CUDA / ROCm / OpenVINO), model downloads, encryption, enrollment, and PAM configuration. Can also be run with flags for individual setup tasks.

```bash
facelock setup # interactive wizard
facelock setup --non-interactive # run wizard without prompts
facelock setup --systemd # install systemd units
facelock setup --systemd --disable # disable systemd units
facelock setup --pam # install to /etc/pam.d/sudo
Expand Down Expand Up @@ -49,6 +58,20 @@ facelock list --user alice # specific user
facelock list --json # JSON output
```

`--json` emits an array of objects:

```json
[
{
"id": 1,
"label": "office",
"user": "alice",
"created_at": 1700000000,
"embedder_model": "arcface_r50"
}
]
```

## facelock remove

Remove a specific face model by ID.
Expand Down Expand Up @@ -116,6 +139,7 @@ Run the persistent authentication daemon.

```bash
facelock daemon # use default config
facelock daemon -c /path/to/config.toml # short alias for --config
facelock daemon --config /path/to/config.toml
```

Expand All @@ -132,29 +156,100 @@ facelock auth --user alice --config /etc/facelock/config.toml

Exit codes: 0 = matched, 1 = no match, 2 = error.

## facelock tpm status
## facelock tpm

TPM integration status and management.

### facelock tpm status

Report TPM availability and configuration.

```bash
facelock tpm status
```

### facelock tpm seal-key

Seal the AES encryption key with the TPM, migrating from a plaintext keyfile to TPM-backed storage.

```bash
facelock tpm seal-key
```

### facelock tpm unseal-key

Unseal the AES key from the TPM back to a plaintext keyfile, migrating from TPM-backed to keyfile storage.

```bash
facelock tpm unseal-key
```

### facelock tpm pcr-baseline

Display the current PCR values for all configured PCR indices.

```bash
facelock tpm pcr-baseline
```

## facelock bench

Benchmark and calibration tools.

```bash
facelock bench cold-auth # cold start authentication latency
facelock bench warm-auth # warm authentication latency
facelock bench model-load # model loading time
facelock bench cold-auth # cold start authentication latency (model load + first auth)
facelock bench warm-auth # warm authentication latency (pre-loaded models, 10 iterations)
facelock bench preview # frame capture + face detection latency
facelock bench enrollment # time to capture and embed snapshots (dry run, embeddings not stored)
facelock bench model-load # ONNX model load time (SCRFD + ArcFace)
facelock bench calibrate # sweep FAR/FRR thresholds and recommend optimal value
facelock bench report # full benchmark report
```

`cold-auth`, `warm-auth`, `calibrate`, and `report` require enrolled faces. When encryption method is `tpm`, these subcommands require root.

## facelock encrypt

Encrypt all unencrypted embeddings in the database with AES-256-GCM.

```bash
facelock encrypt # encrypt using the configured key
facelock encrypt --generate-key # generate a new key file (or seal a new TPM key) WITHOUT re-encrypting embeddings
```

`--generate-key` only creates the key material. Run `facelock encrypt` (without the flag) afterwards to encrypt the embeddings.

## facelock decrypt

Decrypt all software-encrypted embeddings in the database (reverting AES-256-GCM encryption).

```bash
facelock decrypt
```

## facelock audit

View the structured audit log of authentication events.

```bash
facelock audit # show last 20 entries (default)
facelock audit -l 50 # show last 50 entries
facelock audit --lines 50 # long form
facelock audit -f # follow mode: stream new entries as they arrive
facelock audit --follow # long form
```

| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `--follow` | `-f` | false | Watch for new entries (like `tail -f`) |
| `--lines N` | `-l` | 20 | Number of recent entries to display |

## facelock restart

Restart the persistent daemon. On systemd systems, runs `systemctl restart facelock-daemon.service`. Otherwise, sends a D-Bus shutdown request and the daemon restarts on next use via D-Bus activation.

Requires root. If run interactively as a non-root user, the CLI prompts to re-run via `sudo`.

```bash
facelock restart
```
Expand All @@ -171,5 +266,5 @@ For commands that accept `--user`:

| Variable | Purpose |
|----------|---------|
| `FACELOCK_CONFIG` | Override config file path |
| `FACELOCK_CONFIG` | Override config file path for unprivileged CLI commands. Ignored by privileged PAM/root auth flows; use `--config` there. |
| `RUST_LOG` | Control log verbosity (e.g., `facelock_daemon=debug`) |
26 changes: 22 additions & 4 deletions book/src/configuration.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Configuration Reference

Facelock reads its configuration from `/etc/facelock/config.toml`. Override the path with the `FACELOCK_CONFIG` environment variable.
Facelock reads its configuration from `/etc/facelock/config.toml`. Override the path with the `FACELOCK_CONFIG` environment variable. Note: `FACELOCK_CONFIG` is ignored by privileged PAM and root auth flows, which always use either an explicit `--config` path or `/etc/facelock/config.toml`.

All settings are optional. Facelock auto-detects the camera and uses sensible defaults. The annotated config file at `config/facelock.toml` in the repository serves as the canonical example.

Expand All @@ -13,6 +13,11 @@ Camera settings.
| `path` | string (optional) | Auto-detect | Camera device path (e.g., `/dev/video2`). When omitted, Facelock auto-detects the best available camera, preferring IR over RGB. |
| `max_height` | u32 | `480` | Maximum frame height in pixels. Frames taller than this are downscaled to improve processing speed. |
| `rotation` | u16 | `0` | Rotate captured frames. Values: `0`, `90`, `180`, `270`. Useful for cameras mounted sideways. |
| `warmup_frames` | u32 | `2` | Frames to discard immediately after opening the camera to let exposure and gain stabilize. Device quirks may override this. |
| `dark_threshold` | f32 | `0.6` | Fraction of pixels that must be darker than `dark_pixel_value` before the frame is treated as unusably dark. |
| `dark_pixel_value` | u8 | `10` | Pixel brightness cutoff used by the dark-frame check. |
| `ir_emitter` | bool | `false` | Attempt to enable a controllable IR emitter when the camera opens. Only needed for hardware that does not auto-enable its IR LED. |
| `camera_release_secs` | u32 | `5` | Seconds to keep the camera open after daemon-mode auth before releasing it, to avoid repeated warmup cost on back-to-back requests. |

## [recognition]

Expand All @@ -24,8 +29,8 @@ Face detection and embedding parameters.
| `timeout_secs` | u32 | `5` | Maximum seconds to attempt recognition before giving up. Must be > 0. |
| `detection_confidence` | f32 | `0.5` | Minimum confidence for the face detector to report a detection. Lower values detect more faces but increase false positives. |
| `nms_threshold` | f32 | `0.4` | Non-maximum suppression threshold for overlapping detections. |
| `detector_model` | string | `"scrfd_2.5g_bnkps.onnx"` | ONNX detector model filename. Must exist in `daemon.model_dir`. |
| `embedder_model` | string | `"w600k_r50.onnx"` | ONNX embedder model filename. Must exist in `daemon.model_dir`. |
| `detector_model` | string | `"scrfd_2.5g_bnkps.onnx"` | ONNX detector model filename. Must exist in `daemon.model_dir`. Bundled models are verified against the manifest; custom models require `detector_sha256`. |
| `embedder_model` | string | `"w600k_r50.onnx"` | ONNX embedder model filename. Must exist in `daemon.model_dir`. Bundled models are verified against the manifest; custom models require `embedder_sha256`. |
| `execution_provider` | string | `"cpu"` | ONNX Runtime execution provider. Values: `"cpu"`, `"cuda"`, `"rocm"`, `"openvino"`. GPU providers require a GPU-enabled ONNX Runtime package installed on the system. |
| `threads` | u32 | `4` | Number of CPU threads for ONNX inference. |

Expand All @@ -50,14 +55,15 @@ Run `facelock test` to see your similarity scores, then set the threshold below
| High accuracy | `det_10g.onnx` (17MB) | `glintr100.onnx` (249MB) | ~266MB | ~40-50ms slower, best accuracy |

Run `facelock setup` to select a model tier interactively and download the required models.
If you point `detector_model` or `embedder_model` at a custom file, you must also set the matching SHA256 so the daemon can verify it at load time.

## [daemon]

Controls how the PAM module reaches the face engine.

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `mode` | string | `"daemon"` | `"daemon"` connects to a persistent daemon via D-Bus system bus (~150-600ms depending on camera state). `"oneshot"` spawns `facelock auth` per PAM call (slower, ~700ms+, no background process). |
| `mode` | string | `"daemon"` | `"daemon"` connects to a persistent daemon via D-Bus system bus (~200ms warm, ~600ms cold). `"oneshot"` spawns `facelock auth` per PAM call (slower, ~600ms+, no background process). |
| `model_dir` | string | `"/var/lib/facelock/models"` | Directory containing ONNX model files. |
| `idle_timeout_secs` | u64 | `0` | Shut down the daemon after this many idle seconds. `0` means never. Useful with D-Bus activation. |

Expand All @@ -77,6 +83,8 @@ Controls how the PAM module reaches the face engine.
| `require_ir` | bool | `true` | Require an IR camera for authentication. RGB cameras are trivially spoofed with a printed photo. Only set to `false` for development/testing. |
| `require_frame_variance` | bool | `true` | Require multiple frames with different embeddings before accepting. Defends against static photo attacks. |
| `require_landmark_liveness` | bool | `false` | Require landmark movement between frames to pass liveness check. Detects static images by tracking facial landmark positions across frames. Experimental; off by default. |
| `landmark_displacement_px` | f32 | `1.5` | Minimum pixel displacement for a landmark to count as "moving" between frames. Only used when `require_landmark_liveness` is true. |
| `landmark_min_moving` | u32 | `3` | Number of facial landmarks (out of 5) that must show movement to pass the liveness check. Only used when `require_landmark_liveness` is true. |
| `suppress_unknown` | bool | `false` | Suppress warnings for unknown users (users with no enrolled face). |
| `min_auth_frames` | u32 | `3` | Minimum number of matching frames required before accepting. Only applies when `require_frame_variance` is true. |

Expand All @@ -87,6 +95,13 @@ Controls how the PAM module reaches the face engine.
| `max_attempts` | u32 | `5` | Maximum auth attempts per user per window. |
| `window_secs` | u64 | `60` | Rate limit window in seconds. |

### [security.pam_policy]

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `allowed_services` | list of strings | `[]` | If non-empty, only these PAM services may use facelock. |
| `denied_services` | list of strings | `[]` | PAM services that must always skip facelock, even if otherwise allowed. |

## [notification]

Controls how authentication feedback is delivered.
Expand Down Expand Up @@ -117,6 +132,8 @@ Controls how face embeddings are encrypted at rest.
| `key_path` | string | `"/etc/facelock/encryption.key"` | Path to AES-256-GCM key file for `keyfile` method. |
| `sealed_key_path` | string | `"/etc/facelock/encryption.key.sealed"` | Path to TPM-sealed AES key for `tpm` method. |

With `method = "tpm"`, the 32-byte AES key is sealed by the TPM at rest. At daemon startup, the key is unsealed and held in memory. Embeddings use the same AES-256-GCM format as `keyfile` — no re-encryption needed when migrating between methods. Migration commands: `facelock tpm seal-key` (keyfile → tpm) and `facelock tpm unseal-key` (tpm → keyfile).

## [audit]

Structured audit logging of authentication events.
Expand All @@ -133,6 +150,7 @@ TPM 2.0 settings for sealing the AES encryption key. These settings apply when `

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `seal_database` | bool | `false` | Seal the SQLite database file with the TPM key in addition to the encryption key. |
| `pcr_binding` | bool | `false` | Bind sealed key to boot state (PCR values). |
| `pcr_indices` | list of u32 | `[0, 1, 2, 3, 7]` | PCR registers to verify on unseal. |
| `tcti` | string | `"device:/dev/tpmrm0"` | TPM Communication Interface. |
Loading
Loading