diff --git a/README.md b/README.md index 6e2af4e..5ee176b 100644 --- a/README.md +++ b/README.md @@ -297,6 +297,7 @@ option, e.g. `services.easyeffects.enable = lib.mkForce false;` — see | `nomarchy.hardware.amd.rocm.enable` | `false` | Opt-in: ROCm HIP/OpenCL GPU compute (multi-GB); pair with `.gfxOverride` (e.g. `"11.0.0"`) for an unlisted iGPU | | `nomarchy.hardware.fingerprint.enable` | `false` | fprintd for a detected fingerprint reader (installer-set); enroll with `fprintd-enroll` | | `nomarchy.hardware.fingerprint.pam` | `false` | Opt-in: use the fingerprint for login + sudo (PAM) | +| `nomarchy.hardware.fingerprint.parallel` | `true` | With PAM on: password *or* fingerprint at the same prompt (whichever comes first); `false` = stock sequential pam_fprintd. Password alone always stays sufficient | | `nomarchy.hardware.npu.enable` | `false` | Opt-in/experimental: load the on-die NPU driver (`amdxdna`/`intel_vpu`); userspace runtime is BYO | | `nomarchy.hardware.latestKernel` | `false` | Opt-in: ship `linuxPackages_latest` instead of the default kernel — for very new hardware whose drivers landed recently | | `nomarchy.hardware.camera.hideIrSensor` | `false` | Hide a dual-sensor webcam's IR node from PipeWire's **v4l2** path so apps only ever see the colour camera (the "second, dark Integrated Camera"); installer-set on a paired RGB+IR webcam. `/dev/video*` stays open, so Howdy-style face unlock still works; `.irMatch` overrides the IR-name regex. Does **not** hide IR from libcamera / portal / Flatpak pickers — see [HARDWARE.md §7](docs/HARDWARE.md) | diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index 23e3d47..e93a38f 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -134,18 +134,11 @@ high-ROI, etc.) live in the journal + ROADMAP — not here.* fixture. Control-center / MOTD already mention these; the gap is the silent first *graphical* session for people who never open those. -- **Fingerprint *or* password, in parallel** (2026-07-11, Bernardo) — - `fingerprint.pam` gives pam_fprintd's sequential UX: sudo waits on - the reader, password only after failure/timeout. Bernardo wants - either factor accepted at the same prompt (type the password *or* - touch the sensor, whichever comes first). Stock PAM can't express - this; needs a parallel-conversation module (e.g. `pam-any`, not in - nixpkgs — would be a new package + `fingerprint.pam = "any"` mode). - Cost: medium (package + PAM wiring + careful lockout testing on real - hardware). Security note: keep sudo fallback sane if fprintd hangs. - _(#80–#83 + #85–#88 shipped 2026-07-11. Theme A day-2 + neon-glass finish - shipped — VISION ✓. Dock/hibernate V3 → HARDWARE-QUEUE.)_ + shipped — VISION ✓. Dock/hibernate V3 → HARDWARE-QUEUE. Parallel + fingerprint-or-password shipped 2026-07-12 (Bernardo promoted it live; + `fingerprint.parallel`, pam-fprint-grosshack) — reader V3 → + HARDWARE-QUEUE.)_ ### v1.0 pointer diff --git a/agent/HARDWARE-QUEUE.md b/agent/HARDWARE-QUEUE.md index 9e7cccb..ffd59bf 100644 --- a/agent/HARDWARE-QUEUE.md +++ b/agent/HARDWARE-QUEUE.md @@ -59,6 +59,19 @@ the **T14s** (webcam case). the re-lit panel. If that happens on hardware, the "Laptop screen off" row needs a rework (mirror instead of disable, or drop until a Hyprland bump) — file it as a NOW bug with the coredump. +- [ ] **Parallel fingerprint-or-password on the real reader** (AMD dev + box, 2026-07-12) — with a finger enrolled and + `fingerprint.pam = true` (parallel is the default): `sudo -k true` + must show ONE prompt ("Enter Password or Place finger…"); typing + the password immediately works, touching the sensor instead works, + wrong-finger ×3 falls back to password, and password keeps working + with fprintd stopped (`systemctl stop fprintd`). Also check + hyprlock and the greeter accept both factors and don't wedge on a + leftover prompt after a fingerprint win (known cosmetic quirk of + the hack — pthread_cancel'd prompt). `fingerprint.parallel = false` + must restore the old sequential behavior. VM already asserts the + PAM stack shape + password-only lockout safety with no reader + (checks.hardware-toggles). - [ ] **#55 fingerprint enroll on real reader** — with `nomarchy.hardware.fingerprint.enable` and a physical reader: System › Fingerprint › Enroll a finger; List shows it; Verify succeeds; optional diff --git a/agent/JOURNAL.md b/agent/JOURNAL.md index 2cdffd7..3cf17c2 100644 --- a/agent/JOURNAL.md +++ b/agent/JOURNAL.md @@ -19,6 +19,29 @@ Template: --- +## 2026-07-12 — parallel fingerprint-or-password (interactive, promoted) +- **Task:** Bernardo promoted the PROPOSED "fingerprint or password in + parallel" item live; wants it default-on for fingerprint-PAM users. +- **Did:** Packaged pam-fprint-grosshack v0.3.0 (pkgs/, source-reviewed + first: all failure paths → PAM_AUTHINFO_UNAVAIL, typed password only + ferried via AUTHTOK to pam_unix try_first_pass — can't lock out + password login). New `nomarchy.hardware.fingerprint.parallel` + (default true; false = stock sequential). Wiring swaps stock fprintd's + rule modulePath (mkForce) so ordering is inherited. README/template + rows; BACKLOG PROPOSED entry closed into ROADMAP ✓. +- **Verified:** V2 — checks.hardware-toggles extended to three nodes and + green: parallel node asserts grosshack line before pam_unix in + /etc/pam.d/sudo AND `sudo -S` with the right password succeeds / + wrong password fails with NO reader present (the lockout invariant); + seqpam node = stock pam_fprintd, no grosshack; nopam = neither. + Package builds (meson needed libpam-wrapper + python3). flake check + green. +- **Pending:** V3 on the AMD dev box (HARDWARE-QUEUE): the actual + type-or-touch race, fprintd-stopped fallback, hyprlock/greeter + behavior after a fingerprint win (pthread_cancel'd prompt quirk). +- **Next suggestion:** dock + fingerprint V3 batch on hardware, then + #89 slice 3. + ## 2026-07-12 — undock blackout rescue (interactive follow-up) - **Task:** Bernardo: "if I turn the laptop screen off and undock, does the panel come back?" Answer had to be tested, not recalled. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 85e0f18..74b120e 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -443,6 +443,27 @@ Design/decision records and a running log of shipped work (items marked V3 decides if the menu row needs a mirror-based rework. Also fixed in the rescue: `grep -c .` exits 1 at count 0 — an `||`-guard on that pipeline silently disabled the rescue exactly when it mattered. + ✓ **Parallel fingerprint-or-password (2026-07-12, Bernardo promoted + from PROPOSED):** `nomarchy.hardware.fingerprint.parallel` (default + **true** — the better UX is what opting into fingerprint PAM gets you; + `false` restores stock sequential pam_fprintd). Implementation: + packaged `pam-fprint-grosshack` v0.3.0 (pkgs/, GitLab pin — the + field-standard fprintd fork; stock PAM cannot express parallel + factors, linux-pam#301; the cleaner-concept `pam-any` stays the + fallback candidate if grosshack fights greetd/hyprlock). Source + reviewed before packaging: every failure path returns + PAM_AUTHINFO_UNAVAIL (falls through to password) and a typed password + is only ferried via PAM_AUTHTOK to the stock + `auth sufficient pam_unix.so … try_first_pass` — the hack never + validates passwords itself, so password login cannot be locked out by + it. Wiring reuses stock fprintd's rule slot (mkForce'd modulePath) so + ordering is inherited, not recomputed. checks.hardware-toggles grew to + three nodes: parallel default (stack shape + password-works/ + wrong-password-fails with no reader), parallel=false (stock + pam_fprintd), pam=false (neither). Real-reader race → HARDWARE-QUEUE + (AMD dev box). Accepted quirks (documented in the package): process- + wide SIGUSR1 handler, pthread_cancel'd prompt on fingerprint win, + prompt buffer not zeroized. ✓ **neon-glass finish (#88, `VISION § D`):** rewrote whole-swap `rofi.rasi` to rofi-native `@color` glass panel (cyan rim, soft selection wash — same hex-alpha glass pattern as Boreal); btop title/ diff --git a/flake.nix b/flake.nix index 3532111..6c5b988 100644 --- a/flake.nix +++ b/flake.nix @@ -93,6 +93,7 @@ # switch can refresh a broken system-package nomarchy-pull. # Includes nomarchy-what-changed (generation readability, #82). nomarchy-lifecycle = final.callPackage ./pkgs/nomarchy-lifecycle { }; + pam-fprint-grosshack = final.callPackage ./pkgs/pam-fprint-grosshack { }; }; nixosModules.nomarchy = { @@ -539,7 +540,11 @@ # what they SET (kernel cmdline, fprintd, PAM). Real hardware # behaviour (firmware/driver/device) needs bare metal and is out of # scope here. Imports only hardware.nix, so the VM stays minimal. - # Two nodes: pam=true must wire pam_fprintd; pam=false must NOT + # Three nodes: pam=true wires the PARALLEL module by default + # (pam_fprintd_grosshack in stock fprintd's rule slot, before + # pam_unix) and password-only auth must still work with no + # reader present — the lockout-safety invariant; parallel=false + # wires stock sequential pam_fprintd; pam=false wires neither # (NixOS defaults fprintAuth from fprintd.enable — we force off). hardware-toggles = pkgs.testers.runNixOSTest { name = "nomarchy-hardware-toggles"; @@ -552,6 +557,17 @@ fingerprint = { enable = true; pam = true; }; npu.enable = true; }; + users.users.alice = { + isNormalUser = true; + password = "t0ps3cret"; + extraGroups = [ "wheel" ]; + }; + }; + seqpam = { ... }: { + imports = [ ./modules/nixos/hardware.nix ]; + nomarchy.hardware.fingerprint = { + enable = true; pam = true; parallel = false; + }; }; nopam = { ... }: { imports = [ ./modules/nixos/hardware.nix ]; @@ -564,7 +580,23 @@ assert "amd_pstate=active" in cmdline, cmdline assert "i915.enable_guc=3" in cmdline, cmdline machine.succeed("systemctl cat fprintd.service") - machine.succeed("grep -q pam_fprintd /etc/pam.d/sudo") + machine.succeed("grep -q pam_fprintd_grosshack /etc/pam.d/sudo") + # The parallel rule must be an auth line ordered BEFORE + # pam_unix — its typed password is only ferried to pam_unix. + machine.succeed( + "awk '/^auth/ && /pam_fprintd_grosshack/ {g=NR} " + "/^auth/ && /pam_unix/ {u=NR} " + "END {exit !(g && u && g < u)}' /etc/pam.d/sudo" + ) + # Lockout safety: this VM has no fingerprint reader, so the + # grosshack rule can only fail through — the password alone + # must still authenticate sudo (and a wrong one must not). + machine.succeed("su - alice -c 'echo t0ps3cret | sudo -S -k true'") + machine.fail("su - alice -c 'echo wrongpw | sudo -S -k true'") + + seqpam.wait_for_unit("multi-user.target") + seqpam.succeed("grep -q pam_fprintd /etc/pam.d/sudo") + seqpam.fail("grep -q pam_fprintd_grosshack /etc/pam.d/sudo") nopam.wait_for_unit("multi-user.target") nopam.succeed("systemctl cat fprintd.service") diff --git a/modules/nixos/hardware.nix b/modules/nixos/hardware.nix index 8a73f7b..eaeb996 100644 --- a/modules/nixos/hardware.nix +++ b/modules/nixos/hardware.nix @@ -112,6 +112,20 @@ in menu) when set; otherwise false. ''; }; + + parallel = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + With fingerprint PAM on, accept the password OR a fingerprint at the + same prompt (type or touch, whichever comes first) instead of stock + pam_fprintd's sequential wait-for-the-reader-then-password. Uses the + pam-fprint-grosshack module (an fprintd fork — source-reviewed; every + failure path falls through to the normal password rule, so password + login can never be locked out by it). Set false for the stock + sequential behavior. + ''; + }; }; npu.enable = lib.mkEnableOption '' @@ -205,7 +219,20 @@ in "passwd" "chsh" "chfn" "chpasswd" "polkit-1" "swaylock" "groupadd" "groupdel" "groupmod" "groupmems" - ] (_: { fprintAuth = cfg.fingerprint.pam; }); + ] (_: { + fprintAuth = cfg.fingerprint.pam; + } // lib.optionalAttrs (cfg.fingerprint.pam && cfg.fingerprint.parallel) { + # Parallel mode: same rule slot as stock fprintd (so ordering — + # sufficient, before pam_unix — is inherited), different module. + # grosshack prompts for the password itself while polling the + # reader; whichever lands first wins. A typed password makes the + # rule FAIL with the token stored, and the stock + # `auth sufficient pam_unix.so … try_first_pass` right after it + # does the actual validation — password stays sufficient on its + # own, so a broken reader/fprintd can never lock login out. + rules.auth.fprintd.modulePath = lib.mkForce + "${pkgs.pam-fprint-grosshack}/lib/security/pam_fprintd_grosshack.so"; + }); }) # ── Newest kernel for very-new hardware (opt-in escape hatch) ────── diff --git a/pkgs/pam-fprint-grosshack/default.nix b/pkgs/pam-fprint-grosshack/default.nix new file mode 100644 index 0000000..2dbb646 --- /dev/null +++ b/pkgs/pam-fprint-grosshack/default.nix @@ -0,0 +1,50 @@ +# pam_fprintd_grosshack.so — fprintd's PAM module forked to accept the +# password OR a fingerprint at the same prompt (upstream calls itself a +# "gross hack"; it is also the field-standard answer to this PAM gap — +# stock PAM cannot express parallel factors, linux-pam#301). +# +# Source reviewed before packaging (2026-07-12, v0.3.0 = fprintd 1.94.2 +# base + ~60-line delta): every failure path — no reader, no enrolled +# prints, fprintd absent, timeout, password typed — returns +# PAM_AUTHINFO_UNAVAIL, i.e. the module FAILS and PAM falls through to +# the password rule; a typed password is never validated here, only +# ferried via PAM_AUTHTOK to pam_unix (try_first_pass). Accepted +# quirks: installs a process-wide SIGUSR1 handler in the calling +# process, pthread_cancels the prompt thread on fingerprint win, and +# does not zeroize the prompt buffer. Consumed by +# modules/nixos/hardware.nix (nomarchy.hardware.fingerprint.parallel). +{ lib, stdenv, fetchFromGitLab, meson, ninja, pkg-config, gettext, python3 +, glib, libfprint, polkit, pam, systemd, dbus, libpam-wrapper }: + +stdenv.mkDerivation rec { + pname = "pam-fprint-grosshack"; + version = "0.3.0"; + + src = fetchFromGitLab { + owner = "mishakmak"; + repo = "pam-fprint-grosshack"; + rev = "v${version}"; + hash = "sha256-obczZbf/oH4xGaVvp3y3ZyDdYhZnxlCWvL0irgEYIi0="; + }; + + # Only the PAM module is built (the fork disables every other fprintd + # subdir), but the top-level meson still resolves the full fprintd + # dependency set — hence libfprint/polkit in buildInputs. + # pam_wrapper is only exercised by the (disabled) test suite, but the + # top-level meson marks it required whenever the pam option is on. + nativeBuildInputs = [ meson ninja pkg-config gettext python3 ]; + buildInputs = [ glib libfprint polkit pam systemd dbus libpam-wrapper ]; + + mesonFlags = [ + "-Dpam_modules_dir=${placeholder "out"}/lib/security" + "-Dsystemd=false" + "-Dman=false" + ]; + + meta = { + description = "PAM module accepting password or fingerprint in parallel (fprintd fork)"; + homepage = "https://gitlab.com/mishakmak/pam-fprint-grosshack"; + license = lib.licenses.gpl2Plus; + platforms = lib.platforms.linux; + }; +} diff --git a/templates/downstream/system.nix b/templates/downstream/system.nix index 809b3df..eff511e 100644 --- a/templates/downstream/system.nix +++ b/templates/downstream/system.nix @@ -63,6 +63,7 @@ # amd.rocm.gfxOverride = "11.0.0"; # HSA override for an unlisted iGPU # fingerprint.enable = true; # fprintd; installer-set when a reader is detected # fingerprint.pam = true; # use the fingerprint for login + sudo + # fingerprint.parallel = false; # sequential prompt instead of password-or-finger # npu.enable = true; # on-die NPU driver (experimental; userspace runtime BYO) # latestKernel = true; # newest kernel for very-new hardware (drivers not yet in the default) # camera.hideIrSensor = true; # dual-sensor webcam: hide the IR node so apps get the color cam