Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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.
Expand Down
35 changes: 27 additions & 8 deletions bin/validity-sensors-firmware
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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:
Expand Down
46 changes: 45 additions & 1 deletion dbus_service/dbus-service
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
160 changes: 160 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -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 <ntmark2004@gmail.com> 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 <ntmark2004@gmail.com> 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 <ntmark2004@gmail.com> 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/<dev>/... path.

-- SimpleX-T <ntmark2004@gmail.com> 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 <ntmark2004@gmail.com> 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 <ntmark2004@gmail.com> 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 <ntmark2004@gmail.com> 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 <ntmark2004@gmail.com> 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
Expand Down
1 change: 1 addition & 0 deletions debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -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.
.
Expand Down
3 changes: 3 additions & 0 deletions debian/python3-validity.postinst
Original file line number Diff line number Diff line change
Expand Up @@ -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

2 changes: 2 additions & 0 deletions debian/python3-validity.udev
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
5 changes: 5 additions & 0 deletions debian/source/options
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
tar-ignore = ".pybuild"
tar-ignore = "build"
tar-ignore = "*.egg-info"
tar-ignore = "__pycache__"
tar-ignore = "*.pyc"
4 changes: 4 additions & 0 deletions validitysensor/blobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading