feat(fingerprint): password OR fingerprint in parallel at one prompt
Some checks failed
Check / eval (push) Failing after 2m45s

Bernardo promoted the PROPOSED item live: with fingerprint PAM on,
sudo/login should accept whichever factor comes first instead of
pam_fprintd's wait-for-the-reader-then-password. Stock PAM cannot
express parallel factors (linux-pam#301), so this packages
pam-fprint-grosshack v0.3.0 (pkgs/, pinned from GitLab — the
field-standard fprintd fork), source-reviewed before packaging: every
failure path (no reader, no prints, fprintd absent/hung, timeout,
password typed) returns PAM_AUTHINFO_UNAVAIL and falls through; a
typed password is only ferried via PAM_AUTHTOK to the stock
`auth sufficient pam_unix.so … try_first_pass` rule — the module never
validates passwords itself, so it cannot lock out password login.

New option nomarchy.hardware.fingerprint.parallel, default TRUE (the
better UX is what opting into fingerprint PAM buys; false = stock
sequential). Wiring swaps the modulePath of stock fprintd's rule slot
(mkForce) so the sufficient-before-pam_unix ordering is inherited, not
recomputed. README + downstream template rows added.

Verified: V2 — checks.hardware-toggles extended to three nodes, green:
parallel node asserts the grosshack auth line precedes pam_unix in
/etc/pam.d/sudo and that with NO reader a correct password still
passes sudo while a wrong one fails (the lockout-safety invariant);
seqpam node gets stock pam_fprintd and no grosshack; nopam gets
neither. flake check + option-docs + template-sot green.
V3 pending (HARDWARE-QUEUE, AMD dev box): the real type-or-touch race,
fprintd-stopped fallback, hyprlock/greeter after a fingerprint win.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 19:03:56 +01:00
parent c840202018
commit 7aae204014
9 changed files with 175 additions and 14 deletions

View File

@@ -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.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.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.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.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.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) | | `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) |

View File

@@ -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 fixture. Control-center / MOTD already mention these; the gap is the
silent first *graphical* session for people who never open those. 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 _(#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 ### v1.0 pointer

View File

@@ -59,6 +59,19 @@ the **T14s** (webcam case).
the re-lit panel. If that happens on hardware, the "Laptop screen the re-lit panel. If that happens on hardware, the "Laptop screen
off" row needs a rework (mirror instead of disable, or drop until off" row needs a rework (mirror instead of disable, or drop until
a Hyprland bump) — file it as a NOW bug with the coredump. 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 - [ ] **#55 fingerprint enroll on real reader** — with
`nomarchy.hardware.fingerprint.enable` and a physical reader: System `nomarchy.hardware.fingerprint.enable` and a physical reader: System
Fingerprint Enroll a finger; List shows it; Verify succeeds; optional Fingerprint Enroll a finger; List shows it; Verify succeeds; optional

View File

@@ -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) ## 2026-07-12 — undock blackout rescue (interactive follow-up)
- **Task:** Bernardo: "if I turn the laptop screen off and undock, does - **Task:** Bernardo: "if I turn the laptop screen off and undock, does
the panel come back?" Answer had to be tested, not recalled. the panel come back?" Answer had to be tested, not recalled.

View File

@@ -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 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 the rescue: `grep -c .` exits 1 at count 0 — an `||`-guard on that
pipeline silently disabled the rescue exactly when it mattered. 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 ✓ **neon-glass finish (#88, `VISION § D`):** rewrote whole-swap
`rofi.rasi` to rofi-native `@color` glass panel (cyan rim, soft `rofi.rasi` to rofi-native `@color` glass panel (cyan rim, soft
selection wash — same hex-alpha glass pattern as Boreal); btop title/ selection wash — same hex-alpha glass pattern as Boreal); btop title/

View File

@@ -93,6 +93,7 @@
# switch can refresh a broken system-package nomarchy-pull. # switch can refresh a broken system-package nomarchy-pull.
# Includes nomarchy-what-changed (generation readability, #82). # Includes nomarchy-what-changed (generation readability, #82).
nomarchy-lifecycle = final.callPackage ./pkgs/nomarchy-lifecycle { }; nomarchy-lifecycle = final.callPackage ./pkgs/nomarchy-lifecycle { };
pam-fprint-grosshack = final.callPackage ./pkgs/pam-fprint-grosshack { };
}; };
nixosModules.nomarchy = { nixosModules.nomarchy = {
@@ -539,7 +540,11 @@
# what they SET (kernel cmdline, fprintd, PAM). Real hardware # what they SET (kernel cmdline, fprintd, PAM). Real hardware
# behaviour (firmware/driver/device) needs bare metal and is out of # behaviour (firmware/driver/device) needs bare metal and is out of
# scope here. Imports only hardware.nix, so the VM stays minimal. # 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). # (NixOS defaults fprintAuth from fprintd.enable — we force off).
hardware-toggles = pkgs.testers.runNixOSTest { hardware-toggles = pkgs.testers.runNixOSTest {
name = "nomarchy-hardware-toggles"; name = "nomarchy-hardware-toggles";
@@ -552,6 +557,17 @@
fingerprint = { enable = true; pam = true; }; fingerprint = { enable = true; pam = true; };
npu.enable = 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 = { ... }: { nopam = { ... }: {
imports = [ ./modules/nixos/hardware.nix ]; imports = [ ./modules/nixos/hardware.nix ];
@@ -564,7 +580,23 @@
assert "amd_pstate=active" in cmdline, cmdline assert "amd_pstate=active" in cmdline, cmdline
assert "i915.enable_guc=3" in cmdline, cmdline assert "i915.enable_guc=3" in cmdline, cmdline
machine.succeed("systemctl cat fprintd.service") 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.wait_for_unit("multi-user.target")
nopam.succeed("systemctl cat fprintd.service") nopam.succeed("systemctl cat fprintd.service")

View File

@@ -112,6 +112,20 @@ in
menu) when set; otherwise false. 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 '' npu.enable = lib.mkEnableOption ''
@@ -205,7 +219,20 @@ in
"passwd" "chsh" "chfn" "chpasswd" "passwd" "chsh" "chfn" "chpasswd"
"polkit-1" "swaylock" "polkit-1" "swaylock"
"groupadd" "groupdel" "groupmod" "groupmems" "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) ────── # ── Newest kernel for very-new hardware (opt-in escape hatch) ──────

View File

@@ -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;
};
}

View File

@@ -63,6 +63,7 @@
# amd.rocm.gfxOverride = "11.0.0"; # HSA override for an unlisted iGPU # amd.rocm.gfxOverride = "11.0.0"; # HSA override for an unlisted iGPU
# fingerprint.enable = true; # fprintd; installer-set when a reader is detected # fingerprint.enable = true; # fprintd; installer-set when a reader is detected
# fingerprint.pam = true; # use the fingerprint for login + sudo # 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) # 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) # 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 # camera.hideIrSensor = true; # dual-sensor webcam: hide the IR node so apps get the color cam