Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b850fd1
Add support for sensor type 0xd51 (138a:00ab, 06cb:00b7)
SimpleX-T May 19, 2026
20df336
Add FIRMWARE_URIS entries for DEV_AB and DEV_B7
SimpleX-T May 20, 2026
4ac8bbd
Replace existing finger record on same-subtype re-enroll
SimpleX-T May 21, 2026
90d5d67
validity-sensors-firmware: fall back to cabextract for non-Inno archives
SimpleX-T May 21, 2026
d1936a1
Debian packaging: PAM auto-enable, cabextract dep, version 0.16~hp3
SimpleX-T May 21, 2026
bc5dd32
Emit verify-retry-scan at most once per VerifyStart cycle
SimpleX-T May 21, 2026
122a147
Exclude local build artifacts from the source tarball
SimpleX-T May 21, 2026
baec76e
Add diagnostic logging for chip geometry and per-verify retry count
SimpleX-T May 21, 2026
cd0def0
usb.py: defensive USB reset at open_dev() entry
SimpleX-T May 24, 2026
0c00f0a
Add sensor type 0x969 (HP ZBook Studio x360 G5, 138a:00ab)
ggiesen Jul 6, 2026
3b23194
sensor.py: recover 0x969 chips reporting 0x199 post-resume
SimpleX-T Jul 7, 2026
4ba618b
Surface 0404 at reset_blob with an actionable error
SimpleX-T Jul 7, 2026
82584e1
db.py: fix del_record; raise a clear error when the DB partition is full
SimpleX-T Jul 7, 2026
2f4dc2e
README: document 0xd51/0x969 caveats surfaced by PR #256 reviewers
SimpleX-T Jul 7, 2026
7925b97
dbus-service: watchdog on VerifyStart for wedged-chip state
SimpleX-T Jul 7, 2026
a825eaf
debian/changelog: 0.16~hp8 for noble
SimpleX-T Jul 7, 2026
fddbd07
hw_tables: return independent device identities
SimpleX-T Aug 2, 2026
01500cb
persist sensor calibration across reboots
SimpleX-T Aug 2, 2026
bfaeb4e
Add 06cb:00cb (HP Pavilion x360 14-dh, sensor type 0x969)
ggiesen Jul 9, 2026
6a1ae05
debian: include 06cb:00cb support in hp9
SimpleX-T Aug 2, 2026
d1a67b3
serialize sensor operations and make cancellation race-free
SimpleX-T Aug 3, 2026
c809f5d
finish sensor sessions and harden zero-config installs
SimpleX-T Aug 3, 2026
ee925fd
retry explicit no-template results across d51 variants
SimpleX-T Aug 3, 2026
9ca5fc3
reset scan state between rejected capture attempts
SimpleX-T Aug 3, 2026
adc6d98
retry open-fprintd registration across startup races
SimpleX-T Aug 3, 2026
a4fc26c
debian: package startup registration retry as hp14
SimpleX-T Aug 3, 2026
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
91 changes: 70 additions & 21 deletions bin/validity-sensors-firmware
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ import urllib.request

from usb import core as usb_core

from validitysensor.init_data_dir import PYTHON_VALIDITY_DATA_DIR
from validitysensor.init_data_dir import (
PYTHON_VALIDITY_FIRMWARE_DIR,
init_data_dir,
)
from validitysensor.firmware_tables import FIRMWARE_NAMES, FIRMWARE_URIS
from validitysensor.usb import SupportedDevices

Expand All @@ -47,32 +50,61 @@ def download_and_extract_fw(dev_type, fwdir, fwuri=None):
req.add_header('Referer', FIRMWARE_URIS[dev_type].get('referral', ''))
req.add_header('User-Agent', 'Mozilla/5.0 (X11; U; Linux)')

hash = hashlib.sha512()
archive_hash = hashlib.sha512()
with urllib.request.urlopen(req) as response:
with open(fwarchive, 'wb') as out_file:
data = response.read()
hash.update(data)
out_file.write(data)

actual_hash = hash.hexdigest()
while True:
data = response.read(1024 * 1024)
if not data:
break
archive_hash.update(data)
out_file.write(data)

actual_hash = archive_hash.hexdigest()
if actual_hash != expected_hash:
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
])

fwpath = subprocess.check_output(['find', fwdir, '-name', fwname]).decode('utf-8').strip()
print('Found firmware at {}'.format(fwpath))

if not fwpath:
# 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
], stdout=subprocess.DEVNULL, 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],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
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 = None
for root, _, files in os.walk(fwdir):
if fwname in files:
fwpath = os.path.join(root, fwname)
break
if fwpath is None:
raise Exception('No {} found in the archive'.format(fwname))

return fwpath


def install_firmware(source, destination):
temporary = destination + '.installing'
shutil.copyfile(source, temporary)
os.chmod(temporary, 0o600)
os.replace(temporary, destination)


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--driver-uri')
Expand All @@ -82,6 +114,8 @@ if __name__ == "__main__":
if os.geteuid() != 0:
raise Exception('This script needs to be executed as root')

init_data_dir()

dev_type = None
for d in SupportedDevices:
dev = usb_core.find(idVendor=d.value[0], idProduct=d.value[1])
Expand All @@ -91,12 +125,27 @@ 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))
destination = os.path.join(
PYTHON_VALIDITY_FIRMWARE_DIR,
FIRMWARE_NAMES[dev_type],
)
if os.path.isfile(destination):
print('Firmware already cached at {}'.format(destination))
sys.exit(0)

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:
fwpath = download_and_extract_fw(dev_type, fwdir, fwuri=args.driver_uri)
shutil.copy(fwpath, PYTHON_VALIDITY_DATA_DIR)
install_firmware(fwpath, destination)
print('Firmware ready at {}'.format(destination))
Loading