* Phase 9/10/11 status rows: dependencies satisfied, mark "open now" with
pointers to step-level plans where they exist.
* Phase 12 row: GUI tab is folded into the Phase 9 lane plan (not its own).
* ASCII dependency diagram: collapse the just-landed Phase-8/Phase-12-backend
layer; show the three open lanes (9+12-GUI, 11 TOTP, 10 first-login)
fanning into Phase 13.
* "Parallel-safe right now" heading bumped to post-Phase-8+12-backend.
Table swaps the now-stale Recovery and post-Phase-8 GUI rows for the
three actually-open lanes, with file footprints + plan links.
* Sequential tail paragraph names the six Code-complete phases (1, 3, 4,
6, 8, 12) whose deferred gates Phase 14 must clear.
* Phase 12 closeout intro names the merge commit and corrects the now-stale
"waiting on Phase 8" wording.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Flips Phase 12 status to ✅ Code complete; bumps progress to 12/19 (63%).
Closeout block summarizes the 8 implementation tasks, lists all new
artifacts, calls out the 80-test count (was 60), and flags the Phase 14
deferred verifications (libargon2-dev compile + pamtester smoke).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* RecoveryCmd::List no longer takes a user — lists all active codes
across users, matching the daemon's ListRecoveryCodes signature.
* RecoveryCmd::Revoke <user> calls RevokeRecoveryCode; exits 1 with
a stderr message when the user has no active code.
* bus.rs gains list_recovery_codes() / revoke_recovery_code(user).
* Two new clap-parser tests cover the new subcommand shapes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
C-side:
* New mode=recovery argv branch in pam_sm_authenticate. Reads the two-line
/var/lib/authforge/recovery/<user> file, argon2_verify against PAM_AUTHTOK,
on match unlinks the file (one-shot) and writes pending(re_enroll=true).
Always returns PAM_IGNORE on failure paths so a missing/wrong code never
blocks normal auth.
* Makefile links -largon2 alongside -lpam.
Daemon-side:
* policy_apply::render_profile renders the recovery line first in the auth
stack with [success=done default=ignore] — successful recovery short-
circuits the rest, missing/wrong code falls through.
* New policy_apply test asserts the recovery line precedes the default
backstop.
Doc:
* pam/TESTING.md adds libargon2-dev to the build prereqs and a new
Smoke test 4 walking through the manual recovery-code flow.
C compile gate (make -C pam) requires libargon2-dev — flagged as a
deferred verification step until a host with the dev package is available.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the random-number stub at dbus.rs:132 with state.issue_recovery,
add ListRecoveryCodes (returns Vec<RecoveryCodeSummary>) and RevokeRecoveryCode.
New polkit actions list-recovery / revoke-recovery (auth_admin_keep). Adds
4 D-Bus integration tests; the previously-stub generate-code test now
exercises the real Argon2id-backed write path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
StorageConfig gains a recovery_dir field; from_env_or_defaults reads
AUTHFORGE_RECOVERY_DIR (default /var/lib/authforge/recovery). AppState
exposes issue/list/revoke methods that Task 6 wires through D-Bus. Test
fixtures in state.rs and dbus.rs updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-line file format (expires_unix\nargon2id-PHC) keeps the C PAM module
parser trivial — no json-c link needed. Atomic temp+rename means a
concurrent reader never sees a half-written file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 8: GUI Keys tab. Lists current user's enrolled FIDO2 credentials,
enrolls a new key with a touch-prompt modal driven by the daemon's
EnrollmentFailed signal (success comes from the call return), removes
credentials per row, degrades gracefully to a Retry banner when the
daemon is unreachable, and surfaces transient errors via adw::Toast.
8 tasks, 4 unit tests (error classifier).
Pure-logic module: generate 8-digit codes, Argon2id PHC hash with OS RNG salt,
verify via constant-time PasswordVerifier. Dead-code allow until Task 4 wires
the store on top.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds enroll_dialog::present which opens an adw::AlertDialog with a spinner
and races the EnrollOwn call against the EnrollmentFailed signal stream:
- Call returns Ok(_cred) -> close dialog, fire on_success refresh callback.
- Call returns Err -> swap dialog body to the user-message form.
- Signal arrives first -> swap to the signal payload (faster than waiting
for the call's typed Err to traverse the bus).
KeysPage::start_enroll wires the activated row to present(), passing the
parent window for modal anchoring and a refresh closure as on_success.
Plan deviation: AlertDialog is libadwaita v1_5-gated, so gui/Cargo.toml
enables that feature. Targets Ubuntu 24.04+ (libadwaita 1.5).
Extract the inline path-traversal check from PendingStore into a shared
storage::safe_user::join_user_segment helper. RecoveryStore (next) reuses it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
KeysPage::new spawns the connect-and-render flow on glib::MainContext::
spawn_local. On success it shows a placeholder PreferencesPage (Task 5
fills in the list); on failure it shows StatusPage with a Retry button
that re-runs the connect attempt.
main.rs installs the multi-thread tokio runtime guard before app.run()
so zbus's tokio futures execute correctly when polled by glib.
Layout deviation from plan: ToolbarView is libadwaita v1_4-gated; the
project sticks with gtk::Box vertical for portability (commit c6a5e94
established this pattern).
Phase 12 backend lane is unblocked by Phase 6+7 and runs independently of
Phase 8. Phase 9 + Phase 12 GUI lane gates on Phase 8 landing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zbus 4.4 does not expose a glib feature; my earlier draft was wrong about
that. Switch the plan to use the workspace tokio config and install a
multi-thread tokio runtime in main.rs, with the runtime guard kept alive for
the lifetime of app.run(). glib::MainContext::spawn_local still drives the
widget-touching closures; tokio's reactor wakes them.
Also: hoist futures-util into the Task 1 dep bundle so Task 6 doesn't need
a separate Cargo.toml edit.
Single-lane, sequential plan to land the authforge GUI Keys tab — empty-state
banner with Retry, list of enrolled credentials with per-row remove, modal
TouchDialog driving EnrollOwn against the Phase 3 EnrollmentSucceeded /
EnrollmentFailed signals. zbus uses the glib runtime feature (not the
workspace tokio config), so all async runs on the GTK main thread via
glib::MainContext::spawn_local — no tokio dep in the GUI process.
Eight tasks, ~1 day of work. TDD applies to error.rs (pure logic, 4 tests);
widget code ships with the manual smoke recipe at gui/TESTING.md as the
acceptance gate.
Cleans up scattered status markers into a single legend (Done / Code complete
/ Spec'd / Bundled), backfills v0.1.0-phase1 tag reference for Phase 1, marks
the table consistently for what's actually shipping vs. what needs the Phase
14 VM smoke. Updates the lane diagram to show what's done vs. in flight, and
adds a 'lessons learned from this session's parallelism' section capturing
when subagents-in-worktrees actually work and when they slip on sandbox-blocked
verification commands.
Adds closeout notes for Phases 3, 4+5, 6, 7, 15, 16 alongside the existing
Phase 1 and Phase 2 sections — what shipped, plan deviations, and any wire-
breaking signature changes (SetPolicy gained a force flag in Phase 4+5).
Lands Lane 1 of the Phase 4+5+8+15+16 parallel cycle. Phase 4 + Phase 5 must
land together because both modify the SetPolicy code path.
- daemon/src/lockout.rs — pure simulate(new_policy, registry) -> Vec<Violation>.
Iterates Required stacks, flags users with no enrolled credential of any
required method. 5 unit tests cover: optional-mode skipped, required-with-
unenrolled flagged, any-method-satisfies, empty registry, multi-stack.
- daemon/src/policy_apply.rs — PolicyApplier renders the pam-configs profile
(Default: yes when any stack requires fido2; pam_u2f.so + pam_authforge_pending
when fido2 required, only pam_authforge_pending otherwise) and runs
pam-auth-update --package. Stash-and-restore on failure: prior profile
contents are restored and pam-auth-update re-run, so a failed apply leaves
the system in its previous PAM state. 4 unit tests including a real-process
rollback test against a failing /bin/sh shim.
- daemon/src/state.rs — AppState::set_policy(p, force) returns
PolicyApplyResult. Always runs the simulator first; if violations and !force,
returns { applied: false, violations } without writing. Otherwise persists
via PolicyStore::save and invokes PolicyApplier::apply. StorageConfig grows
pam_profile_path + pam_auth_update fields (env-var driven, tests inject a
no-op /bin/sh shim into a tempdir).
- daemon/src/dbus.rs — SetPolicy signature is now (Policy, bool) -> Result.
Wire-breaking pre-alpha; CLI updated in this commit.
- cli/src/{bus,commands}.rs — set_policy takes force flag. policy set runs
with force=false and surfaces violations as a non-zero exit + stderr list
pointing the user at policy apply --force-i-know-what-im-doing. policy
apply now actually invokes pam-auth-update via the daemon.
Test count: 42 daemon (was 33; adds 5 lockout + 4 policy_apply). 13 common.
5 cli. cargo clippy --workspace --all-targets -D warnings clean.
Plan deviation: PolicyApplier::from_env() became PolicyApplier::new(profile_path,
pam_auth_update) with the env defaults moved into StorageConfig::from_env_or_defaults.
Cleaner: state owns one source of truth for env-driven path config.
README walks through what the role does, every default, and how to
invoke it from a parent playbook. examples/playbook.yml is a runnable
copy that targets a `workstations` group with a sudo=required+fido2
stack and one pending user.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Declares the new arch-all binary package in debian/control (depending on
authforge-gui + gnome-control-center) and wires its single data file
(gnome-integration/io.dangerousthings.AuthForge.UsersPanel.desktop) into
debian/rules' override_dh_auto_install so it lands in
/usr/share/applications/. The metapackage already Suggests:
authforge-gnome-integration, so no change there.
Adds the apt_repository / apt steps that pull authforge-daemon, -pam,
and -cli (with -gui gated behind authforge_install_gui), the template
step that renders /etc/authforge/policy.d/90-fleet.conf and notifies the
restart handler, and a loop that calls `authforgectl pending set` for
each entry in authforge_pending_users.
The template emits TOML that round-trips through tomllib for both the
empty-stacks default and a populated multi-stack config.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the data file shipped by the optional authforge-gnome-integration
package: a /usr/share/applications/.desktop overlay tagged with
X-GNOME-Settings-Panel=user-accounts so gnome-control-center surfaces a
"Configure security…" launcher inside each user's detail view. The
README documents why a .desktop overlay was picked over a JS extension
(cross-version stability, packaging simplicity) and how to verify the
integration works in GNOME 46/47.
Adds meta/main.yml (Galaxy metadata for Ubuntu 22.04/24.04 under the
Apache-2.0 license), defaults/main.yml covering the policy/storage/
firstrun/pending knobs the tasks layer will consume, and a single
restart-daemon handler. Tasks and template land in the next commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundles plan tasks A2 (PamU2fCred encoder), A3 (Authenticator trait), A4
(MockAuthenticator), A5 (CtapAuthenticator wrapping ctap-hid-fido2 3.5.9),
A6 (wire enrollment through AppState::enroll), A7 (DeviceFound /
TouchRequired / EnrollmentSucceeded / EnrollmentFailed D-Bus signals).
- daemon/src/fido/format.rs — PamU2fCred -> 'kh,pk,es256,+presence' with
hex::encode for the binary blobs and CoseType matching COSE alg -7/-8.
- daemon/src/fido/authenticator.rs — Authenticator trait with discover() and
make_credential(rp_id, user, pin); AuthnError covers NoDevice / Cancelled /
PinRequired / Backend.
- daemon/src/fido/mock.rs — MockAuthenticator::with_one_yubikey produces
deterministic-but-distinct PamU2fCreds (counter-bumped per call).
- daemon/src/fido/ctap.rs — CtapAuthenticator. discover via
ctap_hid_fido2::get_fidokey_devices(); make_credential via
FidoKeyHidFactory::create + fk.make_credential. Heuristic error mapping
to AuthnError variants. Real-hardware path; compile-clean gate only.
- daemon/src/state.rs — AppState::open now takes Arc<dyn Authenticator>.
New enroll(user, nickname) replaces the Phase 2 add_credential stub: calls
authn.make_credential, writes pam_u2f line via CredentialsStore::add,
records enrollment in userdb, returns Credential with hex(keyHandle) as id.
- daemon/src/dbus.rs — enroll_own / enroll_other now emit TouchRequired
before the call and EnrollmentSucceeded / EnrollmentFailed after. Removed
the unused stub-credential builder + import baggage.
- daemon/src/main.rs — picks CtapAuthenticator for prod; tests inject Mock.
Test count: 33 daemon tests pass (was 26). Adds 2 fido::format tests, 3
fido::mock tests, 1 state::enroll_writes_real_pam_u2f_line, 1 dbus::
enrollment_succeeded_signal_fires_on_enroll_own. cargo clippy --workspace
--all-targets -D warnings clean. cargo fmt clean.
Plan deviations:
- HidInfo doesn't have a serial_number field in 3.5.9; switched to using
product_string and HidParam::Path/VidPid for the device path label.
- FidoKeyHidFactory and LibCfg are at the crate root, not under fidokey::.
- fido/mod.rs has #![allow(dead_code)] for now: discover() and DiscoveredDevice
fields are wired via the trait but only called from tests until Phase 8
GUI consumes the DeviceFound signal.
Replaces the Phase 0 stub. Reads PAM_USER, rejects usernames containing /,
\0, .., or that are . / .. / empty. stat()s /var/lib/authforge/pending/<user>:
present -> emits the design-doc user-facing message + PAM_AUTH_ERR; ENOENT
-> PAM_IGNORE (lets the rest of the stack decide); other errno -> logs and
fails closed (PAM_AUTH_ERR).
Builds clean against libpam0g-dev with -Wall -Wextra -Werror -fPIC -O2.
Resulting .so is a 16KB stripped-with-buildid x86-64 ELF; passes file(1) sanity.
pamtester smoke recipe lands in Task B2 alongside the test PAM stack file.
Lands plan tasks C2 (Daemon proxy wrapping all 9 D-Bus methods via
Proxy::new_owned), C3 (status/list/policy-show), C4 (enroll/remove/policy-
set/apply/validate), and C5 (pending/recovery). Bundled because dispatch
needs the bus wrapper to compile cleanly under -D warnings.
Phase 4/5/12 placeholders in the dispatcher: policy apply re-saves to trigger
PolicyChanged; policy validate is a TOML round-trip; pending list and
recovery list are no-op messages until the corresponding D-Bus methods land.
5 clap-parser tests pass (was 5; same — parser shape unchanged in this
commit). Workspace clippy clean.
Captures which remaining phases are parallel-safe and which must stay
sequential. Four lanes (3 / 4+5 / 6 / 7) can land in any order right after
Phase 2 because they touch disjoint file trees: daemon/src/fido,
daemon/src/policy_apply + lockout, pam/*.c, and cli/src.
Documents practical caveats: Phase 4+5 must land together (shared SetPolicy
path), Phase 6 is the only C lane and needs libpam0g-dev, and the Phase 1
subagent-worktree cwd quirk should be retested before relying on it for
literal parallel-compute execution.
Lands plan tasks 2.15 (AppState refactor with StorageConfig + open()) and 2.16
(dbus.rs tests switched to tempdir-backed AppState; storage errors threaded
through D-Bus methods as Failed). Bundled because the AppState surface change
forces dbus.rs adjustments in the same commit.
- daemon/src/state.rs: AppState::open(StorageConfig) replaces with_fixtures().
StorageConfig.from_env_or_defaults() reads AUTHFORGE_POLICY_DIR /
_PENDING_DIR / _USERDB env vars (defaults: /etc/authforge/policy.d,
/var/lib/authforge/pending, /var/lib/authforge/users.db). State delegates
list/add/remove credentials to CredsPathResolver + CredentialsStore picked
per-call from current Policy; pending and userdb operate independently.
- daemon/src/main.rs: opens state via env-driven config; reuses cfg.policy_dir
for the watcher to keep one source of truth.
- daemon/src/dbus.rs: every write method maps StateError to fdo::Error::Failed.
p2p_pair seeds 00-test.conf with [storage] backend = central pointing into
the tempdir so credential writes don't try to touch /home/<user>/...
(alice/bob/carol aren't real accounts in tests).
- Renamed: list_credentials_returns_fixture_for_alice ->
list_credentials_after_enroll. Removed: with_fixtures().
- .gitignore: add .claude/ so leftover Phase 1 worktree state isn't committed.
Test count: 26/26 daemon tests green (was 17). Common: 13/13. Clippy + fmt clean.
Lands plan tasks 2.10 (pam_u2f line format parser via CredEntry::from_line/
to_line, credId extraction), 2.11 (CredentialsStore add/remove/list with
idempotent add-by-credId), and 2.12 (CredsPathResolver dispatching central vs
per-user paths). Bundled because the three pieces compose into one storage
boundary.
CredEntry treats post-username chunks as opaque blobs split on ':', preserving
pam_u2f's full record on round-trip. credId = first comma-separated field of a
blob. add() is idempotent on credId match (Phase 3 may decide to refresh
publicKey on re-enroll; out of scope here).
Path resolution: Central -> Storage.central_path verbatim. PerUser -> getpwnam
via nix, with a /home/<user>/... fallback if NSS errors (CI users, distro
quirks); pam_u2f does the real lookup at auth time, so the fallback only
matters for write-on-enroll where the user does exist.
7 tests added; clippy + fmt clean.
Lands plan tasks 2.6 (PolicyStore wraps load_from_dir / save_local), 2.7
(PolicyChanged D-Bus signal on the AuthForge interface), and 2.8 (notify-based
inotify watcher in main.rs that emits the signal on any change in the policy.d
directory). Bundled because watcher → emit signal → wraps PolicyStore is one
data flow.
- daemon/src/storage/{mod,policy}.rs — PolicyStore::{load,save,watch}; watch
returns a (RecommendedWatcher, watch::Receiver) so the caller keeps the
watcher alive.
- daemon/src/dbus.rs — adds #[zbus(signal)] policy_changed; integration test
via p2p connection asserts the signal arrives within 2s.
- daemon/src/main.rs — spawns a task that ticks PolicyChanged on every
rx.changed(), keyed off AUTHFORGE_POLICY_DIR env var (default
/etc/authforge/policy.d). Watcher leaked via std::mem::forget; daemon
lifetime = process lifetime.
Test count: 17/17 daemon (was 14) + 13/13 common.