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

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
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

View File

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

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)
- **Task:** Bernardo: "if I turn the laptop screen off and undock, does
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
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/

View File

@@ -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")

View File

@@ -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) ──────

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
# 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