1259 lines
45 KiB
Markdown
1259 lines
45 KiB
Markdown
# 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 <ops@dangerousthings.com>"]
|
||
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 <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, "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 <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: ubuntu-fido
|
||
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/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/<user> 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<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.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<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_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/<user>` (use `stat()`, root-owned, mode 0644, no setuid surprises).
|
||
3. If exists:
|
||
a. If recovery code provided and matches `/var/lib/ubuntu-fido/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/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 <stack> <mode> [--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/<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/ubuntu-fido/pending/<user>` 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?
|