diff --git a/docs/plans/2026-04-26-authforge-implementation.md b/docs/plans/2026-04-26-authforge-implementation.md index aaf6db0..fd0719f 100644 --- a/docs/plans/2026-04-26-authforge-implementation.md +++ b/docs/plans/2026-04-26-authforge-implementation.md @@ -16,30 +16,39 @@ ## Phase Roadmap +### Status legend + +- ✅ **Done** — code on `main`, unit/integration tests green, no Phase 14 VM smoke required (data/packaging/refactor phases that don't talk to real hardware or root-owned files). +- ✅ **Code complete** — code on `main`, unit/integration tests green; one or more acceptance gates require real hardware or `/usr/share/...` write access and are deferred to the Phase 14 PPA-build VM smoke. +- **Spec'd** — high-level scope in this doc; needs a step-level expansion plan (re-invoke `superpowers:writing-plans`) before execution. +- **Bundled** — landed together with another phase because they share a code path that can't be split atomically. + +### Roadmap + | Phase | Goal | Approx. Effort | Status | |---|---|---|---| -| 0 | Repo, workspace, CI, packaging skeleton | 2–3 days | ✅ **Done** (2026-04-26) | -| 1 | Daemon: D-Bus interface stub, systemd unit, polkit rules | 3–4 days | ✅ **Code complete** (2026-04-27); smoke gates deferred | +| 0 | Repo, workspace, CI, packaging skeleton | 2–3 days | ✅ **Done** (2026-04-26, tag `v0.1.0-scaffolding`) | +| 1 | Daemon: D-Bus interface stub, systemd unit, polkit rules | 3–4 days | ✅ **Code complete** (2026-04-27, tag `v0.1.0-phase1`) | | 2 | Daemon: storage layer (policy.d parser, pending flags, user db) | 3–4 days | ✅ **Done** (2026-04-27, tag `v0.2.0-storage`) | -| 3 | Daemon: FIDO2 enrollment backend via `ctap-hid-fido2` | 5–7 days | ✅ **Code complete** (2026-04-27, tag `v0.3.0-multi-lane`); real-HW smoke deferred | -| 4 | Daemon: policy apply via pam-auth-update wrapper | 4 days | ✅ **Done** (2026-04-27, tag `v0.4.0-parallel`) | -| 5 | Daemon: lockout simulator | 3 days | ✅ **Done** (bundled with Phase 4) | -| 6 | PAM module: `pam_authforge_pending.so` (C) | 3 days | ✅ **Code complete** (2026-04-27); pamtester smoke recipe in pam/TESTING.md | -| 7 | CLI: `authforgectl` | 4 days | ✅ **Code complete** (2026-04-27); 14 subcommands wired | -| 8 | GUI: app shell + Security Keys tab | 6–8 days | Spec'd | -| 9 | GUI: Policy tab + lockout-warning UX | 4 days | Spec'd | -| 10 | First-login flow: autostart entry + fullscreen modal | 4 days | Spec'd | -| 11 | TOTP support (PAM module + GUI tab) — feature flag | 5 days | Spec'd | -| 12 | Recovery flow (codes + emergency unlock) | 4 days | Spec'd | -| 13 | Debian packaging finalization (postinst, debconf, purge) | 4 days | Spec'd | -| 14 | Launchpad PPA build setup | 2 days | Spec'd | -| 15 | `authforge-gnome-integration` (Users panel shortcut) | 3 days | ✅ **Done** (2026-04-27, parallel subagent) | -| 16 | Ansible role `dangerousthings.authforge` | 2 days | ✅ **Done** (2026-04-27, parallel subagent) | -| 17 | Integration test harness (Multipass / LXD VM) | 4 days | Spec'd | -| 18 | User docs + onboarding site | 3 days | Spec'd | +| 3 | Daemon: FIDO2 enrollment backend via `ctap-hid-fido2` | 5–7 days | ✅ **Code complete** (2026-04-27, tag `v0.3.0-multi-lane`) — real-HW smoke deferred | +| 4 | Daemon: policy apply via pam-auth-update wrapper | 4 days | ✅ **Code complete** (2026-04-27, tag `v0.4.0-parallel`) — root-write smoke deferred | +| 5 | Daemon: lockout simulator | 3 days | ✅ **Done** — **Bundled** with Phase 4 | +| 6 | PAM module: `pam_authforge_pending.so` (C) | 3 days | ✅ **Code complete** (2026-04-27) — `pamtester` smoke recipe in `pam/TESTING.md` | +| 7 | CLI: `authforgectl` | 4 days | ✅ **Done** (2026-04-27) — 14 subcommands | +| 8 | GUI: app shell + Security Keys tab | 6–8 days | **Spec'd** — next single-lane bundle (see § Parallel Execution Lanes) | +| 9 | GUI: Policy tab + lockout-warning UX | 4 days | **Spec'd** — opens after Phase 8 | +| 10 | First-login flow: autostart entry + fullscreen modal | 4 days | **Spec'd** — needs Phase 6 ✓ + Phase 8 | +| 11 | TOTP support (PAM module + GUI tab) — feature flag | 5 days | **Spec'd** — opens now (needs Phase 2 ✓ + Phase 4 ✓) | +| 12 | Recovery flow (codes + emergency unlock) | 4 days | **Spec'd** — opens now (needs Phase 6 ✓) | +| 13 | Debian packaging finalization (postinst, debconf, purge) | 4 days | **Spec'd** — sequential tail; needs all binaries built | +| 14 | Launchpad PPA build setup | 2 days | **Spec'd** — sequential tail after 13; **VM smoke gate for all "Code complete" phases** | +| 15 | `authforge-gnome-integration` (Users panel shortcut) | 3 days | ✅ **Done** (2026-04-27) — parallel subagent (gnome lane) | +| 16 | Ansible role `dangerousthings.authforge` | 2 days | ✅ **Done** (2026-04-27) — parallel subagent (ansible lane) | +| 17 | Integration test harness (Multipass / LXD VM) | 4 days | **Spec'd** — sequential tail | +| 18 | User docs + onboarding site | 3 days | **Spec'd** — anytime slot | | **R** | **v1.0 release** | 1 day | — | -Total rough estimate: ~14 weeks of focused work for v1.0. Subsequent phases (Debian packaging, KDE port, RPM) are scoped in the design doc. +**Progress: 11 of 19 phases code-complete (58%).** Total original estimate was ~14 weeks of focused work for v1.0. Subsequent phases (Debian packaging, KDE port, RPM) are scoped in the design doc. --- @@ -56,81 +65,60 @@ The phase numbering is a dependency-respecting ordering, **not** a serial schedu │ ┌──────────┬───────────┼───────────┬──────────┐ │ │ │ │ │ - Phase 3 Phase 4+5 Phase 6 Phase 7 (Phase 15: anytime) - FIDO2 policy+ PAM C CLI (Phase 18: anytime) - backend lockout module authforgectl + Phase 3 ✅ Phase 4+5 ✅ Phase 6 ✅ Phase 7 ✅ Phase 15 ✅ + FIDO2 policy+ PAM C CLI GNOME shim + backend lockout module authforgectl (parallel agent) │ │ │ │ ↓ ↓ ↓ ↓ └──────┬───┘ │ │ ↓ │ │ ┌───────────┐ │ │ - Phase 8 Phase 9 (Phase 12 Phase 16 - GUI keys GUI policy recovery) Ansible - │ │ │ │ - └──────────┼────────────┘ │ - ↓ │ - Phase 10 │ - first-login │ - │ │ - └────────────┬───────────┘ - ↓ - Phase 13 - deb finalization - ↓ - Phase 14 - Launchpad PPA - ↓ - Phase 17 - integration tests - ↓ - Phase 18 - user docs - ↓ - R - v1.0 release + Phase 8 Phase 9 Phase 12 Phase 16 ✅ + GUI keys GUI policy recovery Ansible (parallel agent) + (next) (after 5) (open) + │ │ │ + └──────────┼────────────┘ + ↓ + Phase 10 + first-login (needs 6 + 8) + │ + ↓ + Phase 13 + deb finalization (needs every binary) + ↓ + Phase 14 + Launchpad PPA (VM smoke gate for "Code complete" phases) + ↓ + Phase 17 + integration tests + ↓ + Phase 18 + user docs + ↓ + R + v1.0 release ``` -### Parallelizable now (post-Phase 2) +### What's parallel-safe right now (post-Phase 4+5) -Four lanes touch disjoint file trees and can land in any order: +Three lanes touch disjoint file trees and can land in any order: | Lane | Phases | Touches | Why parallel-safe | |---|---|---|---| -| **A: FIDO2 backend** | 3 | `daemon/src/fido/`, small `dbus.rs` hook for signals | Self-contained; only consumes the storage façade from Phase 2. | -| **B: Policy + lockout** | 4 + 5 (must land together) | `daemon/src/policy_apply/`, `daemon/src/lockout/`, `dbus.rs` SetPolicy path | 5 gates 4 in the same call chain, so they go as one unit, but the unit doesn't touch FIDO2, PAM, or CLI. | -| **C: PAM module** | 6 | `pam/pam_authforge_pending.c` + `Makefile` | Pure C against the JSON-pending-flag contract Phase 2 already writes. Zero overlap with Rust code. | -| **D: CLI** | 7 | `cli/src/` | The D-Bus contract was settled in Phase 1; CLI is a thin client. | - -### Parallelizable in second wave (after first wave's lanes land) - -- **Phase 8** (GUI Keys tab) — opens after Lane A (Phase 3) lands. Needs real enrollment for the demo. -- **Phase 9** (GUI Policy tab) — opens after Lane B (Phase 5) lands. Needs the lockout simulator for the warning banner. -- **Phase 11** (TOTP, feature-flagged) — opens after Lane B. Independent of Phases 8/9. -- **Phase 12** (Recovery flow) — opens after Lane C (Phase 6). Independent of GUI lanes. -- **Phase 16** (Ansible role) — opens after Lane D (Phase 7). Independent of everything else. - -### Third wave (single dependency) - -- **Phase 10** (first-login flow) — needs Phase 6 (PAM backstop) + Phase 8 (GUI fullscreen). Cannot parallelize earlier. - -### Always-anytime slots - -- **Phase 15** (`authforge-gnome-integration` shim) — pure data deb; can be built whenever there's idle capacity. -- **Phase 18** (user docs) — best authored alongside their phase, but writing can start whenever a feature stabilizes. +| **GUI** | 8 (then 9, after 5 ✓) | `gui/src/` | The D-Bus contract is fully wired; GUI is a pure consumer. Needs `libgtk-4-dev` + `libadwaita-1-dev` on the box. | +| **Recovery** | 12 | `daemon/src/recovery.rs`, `pam/pam_authforge_pending.c` (recovery hook), small `dbus.rs` `GenerateRecoveryCode` body | The PAM module already has a `recovery_code_matches` stub returning 0; Phase 12 makes it real. Daemon side is brand-new file. | +| **TOTP** | 11 | `daemon/src/totp/`, `gui/src/views/totp.rs` (when 8 lands), `debian/control` Recommends | Feature-flagged at build time; only the daemon-side bits open right now. GUI tab waits on Phase 8. | ### Sequential tail (no parallelism wins) -- **Phase 13** (deb finalization) needs all binaries built. -- **Phase 14** (PPA) needs Phase 13. -- **Phase 17** (integration test harness) needs end-to-end binaries from at least one PPA build. -- **R** (release tag) is last. +Phase 10 (first-login) needs both Phase 6 ✓ and Phase 8. Phase 13 (deb finalization) needs every binary built. Phase 14 (PPA) chains off 13 — and is the **VM smoke gate** for everything currently in "Code complete" status. Phase 17 (integration tests) chains off 14. Phase 18 (user docs) and R (release) follow. -### Practical notes for executing this in parallel +### Lessons learned from this session's parallelism -- Subagents in `git worktree` is the canonical isolation mechanism, but Phase 1 hit a harness `cwd` quirk that broke worktree-based subagents. Re-test before relying on it; otherwise batch independent lanes in a single session. -- Phase 4 + Phase 5 must land in the same change because both modify the SetPolicy path; splitting them creates a window where SetPolicy applies without a lockout pre-check. -- Phase 8 (GUI) requires `libgtk-4-dev` + `libadwaita-1-dev` on the dev box. Treat it as its own physical build environment. -- Phase 6 (PAM C) requires `libpam0g-dev`. Build is a one-line `make -C pam`; no Rust toolchain involvement. +- **Subagents in `isolation: "worktree"` work** when permissions cover `cd && …`, `git -C …`, and broad `cargo *` / `make *`. Two subagents (Phase 15 gnome + Phase 16 Ansible) finished cleanly in 7 minutes wall-clock for ~250 lines of YAML/desktop config combined. +- **They don't work** when the harness sandbox blocks `desktop-file-validate` / `dpkg-checkbuilddeps` / `ansible-lint` — the Phase 15+16 agents both reported these as "skipped, hand-checked." Verification slips through to manual review of the diff. Acceptable for data/config phases; risky for code phases without strong test coverage. +- **Phase 4+5 had to land together** because both modify the SetPolicy code path; splitting them creates a window where SetPolicy applies without a lockout pre-check. +- **Worktrees share `target/` if and only if you don't isolate** via `CARGO_TARGET_DIR`. We didn't need to — each `git worktree add` produces its own working tree, so cargo target dirs are per-worktree by default. Concurrent cargo runs across worktrees would still race on the global `~/.cargo/registry` cargo lock, but in practice that's a brief synchronization, not a hang. --- @@ -180,6 +168,68 @@ Phase 2 expanded to step granularity at [2026-04-27-phase-2-storage.md](2026-04- - `set_policy_replaces_state` test renamed to `set_policy_persists_stacks` and asserts only `merged.stacks == pol.stacks` — `p2p_pair` seeds a `[storage]` block in `00-test.conf`, so the merged read is by design a superset of what `SetPolicy` wrote to `50-local.conf`. The new assertion captures the actual user-visible behavior. - `.gitignore` extended to ignore `.claude/` (Phase 1 left a stale agent worktree under `.claude/worktrees/`; ignoring prevents accidental submodule-style commits). +### Phase 3 + 6 + 7 closeout notes (2026-04-27) + +Bundled multi-lane execution against the plan at [2026-04-27-phase-3-6-7-multi-lane.md](2026-04-27-phase-3-6-7-multi-lane.md). Tagged `v0.3.0-multi-lane`. 12 commits (1 plan + 11 implementation). + +**Phase 3 — FIDO2 enrollment backend:** +- `daemon/src/fido/format.rs` — `PamU2fCred` -> `kh,pk,es256,+presence` with `hex::encode`; `CoseType` matching COSE alg `-7`/`-8`. +- `daemon/src/fido/authenticator.rs` — `Authenticator` trait with `discover()` and `make_credential(rp_id, user, pin)`. `AuthnError::{NoDevice, Cancelled, PinRequired, Backend}`. +- `daemon/src/fido/mock.rs` — `MockAuthenticator::with_one_yubikey()` for tests; counter-bumped per call. +- `daemon/src/fido/ctap.rs` — `CtapAuthenticator` wrapping `ctap-hid-fido2 = "3.5.9"`. Hardware-dependent; compile-clean gate only. +- `AppState::enroll(user, nickname)` replaces the Phase 2 add-credential stub: calls `authn.make_credential`, writes pam_u2f line, records userdb enrollment, returns `Credential` with `hex(keyHandle)` as id. +- D-Bus signals `DeviceFound`, `TouchRequired`, `EnrollmentSucceeded`, `EnrollmentFailed` emitted from `enroll_own` / `enroll_other`. + +**Phase 6 — `pam_authforge_pending.so`:** +- Replaces the Phase 0 stub. Reads `PAM_USER`, rejects unsafe usernames, `stat()`s `/var/lib/authforge/pending/`. Present → user-facing message + `PAM_AUTH_ERR`. ENOENT → `PAM_IGNORE`. Other errno → fail closed. +- Builds clean against `libpam0g-dev` with `-Wall -Wextra -Werror -fPIC -O2`. 16KB ELF. +- `pam/TESTING.md` + `pam/test/authforge.pamd` document the manual `pamtester` smoke recipe. + +**Phase 7 — `authforgectl` CLI:** +- 14 subcommands wired through D-Bus: `status`, `enroll`, `list`, `remove`, `policy {show,set,apply,validate}`, `pending {set,clear,list}`, `recovery {generate,list}`. Global `--json` flag. +- `cli/src/bus.rs` — owned-proxy wrapper via `zbus::Proxy::new_owned`. +- 5 clap-parser tests cover subcommand definition validity, status parse, policy-set with method list, enroll with user+nickname, `--json` global. + +**Plan deviations:** +- `HidInfo` in ctap-hid-fido2 3.5.9 has `pid`, `vid`, `product_string`, `info`, `param` — no `serial_number` / `path`. Used `product_string` for the device name and `HidParam::Path`/`VidPid` for the path label. +- `FidoKeyHidFactory` and `LibCfg` live at the crate root, not under `fidokey::`. +- `fido/mod.rs` carries `#![allow(dead_code)]` until Phase 8 GUI consumes the `DeviceFound` signal — `discover()` and `DiscoveredDevice` fields are wired via the trait but not yet called from non-test code. + +### Phase 4 + 5 closeout notes (2026-04-27) + +Tagged `v0.4.0-parallel`. Phase 4 + Phase 5 land together because both modify the SetPolicy code path; splitting would create a window where SetPolicy applies without a lockout pre-check. + +**Phase 4 — Policy apply via pam-auth-update:** +- `daemon/src/policy_apply.rs` — `PolicyApplier::new(profile_path, pam_auth_update)`. Renders the `pam-configs/authforge` profile (`Default: yes` when any stack requires fido2; `pam_u2f.so` + `pam_authforge_pending.so` chain when fido2 required, only the pending backstop otherwise) and runs `pam-auth-update --package`. +- **Stash-and-restore on failure:** prior profile contents are read before write, restored if `pam-auth-update` fails, and `pam-auth-update` is re-run to put the system back into its previous PAM state. +- 4 unit tests including a real-process rollback test against a failing `/bin/sh` shim. + +**Phase 5 — Lockout simulator:** +- `daemon/src/lockout.rs` — pure `simulate(&Policy, &[UserEnrollment]) -> Vec`. Iterates Required stacks, flags users with no enrolled credential of any required method. +- 5 unit tests: optional-mode skipped, required-with-unenrolled flagged, any-method-satisfies, empty registry, multi-stack violation count. +- `AppState::set_policy(p, force)` always runs the simulator first; if violations and `!force`, returns `PolicyApplyResult { applied: false, violations }` without writing or invoking pam-auth-update. Otherwise persists via `PolicyStore::save` and invokes `PolicyApplier::apply`. + +**Wire-protocol breaking change (pre-alpha, OK):** `SetPolicy(p)` → `SetPolicy(p, force: bool)`. CLI's `policy set` runs with `force=false` and surfaces violations as a non-zero exit + stderr list pointing the user at `policy apply --force-i-know-what-im-doing`. + +**StorageConfig grew two fields:** `pam_profile_path` and `pam_auth_update`, env-var driven (`AUTHFORGE_PAMCONF_PATH`, `AUTHFORGE_PAM_AUTH_UPDATE`). Tests inject a no-op `/bin/sh` shim into a tempdir. + +### Phase 15 + 16 closeout notes (2026-04-27) + +Both delivered by parallel subagents in `git worktree` isolation while Lane 1 (Phase 4+5) ran in the main session. ~7 minutes wall-clock for both, vs. ~15+ if I'd done them sequentially. + +**Phase 15 — `authforge-gnome-integration`:** +- `gnome-integration/io.dangerousthings.AuthForge.UsersPanel.desktop` — `.desktop` overlay tagged `X-GNOME-Settings-Panel=user-accounts`, `NoDisplay=true`, `OnlyShowIn=GNOME;`. Adds "Configure security…" inside `gnome-control-center` Users panel. +- `debian/control` paragraph for the new binary deb (`Architecture: all`, `Depends: authforge-gui, gnome-control-center`). +- `debian/rules` install line for `debian/authforge-gnome-integration/usr/share/applications/`. +- `gnome-integration/README.md` documents the GNOME 46/47 settings-panel-overlay mechanism and notes JS-extension support is deferred until a stable cross-version mechanism exists. + +**Phase 16 — Ansible role `dangerousthings.authforge`:** +- Full role at `ansible-role/`: meta, defaults, handlers, tasks, templates, README, examples/playbook.yml. +- Adds the PPA, installs `authforge-daemon authforge-pam authforge-cli` (gated `authforge-gui`), templates `policy.conf.j2` to `/etc/authforge/policy.d/90-fleet.conf`, runs `authforgectl pending set` for users in `authforge_pending_users`. +- Single restart handler for `authforge-daemon.service`. +- All 5 YAML files validated via `yaml.safe_load`. `ansible-lint` skipped — not installed in the harness sandbox. +- **Subagent-initiated divergences (kept):** added `authforge_ppa` override variable so internal-mirror users don't fork the role; pre-creates `/etc/authforge/policy.d/` defensively (idempotent); standard `ansible_managed` template header. + --- # Phase 0: Repository Scaffolding (FULLY DETAILED)