Cleans up scattered status markers into a single legend (Done / Code complete / Spec'd / Bundled), backfills v0.1.0-phase1 tag reference for Phase 1, marks the table consistently for what's actually shipping vs. what needs the Phase 14 VM smoke. Updates the lane diagram to show what's done vs. in flight, and adds a 'lessons learned from this session's parallelism' section capturing when subagents-in-worktrees actually work and when they slip on sandbox-blocked verification commands. Adds closeout notes for Phases 3, 4+5, 6, 7, 15, 16 alongside the existing Phase 1 and Phase 2 sections — what shipped, plan deviations, and any wire- breaking signature changes (SetPolicy gained a force flag in Phase 4+5).
1448 lines
62 KiB
Markdown
1448 lines
62 KiB
Markdown
# authforge Implementation Plan
|
||
|
||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||
>
|
||
> **Note on plan structure:** This is a multi-month project. Phase 0 is fully detailed at step granularity. Phases 1–18 are specified with goals, deliverables, file targets, and task lists at sufficient detail to begin implementation. Each subsequent phase should re-invoke `superpowers:writing-plans` to expand its tasks to step-level granularity at the moment that phase begins (state from prior phases informs the expansion).
|
||
|
||
**Goal:** Ship `authforge` as an apt package on a PPA that turns U2F / FIDO2 passkey / TOTP MFA on Ubuntu desktops into a two-command install with a polished libadwaita GUI, sane policy, and a first-login enrollment flow.
|
||
|
||
**Architecture:** Rust system daemon (`authforged`) exposes a D-Bus interface. A GTK4 / libadwaita app and a CLI are thin clients. A small C PAM module (`pam_authforge_pending.so`) backstops first-login enrollment. PAM stack edits go through `pam-auth-update`. Policy is drop-in TOML in `/etc/authforge/policy.d/`. Full design in `docs/plans/2026-04-26-authforge-design.md`.
|
||
|
||
**Tech Stack:** Rust 2021 edition (daemon, CLI), GTK4 + libadwaita via `gtk4-rs` and `libadwaita-rs` (GUI), C (PAM module), `libfido2` via `ctap-hid-fido2` crate, D-Bus via `zbus`, polkit, debhelper-compat 13 packaging, sbuild for clean builds, GitHub Actions CI, Launchpad PPA for distribution.
|
||
|
||
**Reference design:** [docs/plans/2026-04-26-authforge-design.md](2026-04-26-authforge-design.md)
|
||
|
||
---
|
||
|
||
## 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, 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 | ✅ **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 | — |
|
||
|
||
**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.
|
||
|
||
---
|
||
|
||
## Parallel Execution Lanes
|
||
|
||
The phase numbering is a dependency-respecting ordering, **not** a serial schedule. Multiple phases touch disjoint file trees and can be developed in parallel by independent contributors, subagents in worktrees, or a single session bouncing between lanes.
|
||
|
||
```
|
||
Phase 0 ✅ scaffold
|
||
│
|
||
Phase 1 ✅ D-Bus contract
|
||
│
|
||
Phase 2 ✅ storage layer
|
||
│
|
||
┌──────────┬───────────┼───────────┬──────────┐
|
||
│ │ │ │ │
|
||
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 (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
|
||
```
|
||
|
||
### What's parallel-safe right now (post-Phase 4+5)
|
||
|
||
Three lanes touch disjoint file trees and can land in any order:
|
||
|
||
| Lane | Phases | Touches | Why parallel-safe |
|
||
|---|---|---|---|
|
||
| **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 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.
|
||
|
||
### Lessons learned from this session's parallelism
|
||
|
||
- **Subagents in `isolation: "worktree"` work** when permissions cover `cd <worktree> && …`, `git -C <worktree> …`, 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.
|
||
|
||
---
|
||
|
||
### Phase 1 closeout notes (2026-04-27)
|
||
|
||
Phase 1 expanded to step granularity at [2026-04-26-phase-1-dbus.md](2026-04-26-phase-1-dbus.md). Code-side work is done; some acceptance gates require an Ubuntu desktop with libgtk-4-dev / libadwaita-1-dev / libfido2-dev / libpam0g-dev installed (see [BUILDING.md](../BUILDING.md)) and are deferred to a smoke pass before Phase 14 (PPA build).
|
||
|
||
**Done in Phase 1:**
|
||
- 9-method D-Bus interface (`io.dangerousthings.AuthForge1`) backed by an in-memory `AppState` with fixtures.
|
||
- polkit authorizer with `Permissive` (env-var bypass) + `System` (real `org.freedesktop.PolicyKit1.Authority`) modes.
|
||
- All wire types in `common`: `Mode`, `Method`, `Transport`, `Credential`, `StackPolicy`, `StorageBackend`, `Storage`, `Firstrun`, `Policy`, `PendingFlag`, `Violation`, `PolicyApplyResult`. All `Serialize + Deserialize + zvariant::Type`.
|
||
- systemd unit (`authforge-daemon.service`) with hardening (`NoNewPrivileges`, `ProtectSystem=full`, scoped `ReadWritePaths`).
|
||
- D-Bus activation file + system policy XML.
|
||
- polkit policy XML with all 7 actions from the design doc.
|
||
- `authforge-daemon.postinst` driving `daemon-reload` + `enable --now`.
|
||
- Packaging (`debian/rules` + `control`) installs the new assets and depends on `dbus`, `policykit-1`.
|
||
- CI installs `dbus` so the new tests run there too.
|
||
- 21 unit/integration tests across `common` (7) and `daemon` (14, including p2p D-Bus tests via `tokio::net::UnixStream::pair`).
|
||
|
||
**Deferred until a Phase 14 smoke run on a clean Ubuntu VM:**
|
||
- `debuild -us -uc -b` 5-deb output (needs libgtk-4-dev / libadwaita-1-dev / libfido2-dev / libpam0g-dev installed).
|
||
- `busctl introspect` / polkit prompt manual smoke.
|
||
- `systemctl status authforge-daemon` after install.
|
||
|
||
**Plan deviations recorded in the commit:**
|
||
- `PendingFlag.deadline_unix` is `u64` with `0` = no deadline (zvariant `Type` derive does not support `Option<T>`; sentinel matches the design doc's existing `deadline_hours = 0` convention).
|
||
- Sender → pid resolution for polkit `unix-process` subjects punts to early Phase 2 — Phase 1 passes pid 0, which the permissive authorizer ignores. Until pid resolution lands, the daemon is run with `AUTHFORGE_POLKIT_BYPASS=1` for any local smoke test (loud `warn!` log on startup).
|
||
|
||
### Phase 2 closeout notes (2026-04-27)
|
||
|
||
Phase 2 expanded to step granularity at [2026-04-27-phase-2-storage.md](2026-04-27-phase-2-storage.md). All 17 tasks landed in 8 commits, tagged `v0.2.0-storage`.
|
||
|
||
**Done in Phase 2:**
|
||
- `common/src/policy.rs` — `Policy::load_from_dir` (lex-ordered last-wins merge; ignores non-`.conf`; missing-dir → default) and `Policy::save_local` (writes `50-local.conf`, never touches siblings). Policy-related types moved here from `types.rs` with re-exports for stability.
|
||
- `daemon/src/storage/policy.rs` — `PolicyStore` wraps load/save; `watch()` returns a `(RecommendedWatcher, watch::Receiver<()>)` driven by `notify::recommended_watcher` (inotify on Linux).
|
||
- `daemon/src/storage/pending.rs` — JSON read/write/clear with explicit path-traversal rejection (`.`, `..`, `/`, `\0`, empty).
|
||
- `daemon/src/storage/credentials.rs` — `pam_u2f` line parser (`username:cred1[:cred2…]`), `CredentialsStore` with idempotent add-by-credId and remove-by-credId, `CredsPathResolver` dispatching central vs per-user paths (NSS errors fall back to `/home/<user>/…`).
|
||
- `daemon/src/storage/userdb.rs` — `rusqlite` (bundled SQLite) cache for the lockout simulator; `record_enrollment`, `drop_enrollment`, `users_with`, `has_any`.
|
||
- `daemon/src/dbus.rs` — adds `#[zbus(signal)] policy_changed`. Tests run against tempdir-backed `AppState` seeded with central-mode storage so credential writes don't try to touch `/home/<user>/…`.
|
||
- `daemon/src/main.rs` — spawns a watcher task that emits `PolicyChanged` on every `rx.changed()`; `AUTHFORGE_POLICY_DIR`/`_PENDING_DIR`/`_USERDB` env-var overrides for non-prod runs.
|
||
- `AppState::open(StorageConfig)` replaces `with_fixtures()`. All storage errors threaded through `StateError` and surfaced as `org.freedesktop.DBus.Error.Failed`.
|
||
|
||
**Test count:** 13/13 common + 26/26 daemon = **39 tests** (was 14+7=21 at end of Phase 1). Clippy + fmt clean.
|
||
|
||
**Plan deviations:**
|
||
- `nix::unistd::User::from_name` returns `Err(ENOENT)` rather than `Ok(None)` on this dev box for unknown users; the resolver now treats any lookup error as "fall back to `/home/<user>`" rather than propagating. Comment updated.
|
||
- `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/<user>`. 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<Violation>`. 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)
|
||
|
||
**Goal:** Empty-but-valid repo that builds, passes a trivial test, and produces installable (no-op) deb packages from `dpkg-buildpackage`. Locks in the project structure so all later phases drop into known places.
|
||
|
||
**Deliverables:**
|
||
- Cargo workspace with crates: `daemon`, `cli`, `common` (shared types).
|
||
- Debian packaging skeleton in `debian/` that builds the metapackage + 5 component debs.
|
||
- C source tree for the PAM module under `pam/`.
|
||
- GUI placeholder under `gui/` (Meson + Rust target).
|
||
- GitHub Actions CI: `cargo build`, `cargo test`, `cargo clippy`, `debuild --no-sign`, `lintian`.
|
||
- `README.md` with build instructions.
|
||
|
||
### Task 0.1: Initialize git repo and license
|
||
|
||
**Files:**
|
||
- Create: `/home/work/VSCodeProjects/ubuntu_fido/.gitignore`
|
||
- Create: `/home/work/VSCodeProjects/ubuntu_fido/LICENSE` (Apache 2.0)
|
||
- Create: `/home/work/VSCodeProjects/ubuntu_fido/README.md`
|
||
|
||
**Step 1:** Run `git init` in the project directory.
|
||
|
||
```bash
|
||
cd /home/work/VSCodeProjects/ubuntu_fido
|
||
git init -b main
|
||
```
|
||
|
||
**Step 2:** Write `.gitignore`:
|
||
|
||
```gitignore
|
||
target/
|
||
*.deb
|
||
*.buildinfo
|
||
*.changes
|
||
*.dsc
|
||
*.tar.xz
|
||
debian/.debhelper/
|
||
debian/files
|
||
debian/*.substvars
|
||
debian/*.debhelper.log
|
||
debian/authforge*/
|
||
build/
|
||
.vscode/
|
||
*.swp
|
||
```
|
||
|
||
**Step 3:** Write Apache 2.0 `LICENSE` file (standard text — copy from https://apache.org/licenses/LICENSE-2.0.txt). Author line: `Copyright 2026 Dangerous Things, LLC`.
|
||
|
||
**Step 4:** Write minimal `README.md`:
|
||
|
||
```markdown
|
||
# authforge
|
||
|
||
Turnkey U2F / FIDO2 passkey / TOTP MFA for Ubuntu desktops.
|
||
|
||
## Install (end users)
|
||
|
||
sudo add-apt-repository ppa:dangerousthings/authforge
|
||
sudo apt install authforge
|
||
|
||
## Build from source
|
||
|
||
See `docs/BUILDING.md`.
|
||
|
||
## Status
|
||
|
||
Pre-alpha. See `docs/plans/` for design and roadmap.
|
||
```
|
||
|
||
**Step 5:** Initial commit.
|
||
|
||
```bash
|
||
git add .
|
||
git commit -m "Initial commit: license, gitignore, readme"
|
||
```
|
||
|
||
### Task 0.2: Create Cargo workspace
|
||
|
||
**Files:**
|
||
- Create: `Cargo.toml` (workspace root)
|
||
- Create: `rust-toolchain.toml`
|
||
- Create: `.cargo/config.toml`
|
||
|
||
**Step 1:** Write `Cargo.toml`:
|
||
|
||
```toml
|
||
[workspace]
|
||
resolver = "2"
|
||
members = ["common", "daemon", "cli"]
|
||
|
||
[workspace.package]
|
||
version = "0.1.0"
|
||
edition = "2021"
|
||
license = "Apache-2.0"
|
||
authors = ["Dangerous Things <ops@dangerousthings.com>"]
|
||
repository = "https://github.com/dangerousthings/authforge"
|
||
rust-version = "1.78"
|
||
|
||
[workspace.dependencies]
|
||
zbus = "4"
|
||
serde = { version = "1", features = ["derive"] }
|
||
serde_json = "1"
|
||
toml = "0.8"
|
||
anyhow = "1"
|
||
thiserror = "1"
|
||
tracing = "0.1"
|
||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||
tokio = { version = "1", features = ["full"] }
|
||
clap = { version = "4", features = ["derive"] }
|
||
ctap-hid-fido2 = "3"
|
||
nix = { version = "0.28", features = ["user", "process", "fs"] }
|
||
```
|
||
|
||
**Step 2:** Write `rust-toolchain.toml`:
|
||
|
||
```toml
|
||
[toolchain]
|
||
channel = "1.78"
|
||
components = ["rustfmt", "clippy"]
|
||
profile = "minimal"
|
||
```
|
||
|
||
**Step 3:** Write `.cargo/config.toml`:
|
||
|
||
```toml
|
||
[build]
|
||
rustflags = ["-D", "warnings"]
|
||
```
|
||
|
||
**Step 4:** Verify with `cargo metadata --format-version=1 > /dev/null` (will fail until the member crates exist; that's the next task).
|
||
|
||
### Task 0.3: Create `common` crate
|
||
|
||
**Files:**
|
||
- Create: `common/Cargo.toml`
|
||
- Create: `common/src/lib.rs`
|
||
- Create: `common/src/policy.rs`
|
||
- Create: `common/src/types.rs`
|
||
|
||
**Step 1:** `common/Cargo.toml`:
|
||
|
||
```toml
|
||
[package]
|
||
name = "authforge-common"
|
||
version.workspace = true
|
||
edition.workspace = true
|
||
license.workspace = true
|
||
|
||
[dependencies]
|
||
serde = { workspace = true }
|
||
toml = { workspace = true }
|
||
thiserror = { workspace = true }
|
||
```
|
||
|
||
**Step 2:** `common/src/lib.rs`:
|
||
|
||
```rust
|
||
pub mod policy;
|
||
pub mod types;
|
||
```
|
||
|
||
**Step 3:** `common/src/types.rs`:
|
||
|
||
```rust
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||
#[serde(rename_all = "lowercase")]
|
||
pub enum Mode {
|
||
Disabled,
|
||
Optional,
|
||
Required,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||
#[serde(rename_all = "lowercase")]
|
||
pub enum Method {
|
||
Fido2,
|
||
Totp,
|
||
}
|
||
```
|
||
|
||
**Step 4:** `common/src/policy.rs` — empty module for now, populated in Phase 2:
|
||
|
||
```rust
|
||
// Policy parsing implemented in Phase 2.
|
||
```
|
||
|
||
**Step 5:** Write a failing test in `common/src/types.rs`:
|
||
|
||
```rust
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn mode_serializes_to_lowercase() {
|
||
let m = Mode::Required;
|
||
let s = serde_json::to_string(&m).unwrap();
|
||
assert_eq!(s, "\"required\"");
|
||
}
|
||
}
|
||
```
|
||
|
||
This needs `serde_json` as a dev-dep; add `[dev-dependencies] serde_json = { workspace = true }` to `common/Cargo.toml`.
|
||
|
||
**Step 6:** Run `cargo test -p authforge-common`. Expected: PASS.
|
||
|
||
**Step 7:** Commit.
|
||
|
||
```bash
|
||
git add common Cargo.toml rust-toolchain.toml .cargo
|
||
git commit -m "Add common crate with shared Mode and Method types"
|
||
```
|
||
|
||
### Task 0.4: Create `daemon` crate skeleton
|
||
|
||
**Files:**
|
||
- Create: `daemon/Cargo.toml`
|
||
- Create: `daemon/src/main.rs`
|
||
|
||
**Step 1:** `daemon/Cargo.toml`:
|
||
|
||
```toml
|
||
[package]
|
||
name = "authforged"
|
||
version.workspace = true
|
||
edition.workspace = true
|
||
license.workspace = true
|
||
|
||
[[bin]]
|
||
name = "authforged"
|
||
path = "src/main.rs"
|
||
|
||
[dependencies]
|
||
authforge-common = { path = "../common" }
|
||
zbus = { workspace = true }
|
||
tokio = { workspace = true }
|
||
tracing = { workspace = true }
|
||
tracing-subscriber = { workspace = true }
|
||
anyhow = { workspace = true }
|
||
```
|
||
|
||
**Step 2:** `daemon/src/main.rs`:
|
||
|
||
```rust
|
||
use anyhow::Result;
|
||
use tracing::info;
|
||
|
||
#[tokio::main]
|
||
async fn main() -> Result<()> {
|
||
tracing_subscriber::fmt()
|
||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||
.init();
|
||
info!("authforged {} starting", env!("CARGO_PKG_VERSION"));
|
||
// D-Bus service registration in Phase 1.
|
||
Ok(())
|
||
}
|
||
```
|
||
|
||
**Step 3:** `cargo build -p authforged`. Expected: success.
|
||
|
||
**Step 4:** Commit.
|
||
|
||
```bash
|
||
git add daemon
|
||
git commit -m "Add daemon crate skeleton"
|
||
```
|
||
|
||
### Task 0.5: Create `cli` crate skeleton
|
||
|
||
**Files:**
|
||
- Create: `cli/Cargo.toml`
|
||
- Create: `cli/src/main.rs`
|
||
|
||
**Step 1:** `cli/Cargo.toml`:
|
||
|
||
```toml
|
||
[package]
|
||
name = "authforgectl"
|
||
version.workspace = true
|
||
edition.workspace = true
|
||
license.workspace = true
|
||
|
||
[[bin]]
|
||
name = "authforgectl"
|
||
path = "src/main.rs"
|
||
|
||
[dependencies]
|
||
authforge-common = { path = "../common" }
|
||
clap = { workspace = true }
|
||
anyhow = { workspace = true }
|
||
zbus = { workspace = true }
|
||
tokio = { workspace = true }
|
||
```
|
||
|
||
**Step 2:** `cli/src/main.rs`:
|
||
|
||
```rust
|
||
use anyhow::Result;
|
||
use clap::Parser;
|
||
|
||
#[derive(Parser)]
|
||
#[command(name = "authforgectl", version, about = "Manage authforge configuration")]
|
||
struct Cli {
|
||
#[command(subcommand)]
|
||
cmd: Cmd,
|
||
}
|
||
|
||
#[derive(clap::Subcommand)]
|
||
enum Cmd {
|
||
Status,
|
||
}
|
||
|
||
#[tokio::main]
|
||
async fn main() -> Result<()> {
|
||
let args = Cli::parse();
|
||
match args.cmd {
|
||
Cmd::Status => println!("authforgectl: not yet implemented"),
|
||
}
|
||
Ok(())
|
||
}
|
||
```
|
||
|
||
**Step 3:** `cargo build` (whole workspace). Expected: success.
|
||
|
||
**Step 4:** Commit.
|
||
|
||
```bash
|
||
git add cli
|
||
git commit -m "Add cli crate skeleton"
|
||
```
|
||
|
||
### Task 0.6: Create PAM module skeleton
|
||
|
||
**Files:**
|
||
- Create: `pam/Makefile`
|
||
- Create: `pam/pam_authforge_pending.c`
|
||
|
||
**Step 1:** `pam/pam_authforge_pending.c`:
|
||
|
||
```c
|
||
#define PAM_SM_AUTH
|
||
#include <security/pam_modules.h>
|
||
#include <security/pam_ext.h>
|
||
#include <stdio.h>
|
||
|
||
PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags,
|
||
int argc, const char **argv) {
|
||
(void)flags; (void)argc; (void)argv;
|
||
pam_syslog(pamh, LOG_INFO, "authforge_pending: stub - allowing");
|
||
return PAM_IGNORE; /* implemented in Phase 6 */
|
||
}
|
||
|
||
PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags,
|
||
int argc, const char **argv) {
|
||
(void)pamh; (void)flags; (void)argc; (void)argv;
|
||
return PAM_SUCCESS;
|
||
}
|
||
```
|
||
|
||
**Step 2:** `pam/Makefile`:
|
||
|
||
```makefile
|
||
CFLAGS ?= -Wall -Wextra -Werror -fPIC -O2
|
||
LIBDIR ?= /usr/lib/$(shell dpkg-architecture -qDEB_HOST_MULTIARCH)/security
|
||
|
||
pam_authforge_pending.so: pam_authforge_pending.c
|
||
$(CC) $(CFLAGS) -shared -o $@ $< -lpam
|
||
|
||
install: pam_authforge_pending.so
|
||
install -D -m 0644 pam_authforge_pending.so $(DESTDIR)$(LIBDIR)/pam_authforge_pending.so
|
||
|
||
clean:
|
||
rm -f pam_authforge_pending.so
|
||
|
||
.PHONY: install clean
|
||
```
|
||
|
||
**Step 3:** Build it:
|
||
|
||
```bash
|
||
cd pam && make && cd ..
|
||
```
|
||
|
||
Expected: produces `pam/pam_authforge_pending.so` (~10 KB). Requires `libpam0g-dev` (will be in build-deps).
|
||
|
||
**Step 4:** Commit.
|
||
|
||
```bash
|
||
git add pam
|
||
git commit -m "Add PAM module stub"
|
||
```
|
||
|
||
### Task 0.7: Create GUI placeholder
|
||
|
||
**Files:**
|
||
- Create: `gui/Cargo.toml`
|
||
- Create: `gui/src/main.rs`
|
||
- Create: `gui/data/io.dangerousthings.AuthForge.desktop`
|
||
- Create: `gui/data/io.dangerousthings.AuthForge.svg` (1KB placeholder icon)
|
||
|
||
**Step 1:** Add `gui` to workspace members in root `Cargo.toml`.
|
||
|
||
**Step 2:** `gui/Cargo.toml`:
|
||
|
||
```toml
|
||
[package]
|
||
name = "authforge-gui"
|
||
version.workspace = true
|
||
edition.workspace = true
|
||
license.workspace = true
|
||
|
||
[[bin]]
|
||
name = "authforge"
|
||
path = "src/main.rs"
|
||
|
||
[dependencies]
|
||
gtk = { package = "gtk4", version = "0.8" }
|
||
adw = { package = "libadwaita", version = "0.6" }
|
||
authforge-common = { path = "../common" }
|
||
anyhow = { workspace = true }
|
||
```
|
||
|
||
**Step 3:** `gui/src/main.rs` — minimal libadwaita "Hello" window:
|
||
|
||
```rust
|
||
use adw::prelude::*;
|
||
use gtk::glib;
|
||
|
||
const APP_ID: &str = "io.dangerousthings.AuthForge";
|
||
|
||
fn main() -> glib::ExitCode {
|
||
let app = adw::Application::builder().application_id(APP_ID).build();
|
||
app.connect_activate(|app| {
|
||
let win = adw::ApplicationWindow::builder()
|
||
.application(app)
|
||
.default_width(720)
|
||
.default_height(540)
|
||
.title("Authentication")
|
||
.build();
|
||
let header = adw::HeaderBar::new();
|
||
let toolbar = adw::ToolbarView::new();
|
||
toolbar.add_top_bar(&header);
|
||
let status = adw::StatusPage::builder()
|
||
.icon_name("dialog-password-symbolic")
|
||
.title("Authentication setup")
|
||
.description("Phase 0 placeholder. Real UI in Phase 8.")
|
||
.build();
|
||
toolbar.set_content(Some(&status));
|
||
win.set_content(Some(&toolbar));
|
||
win.present();
|
||
});
|
||
app.run()
|
||
}
|
||
```
|
||
|
||
**Step 4:** `.desktop` file at `gui/data/io.dangerousthings.AuthForge.desktop`:
|
||
|
||
```desktop
|
||
[Desktop Entry]
|
||
Name=Authentication
|
||
GenericName=Security Keys & MFA
|
||
Comment=Manage U2F/FIDO2 keys and authentication policy
|
||
Exec=authforge
|
||
Icon=io.dangerousthings.AuthForge
|
||
Terminal=false
|
||
Type=Application
|
||
Categories=Settings;Security;
|
||
Keywords=mfa;u2f;fido2;passkey;totp;security;
|
||
StartupNotify=true
|
||
```
|
||
|
||
**Step 5:** Placeholder SVG icon — a simple gray key glyph; sourced from any open icon set under a compatible license. Save at `gui/data/io.dangerousthings.AuthForge.svg`.
|
||
|
||
**Step 6:** Build (assumes GTK4 / libadwaita dev packages installed: `libgtk-4-dev`, `libadwaita-1-dev`).
|
||
|
||
```bash
|
||
cargo build -p authforge-gui
|
||
```
|
||
|
||
Expected: success.
|
||
|
||
**Step 7:** Commit.
|
||
|
||
```bash
|
||
git add gui Cargo.toml
|
||
git commit -m "Add GUI crate skeleton with libadwaita placeholder window"
|
||
```
|
||
|
||
### Task 0.8: Debian packaging skeleton
|
||
|
||
**Files:**
|
||
- Create: `debian/changelog`
|
||
- Create: `debian/control`
|
||
- Create: `debian/copyright`
|
||
- Create: `debian/rules`
|
||
- Create: `debian/source/format`
|
||
- Create: `debian/compat` (or use `debhelper-compat (= 13)` in control)
|
||
- Create: `debian/authforge.install`
|
||
- Create: `debian/authforge-daemon.install`
|
||
- Create: `debian/authforge-daemon.service`
|
||
- Create: `debian/authforge-pam.install`
|
||
- Create: `debian/authforge-cli.install`
|
||
- Create: `debian/authforge-gui.install`
|
||
|
||
**Step 1:** `debian/changelog`:
|
||
|
||
```
|
||
authforge (0.1.0-1) UNRELEASED; urgency=low
|
||
|
||
* Initial packaging skeleton.
|
||
|
||
-- Dangerous Things <ops@dangerousthings.com> Sun, 26 Apr 2026 12:00:00 +0000
|
||
```
|
||
|
||
**Step 2:** `debian/source/format`:
|
||
|
||
```
|
||
3.0 (native)
|
||
```
|
||
|
||
**Step 3:** `debian/control` — declares all 5 binary packages and their relationships:
|
||
|
||
```
|
||
Source: authforge
|
||
Section: admin
|
||
Priority: optional
|
||
Maintainer: Dangerous Things <ops@dangerousthings.com>
|
||
Build-Depends:
|
||
debhelper-compat (= 13),
|
||
cargo,
|
||
rustc (>= 1.78),
|
||
libpam0g-dev,
|
||
libgtk-4-dev,
|
||
libadwaita-1-dev,
|
||
libfido2-dev,
|
||
pkg-config
|
||
Standards-Version: 4.6.2
|
||
Homepage: https://github.com/dangerousthings/authforge
|
||
Vcs-Browser: https://github.com/dangerousthings/authforge
|
||
Vcs-Git: https://github.com/dangerousthings/authforge.git
|
||
|
||
Package: authforge
|
||
Architecture: any
|
||
Depends: authforge-daemon (= ${binary:Version}),
|
||
authforge-pam (= ${binary:Version}),
|
||
authforge-cli (= ${binary:Version}),
|
||
${misc:Depends}
|
||
Recommends: authforge-gui (= ${binary:Version})
|
||
Suggests: authforge-gnome-integration
|
||
Description: Turnkey FIDO2/U2F/TOTP MFA for Ubuntu (metapackage)
|
||
Installs the full authforge stack: daemon, PAM module, CLI, and
|
||
(if recommended) GUI.
|
||
|
||
Package: authforge-daemon
|
||
Architecture: any
|
||
Depends: ${shlibs:Depends}, ${misc:Depends}, libpam-u2f, libfido2-1
|
||
Description: System daemon for authforge MFA management
|
||
Provides the privileged D-Bus service that orchestrates enrollment,
|
||
policy edits, and lockout-prevention checks.
|
||
|
||
Package: authforge-pam
|
||
Architecture: any
|
||
Depends: ${shlibs:Depends}, ${misc:Depends}, libpam-runtime
|
||
Description: PAM module backstopping authforge first-login enrollment
|
||
Refuses authentication when /var/lib/authforge/pending/<user> exists
|
||
and the user has not completed first-login MFA setup.
|
||
|
||
Package: authforge-cli
|
||
Architecture: any
|
||
Depends: ${shlibs:Depends}, ${misc:Depends}, authforge-daemon
|
||
Description: CLI for authforge (admin and fleet)
|
||
Configure policy, enroll on behalf of users, generate recovery codes.
|
||
|
||
Package: authforge-gui
|
||
Architecture: any
|
||
Depends: ${shlibs:Depends}, ${misc:Depends}, authforge-daemon
|
||
Description: GTK4/libadwaita UI for authforge
|
||
End-user-facing settings panel for enrollment and policy.
|
||
```
|
||
|
||
**Step 4:** `debian/rules` (executable, mode 0755):
|
||
|
||
```makefile
|
||
#!/usr/bin/make -f
|
||
export DH_VERBOSE = 1
|
||
export CARGO_HOME = $(CURDIR)/target/cargo-home
|
||
|
||
%:
|
||
dh $@
|
||
|
||
override_dh_auto_build:
|
||
cargo build --release --workspace
|
||
$(MAKE) -C pam
|
||
|
||
override_dh_auto_install:
|
||
# Daemon
|
||
install -D -m 0755 target/release/authforged \
|
||
debian/authforge-daemon/usr/sbin/authforged
|
||
# CLI
|
||
install -D -m 0755 target/release/authforgectl \
|
||
debian/authforge-cli/usr/bin/authforgectl
|
||
# GUI
|
||
install -D -m 0755 target/release/authforge \
|
||
debian/authforge-gui/usr/bin/authforge
|
||
install -D -m 0644 gui/data/io.dangerousthings.AuthForge.desktop \
|
||
debian/authforge-gui/usr/share/applications/io.dangerousthings.AuthForge.desktop
|
||
install -D -m 0644 gui/data/io.dangerousthings.AuthForge.svg \
|
||
debian/authforge-gui/usr/share/icons/hicolor/scalable/apps/io.dangerousthings.AuthForge.svg
|
||
# PAM
|
||
$(MAKE) -C pam install DESTDIR=$(CURDIR)/debian/authforge-pam
|
||
|
||
override_dh_auto_test:
|
||
cargo test --workspace --release
|
||
|
||
override_dh_auto_clean:
|
||
cargo clean
|
||
$(MAKE) -C pam clean
|
||
```
|
||
|
||
**Step 5:** Create empty `.install` files for each package (the `dh_install` magic; populated in Phase 13). For now they can be empty or omitted since `debian/rules` does manual installs.
|
||
|
||
**Step 6:** `debian/copyright` — boilerplate Apache-2.0 copyright in machine-readable format. Generate from `cargo about` later, hand-roll for now.
|
||
|
||
**Step 7:** Build the deb:
|
||
|
||
```bash
|
||
sudo apt install -y debhelper devscripts dh-make sbuild lintian \
|
||
libpam0g-dev libgtk-4-dev libadwaita-1-dev libfido2-dev pkg-config
|
||
debuild -us -uc -b
|
||
```
|
||
|
||
Expected: produces 5 `.deb` files in the parent directory. Lintian may complain (we'll silence one warning at a time in later phases).
|
||
|
||
**Step 8:** Verify install + remove on a throwaway VM (Multipass shell or LXD container).
|
||
|
||
```bash
|
||
sudo dpkg -i ../authforge*.deb || sudo apt -f install
|
||
sudo systemctl status authforge.service # will fail until Phase 1 ships unit
|
||
sudo apt purge authforge*
|
||
```
|
||
|
||
**Step 9:** Commit.
|
||
|
||
```bash
|
||
git add debian
|
||
git commit -m "Add debhelper-13 packaging skeleton (5 component debs)"
|
||
```
|
||
|
||
### Task 0.9: GitHub Actions CI
|
||
|
||
**Files:**
|
||
- Create: `.github/workflows/ci.yml`
|
||
|
||
**Step 1:** Workflow file:
|
||
|
||
```yaml
|
||
name: CI
|
||
on:
|
||
push: { branches: [main] }
|
||
pull_request:
|
||
jobs:
|
||
rust:
|
||
runs-on: ubuntu-24.04
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- uses: dtolnay/rust-toolchain@1.78
|
||
with: { components: rustfmt, clippy }
|
||
- run: sudo apt-get update && sudo apt-get install -y libpam0g-dev libgtk-4-dev libadwaita-1-dev libfido2-dev pkg-config
|
||
- run: cargo fmt --all -- --check
|
||
- run: cargo clippy --workspace --all-targets -- -D warnings
|
||
- run: cargo test --workspace
|
||
deb:
|
||
runs-on: ubuntu-24.04
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- run: sudo apt-get update && sudo apt-get install -y debhelper devscripts lintian libpam0g-dev libgtk-4-dev libadwaita-1-dev libfido2-dev pkg-config cargo rustc
|
||
- run: debuild -us -uc -b
|
||
- run: lintian --info --display-info ../authforge*.changes || true # warnings allowed in Phase 0
|
||
```
|
||
|
||
**Step 2:** Push and verify both jobs go green.
|
||
|
||
**Step 3:** Commit.
|
||
|
||
```bash
|
||
git add .github
|
||
git commit -m "Add GitHub Actions CI for cargo + deb"
|
||
```
|
||
|
||
### Task 0.10: Phase 0 acceptance gate
|
||
|
||
Verify all of the following before declaring Phase 0 complete:
|
||
|
||
- [ ] `cargo build --workspace --release` succeeds.
|
||
- [ ] `cargo test --workspace` succeeds (1 test in `common`).
|
||
- [ ] `cargo clippy --workspace -- -D warnings` is clean.
|
||
- [ ] `make -C pam` produces `pam_authforge_pending.so`.
|
||
- [ ] `debuild -us -uc -b` produces 5 `.deb` files.
|
||
- [ ] On an Ubuntu 24.04 LXD container: `sudo dpkg -i ../authforge*.deb` succeeds, `sudo apt purge authforge*` cleanly removes.
|
||
- [ ] CI is green on `main`.
|
||
|
||
If all check, tag `v0.1.0-scaffolding`:
|
||
|
||
```bash
|
||
git tag -a v0.1.0-scaffolding -m "Phase 0: repository scaffolding complete"
|
||
```
|
||
|
||
---
|
||
|
||
# Phase 1: Daemon — D-Bus Interface, systemd, polkit
|
||
|
||
**Goal:** `authforged` registers on the system bus as `io.dangerousthings.AuthForge`, gets started by systemd, and enforces polkit-mediated method-level authorization. Methods are stubs returning sensible test data.
|
||
|
||
**Re-invoke `superpowers:writing-plans` at start of this phase to expand to step granularity.**
|
||
|
||
**Files to create:**
|
||
- `daemon/src/dbus.rs` — zbus `interface` definition
|
||
- `daemon/src/state.rs` — in-memory state shell
|
||
- `debian/authforge-daemon.service` — systemd unit
|
||
- `debian/io.dangerousthings.AuthForge.conf` — D-Bus policy
|
||
- `debian/io.dangerousthings.AuthForge.service` — D-Bus activation file
|
||
- `debian/io.dangerousthings.AuthForge.policy` — polkit actions
|
||
- `debian/authforge-daemon.postinst` — enable + start unit
|
||
|
||
**D-Bus interface skeleton:**
|
||
|
||
```rust
|
||
#[zbus::interface(name = "io.dangerousthings.AuthForge1")]
|
||
impl AuthForge {
|
||
async fn list_credentials(&self, user: String) -> zbus::fdo::Result<Vec<Credential>> { ... }
|
||
async fn enroll_own(&self, user: String, nickname: String) -> zbus::fdo::Result<Credential> { ... }
|
||
async fn remove_own(&self, user: String, cred_id: String) -> zbus::fdo::Result<()> { ... }
|
||
async fn enroll_other(&self, user: String, nickname: String) -> zbus::fdo::Result<Credential> { ... }
|
||
async fn get_policy(&self) -> zbus::fdo::Result<Policy> { ... }
|
||
async fn set_policy(&self, p: Policy) -> zbus::fdo::Result<PolicyApplyResult> { ... }
|
||
async fn set_pending_flag(&self, user: String, flag: PendingFlag) -> zbus::fdo::Result<()> { ... }
|
||
async fn clear_pending_flag(&self, user: String) -> zbus::fdo::Result<()> { ... }
|
||
async fn generate_recovery_code(&self, user: String) -> zbus::fdo::Result<String> { ... }
|
||
}
|
||
```
|
||
|
||
**polkit actions** (XML in `io.dangerousthings.AuthForge.policy`):
|
||
|
||
| Action | Default for active session |
|
||
|---|---|
|
||
| `io.dangerousthings.AuthForge.enroll-own` | `auth_self_keep` |
|
||
| `io.dangerousthings.AuthForge.remove-own` | `auth_self_keep` |
|
||
| `io.dangerousthings.AuthForge.enroll-other` | `auth_admin_keep` |
|
||
| `io.dangerousthings.AuthForge.set-policy` | `auth_admin_keep` |
|
||
| `io.dangerousthings.AuthForge.set-pending` | `auth_admin_keep` |
|
||
| `io.dangerousthings.AuthForge.clear-pending` | `auth_admin_keep` |
|
||
| `io.dangerousthings.AuthForge.generate-recovery` | `auth_admin_keep` |
|
||
|
||
**Tasks:**
|
||
1. Add zbus interface module with stubs returning fixture data.
|
||
2. Wire up `connection.request_name("io.dangerousthings.AuthForge")`.
|
||
3. Write systemd unit (`Type=dbus`, `BusName=io.dangerousthings.AuthForge`, `User=root`).
|
||
4. Write D-Bus system policy (allow root to own; allow `at_console` to call read methods; restrict write methods).
|
||
5. Write polkit policy XML; daemon checks each method against polkit before executing.
|
||
6. Wire postinst: `systemctl daemon-reload && systemctl enable --now authforge.service`.
|
||
7. Test: `busctl call io.dangerousthings.AuthForge /io/dangerousthings/AuthForge io.dangerousthings.AuthForge1 ListCredentials s "$USER"` returns the fixture data.
|
||
8. Test: as non-root, `SetPolicy` triggers polkit prompt.
|
||
|
||
**Acceptance:** D-Bus introspection works (`busctl introspect ...`), all 9 methods are callable as stubs, polkit prompts appear at correct times.
|
||
|
||
---
|
||
|
||
# Phase 2: Daemon — Storage Layer
|
||
|
||
**Goal:** Real I/O to `/etc/authforge/policy.d/`, `/var/lib/authforge/pending/`, and `~/.config/Yubico/u2f_keys` (or `/etc/u2f_mappings`). Replaces fixtures from Phase 1.
|
||
|
||
**Files:**
|
||
- `common/src/policy.rs` — fully implemented TOML parser with last-key-wins merge
|
||
- `daemon/src/storage/policy.rs` — read/write policy.d/ dir, inotify watcher
|
||
- `daemon/src/storage/pending.rs` — read/write pending flags
|
||
- `daemon/src/storage/credentials.rs` — manipulate u2f_keys files (per-user vs central)
|
||
- `daemon/src/storage/userdb.rs` — `/var/lib/authforge/users.db` cache (sqlite via `rusqlite`)
|
||
|
||
**Tasks:**
|
||
1. Implement `Policy::load_from_dir(path)` with merge semantics. TDD: write 3 test fixture dirs, assert merge order.
|
||
2. Implement `Policy::save(stack, mode, methods)` writing to `50-local.conf` while preserving `90-fleet.conf`.
|
||
3. Implement inotify watcher; daemon emits D-Bus signal `PolicyChanged` on change.
|
||
4. Implement pending flag (JSON file) read/write/clear.
|
||
5. Implement credential file manipulation (parse `pamu2fcfg` line format, append/remove by credId).
|
||
6. Implement userdb cache: tracks all users with any enrolled credentials for fast lockout simulation lookup.
|
||
7. Wire all storage modules into the D-Bus methods.
|
||
|
||
**Acceptance:** Round-trip every state via D-Bus; verify file contents on disk match expected format precisely (pam_u2f reads them).
|
||
|
||
---
|
||
|
||
# Phase 3: Daemon — FIDO2 Enrollment Backend
|
||
|
||
**Goal:** Real device enrollment using `ctap-hid-fido2`. Touch / PIN prompts. Emits per-step events over D-Bus so the GUI can show progress.
|
||
|
||
**Files:**
|
||
- `daemon/src/fido/mod.rs`
|
||
- `daemon/src/fido/enroll.rs` — credential creation
|
||
- `daemon/src/fido/devices.rs` — discovery, hot-plug
|
||
- `daemon/src/fido/format.rs` — convert to pam_u2f-compatible `keyHandle:userKey:counter` strings
|
||
|
||
**Tasks:**
|
||
1. Add `ctap-hid-fido2 = "3"` to daemon deps.
|
||
2. Device discovery loop (poll every 500ms; for NFC, depend on PCSC stack via `pcsc` crate).
|
||
3. Enrollment: `make_credential` with rp_id `"pam://localhost"`, user_id from username hash, optional `uv` flag.
|
||
4. Emit D-Bus signals `DeviceFound { name, transport }`, `TouchRequired { device }`, `EnrollmentSucceeded { credential }`, `EnrollmentFailed { reason }`.
|
||
5. Convert resulting credential into pam_u2f line format.
|
||
6. Write integration test using `libfido2`'s software backend (`FIDO_SW_OPENSSL`).
|
||
|
||
**Acceptance:** With a real Yubikey plugged in, `busctl call ... EnrollOwn s "alice" s "Yellow Yubikey"` succeeds, the line appears in `~alice/.config/Yubico/u2f_keys`, and `pam_u2f` validates against it.
|
||
|
||
---
|
||
|
||
# Phase 4: Daemon — Policy Apply via pam-auth-update
|
||
|
||
**Goal:** When `SetPolicy` is called, daemon updates `/etc/pam.d/*` correctly and idempotently using `pam-auth-update`.
|
||
|
||
**Files:**
|
||
- `daemon/src/policy_apply/mod.rs`
|
||
- `daemon/src/policy_apply/profile.rs` — generates the pam-configs file dynamically
|
||
- `debian/authforge-pam.install` — ships the static pam-configs profile
|
||
- `debian/authforge-daemon.postinst` — runs `pam-auth-update --package` on install
|
||
|
||
**Approach:** The pam-configs profile is templated. Daemon writes the appropriate variant to `/usr/share/pam-configs/authforge` based on which stacks are required, then invokes `pam-auth-update --package`. (Alternative: ship multiple profiles, enable/disable each. The first approach is simpler.)
|
||
|
||
**Tasks:**
|
||
1. Write template renderer.
|
||
2. Implement `apply()` that: writes new profile → calls `pam-auth-update --package` → checks exit code.
|
||
3. Add rollback: stash previous profile before write; restore + re-run pam-auth-update on failure.
|
||
4. Wire into `SetPolicy` D-Bus method (which has already passed the lockout simulator, see Phase 5).
|
||
|
||
**Acceptance:** Setting `sudo` to required in policy actually causes `sudo` to demand a security key in a fresh shell.
|
||
|
||
---
|
||
|
||
# Phase 5: Daemon — Lockout Simulator
|
||
|
||
**Goal:** Before any policy write, dry-run the new policy against currently-known users and refuse changes that would lock anyone out.
|
||
|
||
**Files:**
|
||
- `daemon/src/lockout/mod.rs`
|
||
- `daemon/src/lockout/simulate.rs`
|
||
|
||
**Approach:** For each affected stack and each user in `users.db` plus the active session's user:
|
||
- Determine which methods the policy would require.
|
||
- Check user's enrolled credentials.
|
||
- If no enrolled method satisfies the requirement, the user would be locked out.
|
||
|
||
Returns a list of (user, stack, reason) violations.
|
||
|
||
**Tasks:**
|
||
1. Implement simulator as pure function over `(NewPolicy, UserDb, ActiveUser) -> Vec<Violation>`.
|
||
2. Heavy unit-test coverage (every plausible combo).
|
||
3. Wire into `SetPolicy`: if violations and no `force` flag, return error with violation list.
|
||
4. Optional `pam_test_authenticate`-based real check: spawn a sandboxed PAM run as the affected user with mock credentials; this is a v1.1 hardening.
|
||
|
||
**Acceptance:** Cannot accidentally lock yourself out via GUI or CLI without explicit `--force`.
|
||
|
||
---
|
||
|
||
# Phase 6: PAM Module — `pam_authforge_pending.so`
|
||
|
||
**Goal:** The C module that backstops first-login enrollment. Replaces the Phase 0 stub.
|
||
|
||
**Files:**
|
||
- `pam/pam_authforge_pending.c` — full implementation
|
||
- `pam/Makefile` — already exists from Phase 0
|
||
|
||
**Behavior:**
|
||
- `pam_sm_authenticate`:
|
||
1. Get `PAM_USER`.
|
||
2. Check for `/var/lib/authforge/pending/<user>` (use `stat()`, root-owned, mode 0644, no setuid surprises).
|
||
3. If exists:
|
||
a. If recovery code provided and matches `/var/lib/authforge/recovery/<user>` (8-digit, 24h validity), accept and re-flag for re-enrollment.
|
||
b. Otherwise return `PAM_AUTH_ERR` with conv message: "Account setup incomplete. Please complete enrollment in the Authentication app."
|
||
4. If no flag: return `PAM_IGNORE`.
|
||
- `pam_sm_setcred`: `PAM_SUCCESS`.
|
||
|
||
**Tasks:**
|
||
1. Implement file checks with proper path sanitization (reject usernames with `/`, `\0`, `..`).
|
||
2. Implement recovery code constant-time comparison (`memcmp` is *not* enough — use a constant-time helper).
|
||
3. Add structured logging via `pam_syslog`.
|
||
4. Test with `pamtester`:
|
||
```
|
||
sudo touch /var/lib/authforge/pending/alice
|
||
pamtester authforge alice authenticate
|
||
# expect: failure with our message
|
||
sudo rm /var/lib/authforge/pending/alice
|
||
pamtester authforge alice authenticate
|
||
# expect: success (PAM_IGNORE → other modules carry it)
|
||
```
|
||
|
||
**Acceptance:** A user with a pending flag cannot log in; daemon clearing the flag immediately allows login.
|
||
|
||
---
|
||
|
||
# Phase 7: CLI — `authforgectl`
|
||
|
||
**Goal:** Full admin/fleet CLI. Talks D-Bus to the daemon. No magic — every command is a thin wrapper around a D-Bus call.
|
||
|
||
**Files:**
|
||
- `cli/src/main.rs` — replaces Phase 0 skeleton
|
||
- `cli/src/commands/*.rs` — one file per top-level subcommand
|
||
|
||
**Subcommands:**
|
||
|
||
```
|
||
authforgectl status
|
||
authforgectl enroll [--user USER] [--nickname NAME]
|
||
authforgectl list [--user USER]
|
||
authforgectl remove [--user USER] CRED_ID
|
||
authforgectl policy show
|
||
authforgectl policy set <stack> <mode> [--methods METHOD,...]
|
||
authforgectl policy apply [--force-i-know-what-im-doing]
|
||
authforgectl policy validate
|
||
authforgectl pending set USER [--methods METHOD,...]
|
||
authforgectl pending clear USER
|
||
authforgectl pending list
|
||
authforgectl recovery generate USER
|
||
authforgectl recovery list USER
|
||
```
|
||
|
||
**Output:** human-readable by default; `--json` flag for machine-parseable output (essential for Ansible integration).
|
||
|
||
**Tasks:** straightforward; one task per subcommand.
|
||
|
||
**Acceptance:** All flows from the design doc are achievable from CLI alone (headless server use case).
|
||
|
||
---
|
||
|
||
# Phase 8: GUI — Shell + Security Keys Tab
|
||
|
||
**Goal:** Replace Phase 0 placeholder with the real adaptive libadwaita UI for credential management.
|
||
|
||
**Files:**
|
||
- `gui/src/app.rs` — `adw::Application` setup
|
||
- `gui/src/window.rs` — main window with `AdwViewSwitcher`
|
||
- `gui/src/views/keys.rs` — Security Keys tab (list, add, remove)
|
||
- `gui/src/dbus_client.rs` — async wrapper around zbus client
|
||
- `gui/data/resources.gresource.xml` — bundled UI files (XML/Blueprint)
|
||
- Various `.ui` files (use Blueprint via `blueprint-compiler` if available; fallback to XML)
|
||
|
||
**Tasks:**
|
||
1. Move from `ApplicationWindow` to `AdwApplicationWindow` + `AdwViewStack` + `AdwViewSwitcher`.
|
||
2. Implement `KeysView` with `AdwPreferencesPage` containing one `AdwActionRow` per credential.
|
||
3. Implement enrollment dialog: `AdwDialog` with `AdwStatusPage` showing icon based on detected device type (USB / NFC / implant).
|
||
4. Listen for D-Bus signals `DeviceFound`, `TouchRequired`, etc., update dialog UI in response.
|
||
5. Empty state when no credentials.
|
||
6. Smoke test on a real Ubuntu 24.04 GNOME session: enroll a Yubikey, see it appear in the list, remove it, see it disappear.
|
||
|
||
**Acceptance:** A user can do all credential management from this tab without a terminal.
|
||
|
||
---
|
||
|
||
# Phase 9: GUI — Policy Tab
|
||
|
||
**Goal:** The "where is MFA required" toggles, with the lockout warning UX baked in.
|
||
|
||
**Files:**
|
||
- `gui/src/views/policy.rs`
|
||
- `gui/src/views/policy_advanced.rs` — pkexec-gated full list
|
||
|
||
**Tasks:**
|
||
1. Three master toggles (gdm-password / sudo / sshd) as `AdwComboRow` with three options.
|
||
2. "Advanced…" expander triggers polkit prompt; on success, shows full list including any auto-detected stacks.
|
||
3. Pre-apply lockout simulation: button labeled "Apply" calls `policy validate` first; if violations, show inline `AdwBanner` with "fix it" suggestions.
|
||
4. After apply, show toast confirming change.
|
||
5. Visual indication of which stacks are currently in each mode.
|
||
|
||
**Acceptance:** Cannot lock self out via GUI; admin can configure ssh policy via the advanced expander.
|
||
|
||
---
|
||
|
||
# Phase 10: First-Login Flow
|
||
|
||
**Goal:** End-to-end demo of Flow C from the design doc.
|
||
|
||
**Files:**
|
||
- `gui/src/views/firstrun.rs` — fullscreen modal mode
|
||
- `gui/data/authforge-firstrun.desktop` — autostart entry
|
||
- `debian/authforge-gui.install` — install autostart entry to `/etc/xdg/autostart/`
|
||
- `gui/src/main.rs` — handle `--first-run` CLI flag
|
||
|
||
**Tasks:**
|
||
1. Add `--first-run` flag to `authforge` binary; alters startup to fullscreen modal mode (no header bar, no decorations, sticky-on-top).
|
||
2. On startup in first-run mode, query daemon for `pending_flag(current_user)`; if absent, exit immediately.
|
||
3. Show welcome page + run enrollment flow.
|
||
4. On successful enrollment, call `clear_pending_flag` via D-Bus, exit cleanly.
|
||
5. Watchdog: if no successful enrollment within 60s of any user input, call `gnome-session-quit --logout --no-prompt`.
|
||
6. Autostart `.desktop` file with `OnlyShowIn=GNOME` and `X-GNOME-Autostart-Phase=Initialization`.
|
||
|
||
**Test (manual, in VM):**
|
||
1. `useradd -m testuser`, `passwd testuser` (set tempPW), `chage -d 0 testuser`.
|
||
2. `sudo authforgectl pending set testuser --methods fido2`.
|
||
3. Log out, log in as testuser → forced password change → enrollment modal appears → enroll Yubikey → modal closes → desktop usable.
|
||
4. Verify: subsequent SSH-as-testuser-without-key is blocked by `pam_authforge_pending.so` if pending wasn't cleared.
|
||
|
||
**Acceptance:** End-to-end flow C works in a VM smoke test.
|
||
|
||
---
|
||
|
||
# Phase 11: TOTP Support (Feature Flag)
|
||
|
||
**Goal:** TOTP as a third authenticator method, gated behind a build-time Cargo feature.
|
||
|
||
**Files:**
|
||
- `daemon/Cargo.toml` — add `totp` feature
|
||
- `daemon/src/totp/mod.rs` — secret generation, recovery codes
|
||
- `gui/src/views/totp.rs` — QR code display, secret enrollment
|
||
- `debian/authforge-daemon.install` — conditionally install pam-configs entry for TOTP
|
||
- `debian/control` — `Recommends: libpam-google-authenticator` when feature enabled
|
||
|
||
**Tasks:**
|
||
1. Cargo feature `totp` (default-on).
|
||
2. Generate base32 secret (160 bits per RFC 6238).
|
||
3. Render QR code via `qrcode` crate; show in modal with copyable secret string.
|
||
4. Generate 8 recovery codes (8 digits each, stored hashed via Argon2id).
|
||
5. Wire into pam-auth-update profile: when TOTP-required stack is set, profile includes `pam_google_authenticator.so` with `secret=/etc/google-authenticator/${USER}` (file written by daemon during enrollment).
|
||
6. GUI tab visible only when `totp` feature is enabled at build (use `#[cfg(feature = "totp")]`).
|
||
|
||
**Acceptance:** Can enroll TOTP, scan QR into Aegis, log into sudo using TOTP token.
|
||
|
||
---
|
||
|
||
# Phase 12: Recovery Flow
|
||
|
||
**Goal:** Lost-key path. Admin generates code, user uses it once.
|
||
|
||
**Files:**
|
||
- `daemon/src/recovery.rs`
|
||
- `gui/src/views/recovery.rs`
|
||
|
||
**Tasks:**
|
||
1. `generate_recovery_code(user)` writes `/var/lib/authforge/recovery/<user>` (root-owned, 0600) containing Argon2id hash + expiry timestamp.
|
||
2. PAM module checks recovery code at password prompt (already added in Phase 6); on success, removes recovery file and writes a `/var/lib/authforge/pending/<user>` flag with `re_enroll = true`.
|
||
3. CLI: `authforgectl recovery generate alice` outputs the 8-digit code.
|
||
4. GUI: "Recovery" tab shows generate / list / revoke.
|
||
5. Print-PDF feature: `gtk_print_unix_dialog` with template containing user's TOTP secret (opt-in only) and recovery codes.
|
||
|
||
**Acceptance:** A user with no working key can use a recovery code to log in once and be re-enrolled.
|
||
|
||
---
|
||
|
||
# Phase 13: Debian Packaging Finalization
|
||
|
||
**Goal:** All packages install cleanly, postinst/prerm/postrm scripts handle every state transition correctly, debconf preseed works.
|
||
|
||
**Files:**
|
||
- `debian/authforge-daemon.{postinst,prerm,postrm}`
|
||
- `debian/authforge-pam.{postinst,prerm,postrm}`
|
||
- `debian/authforge.config` — debconf script
|
||
- `debian/authforge.templates` — debconf templates
|
||
|
||
**Tasks:**
|
||
1. postinst: enable + start daemon; run `pam-auth-update --package`; if first-time install and debconf provided answers, write initial `/etc/authforge/policy.d/00-debconf.conf`.
|
||
2. prerm: disable PAM enforcement (`pam-auth-update --package --remove`) so removal can't lock anyone out.
|
||
3. postrm purge: rm `/etc/authforge/`, `/var/lib/authforge/`.
|
||
4. debconf templates: ask "Default policy? (None / Optional everywhere / Required for sudo)".
|
||
5. lintian: get all warnings down to zero or explicitly overridden with rationale.
|
||
6. Test matrix: install → upgrade (from 0.0.x → 0.1.0) → remove → purge → reinstall, on Ubuntu 22.04 and 24.04.
|
||
|
||
**Acceptance:** `piuparts authforge_0.1.0-1_amd64.deb` passes.
|
||
|
||
---
|
||
|
||
# Phase 14: Launchpad PPA Setup
|
||
|
||
**Goal:** End users can `sudo add-apt-repository ppa:dangerousthings/authforge`.
|
||
|
||
**Tasks:**
|
||
1. Create Launchpad team `dangerousthings` (or use existing).
|
||
2. Create PPA `authforge`.
|
||
3. Generate signing key (gpg, store passphrase in 1Password / vault).
|
||
4. `dput ppa:dangerousthings/authforge authforge_0.1.0-1_source.changes` (note: Launchpad builds from source).
|
||
5. Wait for build, verify install on a fresh VM.
|
||
6. Document signing key fingerprint in README.
|
||
|
||
**Acceptance:** Two-command install works on a clean Ubuntu 24.04 VM.
|
||
|
||
---
|
||
|
||
# Phase 15: `authforge-gnome-integration`
|
||
|
||
**Goal:** Optional shortcut deb that makes authforge discoverable from `gnome-control-center` Users panel.
|
||
|
||
**Files:**
|
||
- New source tree under `gnome-integration/` (separate Cargo workspace member or pure-data deb)
|
||
- `debian/authforge-gnome-integration.install`
|
||
|
||
**Approach (research required during this phase):** GNOME 46+ allows panel extensions via dbus-activated services. Ship a small JS/GJS extension or a dynamic library loaded by gnome-control-center. If neither stable approach exists, ship a `.desktop` file under `/usr/share/applications/` tagged with `X-GNOME-Settings-Panel=user-accounts` or similar — exact mechanism is GNOME-version-dependent.
|
||
|
||
**Tasks:**
|
||
1. Spike: identify the cleanest hook in GNOME 46 / 47.
|
||
2. Implement.
|
||
3. Test against current Ubuntu LTS GNOME version.
|
||
4. Pin Recommends to specific gnome-control-center major version range.
|
||
|
||
**Acceptance:** Opening Settings → Users → some-user shows a "Configure security…" link that launches `authforge --user some-user` with proper polkit context.
|
||
|
||
---
|
||
|
||
# Phase 16: Ansible Role
|
||
|
||
**Goal:** `dangerousthings.authforge` role on Ansible Galaxy that handles install, policy, and enrollment for fleet deployments.
|
||
|
||
**Files (in a separate `ansible-role` tree, possibly its own repo):**
|
||
- `ansible-role/tasks/main.yml`
|
||
- `ansible-role/defaults/main.yml`
|
||
- `ansible-role/templates/policy.conf.j2`
|
||
- `ansible-role/meta/main.yml`
|
||
|
||
**Tasks:**
|
||
1. Role tasks: add PPA, install packages, drop policy file at `/etc/authforge/policy.d/90-fleet.conf`, restart daemon.
|
||
2. Variables for: enabled stacks, modes per stack, central credential storage on/off, default firstrun methods.
|
||
3. Examples in `examples/playbook.yml`.
|
||
4. Publish to Galaxy.
|
||
|
||
**Acceptance:** A bare Ubuntu 24.04 VM, after running the role, has the package installed and policy applied.
|
||
|
||
---
|
||
|
||
# Phase 17: Integration Test Harness
|
||
|
||
**Goal:** Repeatable end-to-end test that proves all major flows on a clean VM.
|
||
|
||
**Files:**
|
||
- `tests/integration/run.sh`
|
||
- `tests/integration/Vagrantfile` (or LXD/Multipass alternative)
|
||
- `tests/integration/scenarios/*.bats` (Bash Automated Testing System)
|
||
|
||
**Scenarios:**
|
||
1. Fresh install → enroll Yubikey via CLI → set sudo to optional → sudo with key works.
|
||
2. First-login flow: create user, set pending, simulate login, expect enrollment modal.
|
||
3. Policy lockout prevention: set sudo to required without enrolling → expect rejection.
|
||
4. Recovery flow: enroll, "lose" key (delete file), generate recovery, login with code, re-enroll.
|
||
5. Upgrade in place: install 0.1.0, upgrade to 0.1.1, verify state preserved.
|
||
6. Purge: install, configure, purge, expect clean system.
|
||
|
||
Use `umockdev` to simulate USB devices in CI; require real hardware for nightly extended runs.
|
||
|
||
**Acceptance:** All 6 scenarios run green on PR.
|
||
|
||
---
|
||
|
||
# Phase 18: User Docs
|
||
|
||
**Files:**
|
||
- `docs/user/getting-started.md`
|
||
- `docs/user/admin-guide.md`
|
||
- `docs/user/fleet-deployment.md`
|
||
- `docs/user/recovery.md`
|
||
- `docs/user/troubleshooting.md`
|
||
- Optional: small static site (mdBook or Docusaurus) at `https://authforge.dangerousthings.com`
|
||
|
||
**Tasks:**
|
||
1. Getting started: end-user view of "install + enroll my Yubikey + require it for sudo".
|
||
2. Admin guide: managing users, policy, recovery.
|
||
3. Fleet deployment: Ansible role usage, debconf preseed, central credential storage.
|
||
4. Recovery: what to do if you lose your key.
|
||
5. Troubleshooting: common failure modes (lockout via misconfig, NFC not detected, polkit prompt not appearing).
|
||
|
||
**Acceptance:** A new user can go from "I read the README" to "I have MFA on sudo" in under 10 minutes.
|
||
|
||
---
|
||
|
||
# R: v1.0 Release
|
||
|
||
**Tasks:**
|
||
1. Bump version to `1.0.0` in `Cargo.toml`, `debian/changelog`.
|
||
2. Tag `v1.0.0`.
|
||
3. Push to PPA.
|
||
4. Announce: blog post on dangerousthings.com, post to r/Ubuntu, r/yubikey, r/Linux, lobste.rs, Hacker News.
|
||
5. Open feedback channel (GitHub Discussions).
|
||
|
||
---
|
||
|
||
# Cross-cutting concerns (apply to every phase)
|
||
|
||
- **Frequent commits.** One logical change per commit. Use conventional commit prefixes (`feat:`, `fix:`, `test:`, `docs:`, `refactor:`, `chore:`).
|
||
- **TDD where mechanical.** Daemon logic, CLI argument parsing, policy parsing — all must be test-first. UI work is exempt; it gets manual smoke + screenshot review.
|
||
- **No untested code paths** in lockout simulator or PAM module. These are the lockout-risk surface — every branch covered.
|
||
- **YAGNI.** Resist the urge to build "future" extension points. The architecture allows KDE / Debian / Fedora additions cleanly; do not preemptively code abstractions for them in v1.
|
||
- **Use `superpowers:verification-before-completion` before claiming any phase complete.** Run the acceptance checklist; do not advance without evidence.
|
||
- **Re-invoke `superpowers:writing-plans` at the start of each phase ≥ 1** to expand its task list to step granularity using state from prior phases.
|
||
|
||
---
|
||
|
||
# Risks and contingencies
|
||
|
||
| Risk | Likelihood | Impact | Plan |
|
||
|---|---|---|---|
|
||
| `gnome-control-center` Users panel hook (Phase 15) has no stable extension point. | Medium | Low | Ship without the integration deb if needed; standalone app remains fully functional. Discoverability only. |
|
||
| `ctap-hid-fido2` doesn't expose hot-plug events | Low | Medium | Fall back to polling (already planned). Add event support upstream if needed. |
|
||
| Real-world Ubuntu PAM stacks already include third-party MFA modules (sssd, libpam-yubico) | Medium | Medium | postinst detects + warns; admin must explicitly accept import path. Document migration. |
|
||
| Launchpad PPA build fails for non-amd64 arches | Low | Low | Restrict architectures in `debian/control` initially (`Architecture: amd64 arm64`); expand after demand. |
|
||
| First-login modal can be killed via Ctrl-Alt-F2 → ssh → kill | Medium | Low | PAM module backstop (Phase 6) catches this — sshd login also blocked while pending flag exists. Document. |
|
||
|
||
---
|
||
|
||
# Execution Handoff
|
||
|
||
When the user returns:
|
||
|
||
> Plan complete and saved to `docs/plans/2026-04-26-authforge-implementation.md`. The companion design doc is at `docs/plans/2026-04-26-authforge-design.md`. Phase 0 is fully detailed; phases 1–18 are spec'd to a level sufficient to begin work. Each later phase should re-invoke `superpowers:writing-plans` for step-level expansion as it begins.
|
||
>
|
||
> Two execution options:
|
||
>
|
||
> **1. Subagent-driven (this session)** — I dispatch a fresh subagent per task, review between tasks, fast iteration.
|
||
>
|
||
> **2. Parallel session** — Open a new session in a worktree using `superpowers:executing-plans`, batch execution with checkpoints.
|
||
>
|
||
> Which approach?
|