# ubuntu-fido 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 `ubuntu-fido` 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 (`ubuntu-fidod`) exposes a D-Bus interface. A GTK4 / libadwaita app and a CLI are thin clients. A small C PAM module (`pam_ubuntu_fido_pending.so`) backstops first-login enrollment. PAM stack edits go through `pam-auth-update`. Policy is drop-in TOML in `/etc/ubuntu-fido/policy.d/`. Full design in `docs/plans/2026-04-26-ubuntu-fido-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-ubuntu-fido-design.md](2026-04-26-ubuntu-fido-design.md) --- ## Phase Roadmap | Phase | Goal | Approx. Effort | Status | |---|---|---|---| | 0 | Repo, workspace, CI, packaging skeleton | 2–3 days | **Detailed below** | | 1 | Daemon: D-Bus interface stub, systemd unit, polkit rules | 3–4 days | Spec'd | | 2 | Daemon: storage layer (policy.d parser, pending flags, user db) | 3–4 days | Spec'd | | 3 | Daemon: FIDO2 enrollment backend via `ctap-hid-fido2` | 5–7 days | Spec'd | | 4 | Daemon: policy apply via pam-auth-update wrapper | 4 days | Spec'd | | 5 | Daemon: lockout simulator | 3 days | Spec'd | | 6 | PAM module: `pam_ubuntu_fido_pending.so` (C) | 3 days | Spec'd | | 7 | CLI: `ubuntu-fidoctl` | 4 days | Spec'd | | 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 | `ubuntu-fido-gnome-integration` (Users panel shortcut) | 3 days | Spec'd | | 16 | Ansible role `dangerousthings.ubuntu_fido` | 2 days | Spec'd | | 17 | Integration test harness (Multipass / LXD VM) | 4 days | Spec'd | | 18 | User docs + onboarding site | 3 days | Spec'd | | **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. --- # 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/ubuntu-fido*/ 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 # ubuntu-fido Turnkey U2F / FIDO2 passkey / TOTP MFA for Ubuntu desktops. ## Install (end users) sudo add-apt-repository ppa:dangerousthings/ubuntu-fido sudo apt install ubuntu-fido ## 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 "] repository = "https://github.com/dangerousthings/ubuntu-fido" 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 = "ubuntu-fido-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 ubuntu-fido-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 = "ubuntu-fidod" version.workspace = true edition.workspace = true license.workspace = true [[bin]] name = "ubuntu-fidod" path = "src/main.rs" [dependencies] ubuntu-fido-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!("ubuntu-fidod {} starting", env!("CARGO_PKG_VERSION")); // D-Bus service registration in Phase 1. Ok(()) } ``` **Step 3:** `cargo build -p ubuntu-fidod`. 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 = "ubuntu-fidoctl" version.workspace = true edition.workspace = true license.workspace = true [[bin]] name = "ubuntu-fidoctl" path = "src/main.rs" [dependencies] ubuntu-fido-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 = "ubuntu-fidoctl", version, about = "Manage ubuntu-fido 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!("ubuntu-fidoctl: 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_ubuntu_fido_pending.c` **Step 1:** `pam/pam_ubuntu_fido_pending.c`: ```c #define PAM_SM_AUTH #include #include #include 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, "ubuntu_fido_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_ubuntu_fido_pending.so: pam_ubuntu_fido_pending.c $(CC) $(CFLAGS) -shared -o $@ $< -lpam install: pam_ubuntu_fido_pending.so install -D -m 0644 pam_ubuntu_fido_pending.so $(DESTDIR)$(LIBDIR)/pam_ubuntu_fido_pending.so clean: rm -f pam_ubuntu_fido_pending.so .PHONY: install clean ``` **Step 3:** Build it: ```bash cd pam && make && cd .. ``` Expected: produces `pam/pam_ubuntu_fido_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.UbuntuFido.desktop` - Create: `gui/data/io.dangerousthings.UbuntuFido.svg` (1KB placeholder icon) **Step 1:** Add `gui` to workspace members in root `Cargo.toml`. **Step 2:** `gui/Cargo.toml`: ```toml [package] name = "ubuntu-fido-gui" version.workspace = true edition.workspace = true license.workspace = true [[bin]] name = "ubuntu-fido" path = "src/main.rs" [dependencies] gtk = { package = "gtk4", version = "0.8" } adw = { package = "libadwaita", version = "0.6" } ubuntu-fido-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.UbuntuFido"; 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.UbuntuFido.desktop`: ```desktop [Desktop Entry] Name=Authentication GenericName=Security Keys & MFA Comment=Manage U2F/FIDO2 keys and authentication policy Exec=ubuntu-fido Icon=io.dangerousthings.UbuntuFido 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.UbuntuFido.svg`. **Step 6:** Build (assumes GTK4 / libadwaita dev packages installed: `libgtk-4-dev`, `libadwaita-1-dev`). ```bash cargo build -p ubuntu-fido-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/ubuntu-fido.install` - Create: `debian/ubuntu-fido-daemon.install` - Create: `debian/ubuntu-fido-daemon.service` - Create: `debian/ubuntu-fido-pam.install` - Create: `debian/ubuntu-fido-cli.install` - Create: `debian/ubuntu-fido-gui.install` **Step 1:** `debian/changelog`: ``` ubuntu-fido (0.1.0-1) UNRELEASED; urgency=low * Initial packaging skeleton. -- Dangerous Things 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: ubuntu-fido Section: admin Priority: optional Maintainer: Dangerous Things 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/ubuntu-fido Vcs-Browser: https://github.com/dangerousthings/ubuntu-fido Vcs-Git: https://github.com/dangerousthings/ubuntu-fido.git Package: ubuntu-fido Architecture: any Depends: ubuntu-fido-daemon (= ${binary:Version}), ubuntu-fido-pam (= ${binary:Version}), ubuntu-fido-cli (= ${binary:Version}), ${misc:Depends} Recommends: ubuntu-fido-gui (= ${binary:Version}) Suggests: ubuntu-fido-gnome-integration Description: Turnkey FIDO2/U2F/TOTP MFA for Ubuntu (metapackage) Installs the full ubuntu-fido stack: daemon, PAM module, CLI, and (if recommended) GUI. Package: ubuntu-fido-daemon Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, libpam-u2f, libfido2-1 Description: System daemon for ubuntu-fido MFA management Provides the privileged D-Bus service that orchestrates enrollment, policy edits, and lockout-prevention checks. Package: ubuntu-fido-pam Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, libpam-runtime Description: PAM module backstopping ubuntu-fido first-login enrollment Refuses authentication when /var/lib/ubuntu-fido/pending/ exists and the user has not completed first-login MFA setup. Package: ubuntu-fido-cli Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, ubuntu-fido-daemon Description: CLI for ubuntu-fido (admin and fleet) Configure policy, enroll on behalf of users, generate recovery codes. Package: ubuntu-fido-gui Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, ubuntu-fido-daemon Description: GTK4/libadwaita UI for ubuntu-fido 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/ubuntu-fidod \ debian/ubuntu-fido-daemon/usr/sbin/ubuntu-fidod # CLI install -D -m 0755 target/release/ubuntu-fidoctl \ debian/ubuntu-fido-cli/usr/bin/ubuntu-fidoctl # GUI install -D -m 0755 target/release/ubuntu-fido \ debian/ubuntu-fido-gui/usr/bin/ubuntu-fido install -D -m 0644 gui/data/io.dangerousthings.UbuntuFido.desktop \ debian/ubuntu-fido-gui/usr/share/applications/io.dangerousthings.UbuntuFido.desktop install -D -m 0644 gui/data/io.dangerousthings.UbuntuFido.svg \ debian/ubuntu-fido-gui/usr/share/icons/hicolor/scalable/apps/io.dangerousthings.UbuntuFido.svg # PAM $(MAKE) -C pam install DESTDIR=$(CURDIR)/debian/ubuntu-fido-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 ../ubuntu-fido*.deb || sudo apt -f install sudo systemctl status ubuntu-fido.service # will fail until Phase 1 ships unit sudo apt purge ubuntu-fido* ``` **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 ../ubuntu-fido*.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_ubuntu_fido_pending.so`. - [ ] `debuild -us -uc -b` produces 5 `.deb` files. - [ ] On an Ubuntu 24.04 LXD container: `sudo dpkg -i ../ubuntu-fido*.deb` succeeds, `sudo apt purge ubuntu-fido*` 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:** `ubuntu-fidod` registers on the system bus as `io.dangerousthings.UbuntuFido`, 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/ubuntu-fido-daemon.service` — systemd unit - `debian/io.dangerousthings.UbuntuFido.conf` — D-Bus policy - `debian/io.dangerousthings.UbuntuFido.service` — D-Bus activation file - `debian/io.dangerousthings.UbuntuFido.policy` — polkit actions - `debian/ubuntu-fido-daemon.postinst` — enable + start unit **D-Bus interface skeleton:** ```rust #[zbus::interface(name = "io.dangerousthings.UbuntuFido1")] impl UbuntuFido { async fn list_credentials(&self, user: String) -> zbus::fdo::Result> { ... } async fn enroll_own(&self, user: String, nickname: String) -> zbus::fdo::Result { ... } 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 { ... } async fn get_policy(&self) -> zbus::fdo::Result { ... } async fn set_policy(&self, p: Policy) -> zbus::fdo::Result { ... } 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 { ... } } ``` **polkit actions** (XML in `io.dangerousthings.UbuntuFido.policy`): | Action | Default for active session | |---|---| | `io.dangerousthings.UbuntuFido.enroll-own` | `auth_self_keep` | | `io.dangerousthings.UbuntuFido.remove-own` | `auth_self_keep` | | `io.dangerousthings.UbuntuFido.enroll-other` | `auth_admin_keep` | | `io.dangerousthings.UbuntuFido.set-policy` | `auth_admin_keep` | | `io.dangerousthings.UbuntuFido.set-pending` | `auth_admin_keep` | | `io.dangerousthings.UbuntuFido.clear-pending` | `auth_admin_keep` | | `io.dangerousthings.UbuntuFido.generate-recovery` | `auth_admin_keep` | **Tasks:** 1. Add zbus interface module with stubs returning fixture data. 2. Wire up `connection.request_name("io.dangerousthings.UbuntuFido")`. 3. Write systemd unit (`Type=dbus`, `BusName=io.dangerousthings.UbuntuFido`, `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 ubuntu-fido.service`. 7. Test: `busctl call io.dangerousthings.UbuntuFido /io/dangerousthings/UbuntuFido io.dangerousthings.UbuntuFido1 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/ubuntu-fido/policy.d/`, `/var/lib/ubuntu-fido/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/ubuntu-fido/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/ubuntu-fido-pam.install` — ships the static pam-configs profile - `debian/ubuntu-fido-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/ubuntu-fido` 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`. 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_ubuntu_fido_pending.so` **Goal:** The C module that backstops first-login enrollment. Replaces the Phase 0 stub. **Files:** - `pam/pam_ubuntu_fido_pending.c` — full implementation - `pam/Makefile` — already exists from Phase 0 **Behavior:** - `pam_sm_authenticate`: 1. Get `PAM_USER`. 2. Check for `/var/lib/ubuntu-fido/pending/` (use `stat()`, root-owned, mode 0644, no setuid surprises). 3. If exists: a. If recovery code provided and matches `/var/lib/ubuntu-fido/recovery/` (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/ubuntu-fido/pending/alice pamtester ubuntu-fido alice authenticate # expect: failure with our message sudo rm /var/lib/ubuntu-fido/pending/alice pamtester ubuntu-fido 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 — `ubuntu-fidoctl` **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:** ``` ubuntu-fidoctl status ubuntu-fidoctl enroll [--user USER] [--nickname NAME] ubuntu-fidoctl list [--user USER] ubuntu-fidoctl remove [--user USER] CRED_ID ubuntu-fidoctl policy show ubuntu-fidoctl policy set [--methods METHOD,...] ubuntu-fidoctl policy apply [--force-i-know-what-im-doing] ubuntu-fidoctl policy validate ubuntu-fidoctl pending set USER [--methods METHOD,...] ubuntu-fidoctl pending clear USER ubuntu-fidoctl pending list ubuntu-fidoctl recovery generate USER ubuntu-fidoctl 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/ubuntu-fido-firstrun.desktop` — autostart entry - `debian/ubuntu-fido-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 `ubuntu-fido` 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 ubuntu-fidoctl 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_ubuntu_fido_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/ubuntu-fido-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/ubuntu-fido/recovery/` (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/ubuntu-fido/pending/` flag with `re_enroll = true`. 3. CLI: `ubuntu-fidoctl 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/ubuntu-fido-daemon.{postinst,prerm,postrm}` - `debian/ubuntu-fido-pam.{postinst,prerm,postrm}` - `debian/ubuntu-fido.config` — debconf script - `debian/ubuntu-fido.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/ubuntu-fido/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/ubuntu-fido/`, `/var/lib/ubuntu-fido/`. 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 ubuntu-fido_0.1.0-1_amd64.deb` passes. --- # Phase 14: Launchpad PPA Setup **Goal:** End users can `sudo add-apt-repository ppa:dangerousthings/ubuntu-fido`. **Tasks:** 1. Create Launchpad team `dangerousthings` (or use existing). 2. Create PPA `ubuntu-fido`. 3. Generate signing key (gpg, store passphrase in 1Password / vault). 4. `dput ppa:dangerousthings/ubuntu-fido ubuntu-fido_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: `ubuntu-fido-gnome-integration` **Goal:** Optional shortcut deb that makes ubuntu-fido discoverable from `gnome-control-center` Users panel. **Files:** - New source tree under `gnome-integration/` (separate Cargo workspace member or pure-data deb) - `debian/ubuntu-fido-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 `ubuntu-fido --user some-user` with proper polkit context. --- # Phase 16: Ansible Role **Goal:** `dangerousthings.ubuntu_fido` 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/ubuntu-fido/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://ubuntu-fido.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-ubuntu-fido-implementation.md`. The companion design doc is at `docs/plans/2026-04-26-ubuntu-fido-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?