diff --git a/README.md b/README.md index e327899..1cd7439 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,30 @@ $ sudo systemctl enable open-fprintd-resume open-fprintd-suspend For even more error procedures, check [this Arch comment thread](https://aur.archlinux.org/packages/python-validity/#comment-755904) or [this python-validity bug comment thread](https://github.com/uunicorn/python-validity/issues/3). +#### `factory_reset` / `init_flash` fails with `0404` + +On 0xd51 and 0x969 silicon (HP EliteBook 840 G5, HP G6 family, +HP ZBook Studio x360 G5, and likely other 138a:00ab / 06cb:00b7 variants) +the `reset_blob` we ship — extracted from Windows drivers for older +0x199-class Prometheus chips — is rejected by the chip with status `0404`. +This affects two scenarios: + +- **Factory-fresh chip** (e.g. after a UEFI BIOS reset). `init_flash` + cannot format the flash and the daemon crash-loops. +- **Windows-Hello-paired chip.** After hitting the "Signature verification + failed" error, users typically try `playground/factory-reset.py`; on + these chips it fails at the very first command with `0404`. + +There is currently **no known Linux-side workaround** — we do not have a +reset_blob known to work on 0xd51 / 0x969. If you hit this, please add +your hardware details (`dmidecode -t 1`, `lsusb -v`, and the failing +journal output) to +[uunicorn/python-validity#256](https://github.com/uunicorn/python-validity/pull/256) +so affected models can be tracked. Windows-paired users can, as a +workaround, boot Windows and reinstall the Synaptics driver (Device +Manager → uninstall with "delete driver software" → reboot → let Windows +reinstall) to re-pair the chip on the Windows side. + ## Enabling fingerprint for system authentication if it doesn't come automatically, you might need to make changes to files in `/etc/pam.d` to enable fingerprint login (depending on your distro). @@ -137,6 +161,49 @@ user_to_sid: ``` Note the indentation; each entry has to be preceded by at least one space. +### Template competition (0xd51 / 0x969 chips) + +The chip's on-chip matcher scores captured images against **every** enrolled +template — including any Windows Hello templates written by a previous +Windows session — and returns the highest-scoring match. On some HP models +(reported for the ZBook G6 family, but likely broader) Windows Hello writes +very high-quality templates that consistently outscore Linux `fprintd` +templates for the same finger, so `fprintd-verify` silently loses even when +enrollment succeeded. + +Two workarounds, in order of preference: + +1. **Enroll different fingers per OS.** Right-index in Linux, right-middle + in Windows (or whichever split you prefer). No competition, both OSes + keep fingerprint auth. +2. **Erase the on-chip database from Linux.** Wipes all templates on both + OSes; Windows Hello fingerprint login stops working until you re-enroll + in Windows. PIN / TPM state is unaffected. See + `playground/erase-flash.py` (partition `4`). + +Investigated and documented by @Karloss1234 on PR +[uunicorn/python-validity#256](https://github.com/uunicorn/python-validity/pull/256). + +### KDE / Kubuntu lock-screen PAM + +On Kubuntu the greeter/lock-screen PAM stacks aren't touched by +`pam-auth-update`. To wire the fingerprint reader into the KDE lock screen +you need three files under `/etc/pam.d` mirroring the same `sufficient` +line: + +``` +# /etc/pam.d/kde, /etc/pam.d/kde-fingerprint, /etc/pam.d/kde-smartcard +#%PAM-1.0 +auth sufficient pam_fprintd.so max_tries=3 timeout=10 +auth required pam_unix.so +``` + +Also check `/etc/pam.d/sddm-greeter` for a `pam_permit.so` fallback and +replace it with `pam_unix.so` — otherwise the lock screen can unlock +without authentication after fingerprint timeout. + +Contributed by @Karloss1234; not required on GNOME / Ubuntu proper. + ## Playground This package contains a set of scripts you can use to do a low-level debugging of the sensor protocol. diff --git a/bin/validity-sensors-firmware b/bin/validity-sensors-firmware index c9d357b..124fd75 100755 --- a/bin/validity-sensors-firmware +++ b/bin/validity-sensors-firmware @@ -59,10 +59,23 @@ def download_and_extract_fw(dev_type, fwdir, fwuri=None): raise Exception('Hash mismatch for driver download! Expected {}, got {}'.format( expected_hash, actual_hash)) - subprocess.check_call([ - 'innoextract', '--output-dir', fwdir, '--include', fwname, '--collisions', 'overwrite', - fwarchive - ]) + # Lenovo softpaqs are Inno Setup installers; HP softpaqs are CAB-wrapped + # self-extracting exes. Try innoextract first, fall back to cabextract. + try: + subprocess.check_call([ + 'innoextract', '--output-dir', fwdir, '--include', fwname, + '--collisions', 'overwrite', fwarchive + ], stderr=subprocess.DEVNULL) + except (subprocess.CalledProcessError, FileNotFoundError): + try: + # No -F filter: HP softpaqs nest the target under e.g. src/driver/INF/x64/, + # and cabextract -F matches the full path. Extract everything; the find + # call below locates the target file regardless of subdirectory. + subprocess.check_call(['cabextract', '-q', '-d', fwdir, fwarchive]) + except (subprocess.CalledProcessError, FileNotFoundError) as e: + raise Exception( + 'Failed to extract {} from {}: neither innoextract nor cabextract ' + 'could handle the archive ({}).'.format(fwname, fwarchive, e)) fwpath = subprocess.check_output(['find', fwdir, '-name', fwname]).decode('utf-8').strip() print('Found firmware at {}'.format(fwpath)) @@ -91,10 +104,16 @@ if __name__ == "__main__": if not dev_type: raise Exception('No supported validity device found') - try: - subprocess.check_call(['innoextract', '--version'], stdout=subprocess.DEVNULL) - except Exception as e: - print('Impossible to run innoextract: {}'.format(e)) + have_extractor = False + for tool in ('innoextract', 'cabextract'): + try: + subprocess.check_call([tool, '--version'], stdout=subprocess.DEVNULL) + have_extractor = True + break + except (subprocess.CalledProcessError, FileNotFoundError): + continue + if not have_extractor: + print('Need at least one of innoextract or cabextract installed.') sys.exit(1) with tempfile.TemporaryDirectory() as fwdir: diff --git a/dbus_service/dbus-service b/dbus_service/dbus-service index 1c63e8d..41aed4d 100755 --- a/dbus_service/dbus-service +++ b/dbus_service/dbus-service @@ -114,8 +114,52 @@ class Device(dbus.service.Object): self.VerifyFingerSelected('any') + # pam_fprintd re-prints "Place your finger on the reader" for every + # verify-retry-scan signal, which on this chip's chatty capture loop + # can fire 30+ times per verify cycle and floods the terminal during + # sudo. Emit it at most once per VerifyStart so the user gets one + # initial prompt + one early retry hint, then silence until match + # or timeout. Enroll keeps its per-stage signals — those are useful + # because each stage is a discrete user action. + retry_emitted = [False] + retry_count = [0] + + # Watchdog: 0xd51 / 0x969 chips can enter a "wedged capture-quality + # gate" state after long uptime + heavy dev cycles, where every + # captured frame is rejected and no capture-complete interrupt is + # ever emitted. sensor.identify() then loops in wait_int() until + # something external cancels it (pam_fprintd's 10s timeout). This + # threshold lets us abort daemon-side after ~25 rejected frames + # (typically 12-15s), emit a specific journal message pointing at + # the real recovery path (cold power cycle, not a systemctl + # restart), and unblock the identify() thread cleanly. See the + # discussion on PR uunicorn/python-validity#256 for the wedge + # signature and recovery notes. + CHIP_WEDGE_RETRY_THRESHOLD = 25 + def update_cb(e): - self.VerifyStatus('verify-retry-scan', False) + # Always log every chip retry to journal — useful for the + # task #17 capture-quality benchmark. The D-Bus emit below + # is still throttled to once per cycle (avoids spamming + # pam_fprintd which re-prints the prompt on each signal). + retry_count[0] += 1 + logging.info('Chip capture retry-scan #%d (user=%s)', + retry_count[0], user) + if retry_count[0] == CHIP_WEDGE_RETRY_THRESHOLD: + logging.warning( + 'Chip appears wedged after %d consecutive retries with ' + 'no capture-complete interrupt. Aborting verify. This ' + 'usually means the on-chip capture-quality gate is ' + 'rejecting every frame, which the daemon cannot reset ' + 'from userspace. Recovery: cold power cycle (shutdown, ' + 'unplug charger, hold power ~15s, boot). A `systemctl ' + 'restart python3-validity` is unlikely to help; the ' + 'wedge is below the daemon.', + retry_count[0]) + sensor.cancel() + if not retry_emitted[0]: + self.VerifyStatus('verify-retry-scan', False) + retry_emitted[0] = True def run(): try: diff --git a/debian/changelog b/debian/changelog index 8d7c453..70af3ae 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,163 @@ +python-validity (0.16~hp8) noble; urgency=medium + + * sensor.py: add support for sensor type 0x969 alongside 0xd51. HP + ZBook Studio x360 G5 uses the same 138a:00ab PID but a different + silicon variant reporting 0x969; HP G6 family on 06cb:00b7 also + covers this variant. Mirrors the existing 0xd51 handling across + line_update_type1_devices, the alias-to-0x199 block in + Sensor.open(), and the b[0]==3 interrupt fix in Sensor.capture(). + Contributed by @ggiesen (SimpleX-T/python-validity#2), verified + on HP ZBook Studio x360 G5. Also independently developed by + @Karloss1234 on HP ZBook 17 G6. + * sensor.py: recover 0x969 chips reporting 0x199 post-resume. On + suspend/resume, 0x969 chips re-enumerate reporting sensor type + 0x199 directly, causing Sensor.open() to skip the alias + + interrupt fix. Detect via the device name (FM-3439-xxx or + FM- 154-xxx — the two known 0x969 model families) and restore + real_device_type = 0x969. Diagnosed by @Karloss1234 on Kubuntu + 26.04. + * init_flash.py, sensor.py: catch status 0404 from usb.cmd(reset_ + blob) at init_flash() and factory_reset(), raise an actionable + error naming the chip-family limitation (0xd51 / 0x969 silicon + does not accept the reset_blob shipped by this driver, which + was extracted from Windows drivers for older 0x199-class chips). + No fix — until a working reset_blob for the newer chips is + obtained, init on factory-fresh chips and factory_reset for + Windows-Hello-paired chips remain unsupported on this family. + But the daemon no longer surfaces a bare "Failed: 0404" that + users can't act on. Reported by @bcoutts (fresh chip, HP ProBook + 445R G6) and @ntoyiakhona06-creator (Windows-paired 840 G5). + * db.py: fix del_record missing db_write_enable. On 0x969 chips + the delete cmd (0x48) returns 04b6 silently without the + write-enable prefix, so records marked deleted never actually + freed flash space. Mirror new_record's shape: db_write_enable + + try/finally: call_cleanups(). Diagnosed by @Karloss1234 while + tracing 04b5 flash-full errors on enroll. + * db.py: proactive flash-full check in new_record. The chip does + not automatically compact the database partition after deletes, + so long-running Linux-only installs accumulate uncompacted + dead records until enroll fails at new_finger with an opaque + 04b5. Use the db_info() call that was already there (with a + TODO to check the numbers) to actually check them; raise a + clear error naming the recovery path (erase_flash(4) + reboot) + before the on-wire failure. + * dbus-service: watchdog on VerifyStart. 0xd51 / 0x969 chips can + enter a "wedged capture-quality gate" state after long uptime + + heavy dev cycles where every captured frame is rejected and no + capture-complete interrupt is ever emitted — sensor.identify() + then loops in wait_int() indefinitely. Add a retry-count + threshold (25 consecutive retries, ~12-15s) that calls + sensor.cancel() and logs a specific "chip appears wedged; cold + power cycle required" warning to the journal. Recovery is a + real cold power cycle (shutdown, unplug charger, hold power + ~15s, boot) — the wedge sits below the daemon and no systemctl + restart can clear it. + * README: document Windows Hello template competition workarounds + (enroll different fingers per OS, or erase partition 4), the + Kubuntu / KDE lock-screen PAM stack recipe, and the known 0404 + reset_blob issue. + + -- SimpleX-T Tue, 07 Jul 2026 23:27:29 +0100 + +python-validity (0.16~hp7) noble; urgency=medium + + * usb.py: defensive USB reset at the start of open_dev(). The 0xd51- + family chips (138a:00ab, 06cb:00b7) can be left in a "stuck" + protocol state across cold boot, unclean exit of the daemon, or + sudden suspend/resume — in that state the chip accepts the + bulk-OUT but never replies on bulk-IN, so the first cleartext + command (get_flash_info, cmd 3e) times out and the daemon enters + a 15-second restart loop. The reset is the in-driver equivalent + of the manual `udevadm trigger --attr-match=idVendor=... + --attr-match=idProduct=...` workaround multiple users were + running to recover after every reboot. Reported by Killersparrow1 + (uunicorn/python-validity#238, Fedora 44, sensor vanishes on + reboot) and a separate Arch / ZBook G5 user (USBTimeoutError on + cmd 3e). Also observed intermittently on the maintainer's + machine. + * After reset, re-find the device by vid/pid because the USB + address can shift across the reset. + + -- SimpleX-T Sun, 25 May 2026 00:30:00 +0100 + +python-validity (0.16~hp6) noble; urgency=medium + + * Diagnostic logging (no behavior change): + - sensor.py: log resolved capture geometry once at chip open + (real_type, spoofed_type, lines_per_frame, bytes_per_line, + line_width, calibration data lines). Makes future "is the + spoofed-to-0x199 profile right for this chip?" questions + answerable from journal alone. + - dbus-service: log every internal chip retry-scan during + VerifyStart, independent of the D-Bus signal throttle. + Lets users / support tickets quantify capture quality. + * Both changes are INFO-level log lines only. ~1 log per chip + open + ~0-3 lines per verify in normal use. + + -- SimpleX-T Thu, 21 May 2026 23:55:00 +0100 + +python-validity (0.16~hp5) noble; urgency=medium + + * debian/source/options: tar-ignore .pybuild, build, *.egg-info, + __pycache__, *.pyc. Without this, dpkg-source for 3.0 (native) + packages bundles leftover local build artifacts (cached pybuild + state with absolute paths from the developer's machine) into the + source tarball, which caused per-series Launchpad builds after + the first one to fail with "Permission denied" trying to write + into a baked-in /home//... path. + + -- SimpleX-T Thu, 21 May 2026 13:30:00 +0100 + +python-validity (0.16~hp4) noble; urgency=medium + + * VerifyStart: emit verify-retry-scan at most once per verify cycle so + pam_fprintd doesn't repeat "Place your finger on the reader" for every + one of the chip's chatty capture-quality retries. On the 0xd51 chip the + capture loop fires the signal 20+ times during a 10s timeout window, + which flooded the terminal during sudo. Enrollment is unchanged — + per-stage retry-scans there are useful because each stage is a discrete + user action. + + -- SimpleX-T Thu, 21 May 2026 23:30:00 +0100 + +python-validity (0.16~hp3) noble; urgency=medium + + * sensor.enroll: replace existing finger record when re-enrolling the same + subtype for the same user. Without this, the chip's db rejects creating + a duplicate, surfacing as enroll-failed at the final stage after all + captures pass. The delete is performed inside do_create_finger (right + before db.new_finger) so the chip's enroll session isn't disrupted — + pre-deleting before EnrollStart caused subsequent captures to return + retry-scan indefinitely until the daemon was restarted. + + -- SimpleX-T Thu, 21 May 2026 23:00:00 +0100 + +python-validity (0.16~hp2) noble; urgency=medium + + * validity-sensors-firmware: fall back to cabextract when innoextract + cannot handle the archive. HP softpaqs (e.g. sp135736.exe used for + 138a:00ab and 06cb:00b7) are CAB-wrapped self-extracting exes, not + Inno Setup installers, so innoextract rejected them and the postinst + surfaced a noisy Python traceback. With the fallback, HP softpaq + extraction now works, and either extractor satisfies the runtime + requirement. + * Recommends: cabextract (so the fallback path is available by default). + + -- SimpleX-T Thu, 21 May 2026 11:00:00 +0100 + +python-validity (0.16~hp1) noble; urgency=medium + + * Add support for sensor type 0xd51 (138a:00ab, 06cb:00b7) — HP EliteBook + 840 G5 and related models. Fixes a chip-specific interrupt protocol + that caused fprintd-verify to hang forever on these devices. + * Add FIRMWARE_URIS entries for DEV_AB and DEV_B7 so that + validity-sensors-firmware no longer crashes with KeyError on these PIDs. + * Enable libpam-fprintd via pam-auth-update in postinst so that sudo / + screen-unlock prompt for fingerprint immediately after install. + * Recommend libpam-fprintd. + + -- SimpleX-T Wed, 20 May 2026 22:25:34 +0100 + python-validity (0.15~ppa2) noble; urgency=medium * Change all write paths to /var/run/python-validity diff --git a/debian/control b/debian/control index 2b09da3..99c2ac9 100644 --- a/debian/control +++ b/debian/control @@ -19,6 +19,7 @@ Depends: ${python3:Depends}, dbus, open-fprintd (>= 0.6~), innoextract (>= 1.6~) +Recommends: libpam-fprintd, cabextract Description: Validity Fingerprint Sensor DBus Driver This package adds support to some Validity sensors. . diff --git a/debian/python3-validity.postinst b/debian/python3-validity.postinst index ca62874..7dfc806 100644 --- a/debian/python3-validity.postinst +++ b/debian/python3-validity.postinst @@ -8,5 +8,8 @@ if [ "$1" = "configure" ]; then systemctl daemon-reload || true udevadm control --reload-rules || true udevadm trigger || true + if [ -x /usr/sbin/pam-auth-update ]; then + pam-auth-update --package --enable fprintd || true + fi fi diff --git a/debian/python3-validity.udev b/debian/python3-validity.udev index d9ca628..bb4a74a 100644 --- a/debian/python3-validity.udev +++ b/debian/python3-validity.udev @@ -4,6 +4,8 @@ ENV{DEVTYPE}!="usb_device", GOTO="python_validity_end" ATTRS{idVendor}=="138a", ATTRS{idProduct}=="0090", GOTO="python_validity_match" ATTRS{idVendor}=="138a", ATTRS{idProduct}=="0097", GOTO="python_validity_match" ATTRS{idVendor}=="06cb", ATTRS{idProduct}=="009a", GOTO="python_validity_match" +ATTRS{idVendor}=="138a", ATTRS{idProduct}=="00ab", GOTO="python_validity_match" +ATTRS{idVendor}=="06cb", ATTRS{idProduct}=="00b7", GOTO="python_validity_match" GOTO="python_validity_end" diff --git a/debian/source/options b/debian/source/options new file mode 100644 index 0000000..8dc4dda --- /dev/null +++ b/debian/source/options @@ -0,0 +1,5 @@ +tar-ignore = ".pybuild" +tar-ignore = "build" +tar-ignore = "*.egg-info" +tar-ignore = "__pycache__" +tar-ignore = "*.pyc" diff --git a/validitysensor/blobs.py b/validitysensor/blobs.py index a9f16ab..fb2a1a0 100644 --- a/validitysensor/blobs.py +++ b/validitysensor/blobs.py @@ -8,9 +8,13 @@ def __load_blob(blob: str) -> bytes: from . import blobs_97 as blobs elif usb.usb_dev().idProduct == 0x009d: from . import blobs_9d as blobs + elif usb.usb_dev().idProduct == 0x00ab: + from . import blobs_97 as blobs # HP EliteBook 840 G5; verified elif usb.usb_dev().idVendor == 0x06cb: if usb.usb_dev().idProduct == 0x009a: from . import blobs_9a as blobs + elif usb.usb_dev().idProduct == 0x00b7: + from . import blobs_9a as blobs # HP G6 series; same sensor type as 0x00ab globals()[blob] = getattr(blobs, blob) return globals()[blob] diff --git a/validitysensor/db.py b/validitysensor/db.py index accda03..fa20c80 100644 --- a/validitysensor/db.py +++ b/validitysensor/db.py @@ -196,7 +196,16 @@ def get_record_children(self, dbid: int): return rec def del_record(self, dbid: int): - assert_status(tls.cmd(pack(' typing.Tuple[int, int, int, int]: raise Exception('wait_start: Unexpected interrupt type %s' % hexlify(b).decode()) # wait for finger + # Sensor type 0xd51 (138a:00ab, 06cb:00b7) does not emit the + # b[0]=2 "finger detected" interrupt — it jumps directly from the + # start ack to b[0]=3 capture events. Accept b[0]=3 as a substitute + # and pass the interrupt through to the wait-capture-complete loop. + saved_b = None while True: b = usb.wait_int() if b[0] == 2: break + if b[0] == 3 and getattr(self, 'real_device_type', None) in (0xd51, 0x969): + saved_b = b + break # wait capture complete while True: - b = usb.wait_int() + b = saved_b if saved_b is not None else usb.wait_int() + saved_b = None if b[0] != 3: raise Exception('Unexpected interrupt type %s' % hexlify(b).decode()) @@ -811,11 +886,20 @@ def enroll(self, identity: SidIdentity, subtype: int, def do_create_finger(final_template: bytes, tid: bytes): tinfo = self.make_finger_data(subtype, final_template, tid) - usr = db.lookup_user(identity) - if usr is None: + existing = db.lookup_user(identity) + if existing is None: usr = db.new_user(identity) else: - usr = usr.dbid + # Replace any existing enrollment for this finger slot. The chip + # rejects creating a second record with the same subtype for the + # same user. Deleting here (after all captures are done) keeps + # the chip's enroll session uninterrupted — earlier attempts to + # pre-delete before EnrollStart left the chip in a state where + # subsequent captures kept returning retry-scan indefinitely. + for f in existing.fingers: + if f['subtype'] == subtype: + db.del_record(f['dbid']) + usr = existing.dbid recid = db.new_finger(usr, tinfo) usb.wait_int() diff --git a/validitysensor/usb.py b/validitysensor/usb.py index 464b092..0cd293e 100644 --- a/validitysensor/usb.py +++ b/validitysensor/usb.py @@ -1,5 +1,6 @@ import errno import logging +import time import typing from binascii import hexlify, unhexlify from enum import Enum @@ -18,6 +19,8 @@ class SupportedDevices(Enum): DEV_97 = (0x138a, 0x0097) DEV_9d = (0x138a, 0x009d) DEV_9a = (0x06cb, 0x009a) + DEV_AB = (0x138a, 0x00ab) # HP EliteBook 840 G5 — sensor type 0xd51 + DEV_B7 = (0x06cb, 0x00b7) # HP G6 series — sensor type 0xd51 @classmethod def from_usbid(cls, vendorid, productid): @@ -61,6 +64,34 @@ def open_dev(self, dev: ucore.Device): if dev is None: raise Exception('No matching devices found') + # Defensive USB reset on init. + # + # The 0xd51-family chips (HP 138a:00ab / 06cb:00b7) can be left in + # a "stuck" protocol state across a previous unclean exit of this + # daemon, a cold boot, or a sudden suspend/resume. In that state + # the chip accepts the bulk-OUT but never replies on bulk-IN, so + # the very first cleartext command (typically `cmd 3e` + # get_flash_info) times out — the daemon then restart-loops at + # 15s intervals and the sensor is "vanished" until a manual USB + # reset. This block is the in-driver equivalent of the manual + # `udevadm trigger --attr-match=idVendor=... --attr-match=idProduct=...` + # workaround users have been running to recover. + # + # Reported by Killersparrow1 (#238, Fedora 44, vanishes on reboot) + # and Maarten (Arch, ZBook G5, USBTimeoutError on first 3e). Also + # observed locally on the maintainer's machine (sensor prompts but + # doesn't detect after a while). + try: + vid, pid = dev.idVendor, dev.idProduct + dev.reset() + time.sleep(0.5) + # USB address may shift after reset; re-find by vid/pid. + dev = ucore.find(idVendor=vid, idProduct=pid) + if dev is None: + raise Exception('Device disappeared after USB reset') + except USBError as e: + logging.warning('open_dev: USB reset failed (often non-fatal): %s', e) + self.dev = dev self.dev.default_timeout = 15000 dev.set_configuration()