$ lsusb | grep -i validity
Bus 001 Device 019: ID 138a:00ab Validity Sensors, Inc.
$ lsusb | grep -i validity
Bus 001 Device 019: ID 138a:00ab Validity Sensors, Inc.
$ lsusb | grep -i validity
Bus 001 Device 019: ID 138a:00ab Validity Sensors, Inc.
cmd_01 (rom info) ............. OK, 38 bytes returned
cmd_19 ........................ OK, 68 bytes
vfs7552_init_00 (501 bytes) ... rejected, -weight: 500;">status 0x04be
cmd_01 (rom info) ............. OK, 38 bytes returned
cmd_19 ........................ OK, 68 bytes
vfs7552_init_00 (501 bytes) ... rejected, -weight: 500;">status 0x04be
cmd_01 (rom info) ............. OK, 38 bytes returned
cmd_19 ........................ OK, 68 bytes
vfs7552_init_00 (501 bytes) ... rejected, -weight: 500;">status 0x04be
init_hardcoded (581 bytes): send 06 02 00 00 01 4a 23 14 06 e5 54 2f c6 dc 3b 1a ...
recv 2 bytes, -weight: 500;">status=00 00: 00 00
init_hardcoded (581 bytes): send 06 02 00 00 01 4a 23 14 06 e5 54 2f c6 dc 3b 1a ...
recv 2 bytes, -weight: 500;">status=00 00: 00 00
init_hardcoded (581 bytes): send 06 02 00 00 01 4a 23 14 06 e5 54 2f c6 dc 3b 1a ...
recv 2 bytes, -weight: 500;">status=00 00: 00 00
$ fprintd-verify
Using device /net/reactivated/Fprint/Device/0
Listing enrolled fingers: - #0: right-index-finger
Verify started!
Verifying: any
^C # me, after a minute of nothing
$ fprintd-verify
Using device /net/reactivated/Fprint/Device/0
Listing enrolled fingers: - #0: right-index-finger
Verify started!
Verifying: any
^C # me, after a minute of nothing
$ fprintd-verify
Using device /net/reactivated/Fprint/Device/0
Listing enrolled fingers: - #0: right-index-finger
Verify started!
Verifying: any
^C # me, after a minute of nothing
4104 B cmd40 (× 4) calibration frames
1966 B cmd02 (× 9) per-stage frame data
5040, 9584, 14128, 14128, 18672, 18672, 18672, 23304 B cmd6b enrollment template
4104 B cmd40 (× 4) calibration frames
1966 B cmd02 (× 9) per-stage frame data
5040, 9584, 14128, 14128, 18672, 18672, 18672, 23304 B cmd6b enrollment template
4104 B cmd40 (× 4) calibration frames
1966 B cmd02 (× 9) per-stage frame data
5040, 9584, 14128, 14128, 18672, 18672, 18672, 23304 B cmd6b enrollment template
def capture(self, mode): assert_status(tls.app(self.build_cmd_02(mode))) # -weight: 500;">start b = usb.wait_int() if b[0] != 0: raise Exception('wait_start: Unexpected interrupt type ...') # wait for finger while True: b = usb.wait_int() if b[0] == 2: break # wait capture complete while True: b = usb.wait_int() if b[0] != 3: raise Exception('Unexpected interrupt type ...') if b[2] & 4: break ...
def capture(self, mode): assert_status(tls.app(self.build_cmd_02(mode))) # -weight: 500;">start b = usb.wait_int() if b[0] != 0: raise Exception('wait_start: Unexpected interrupt type ...') # wait for finger while True: b = usb.wait_int() if b[0] == 2: break # wait capture complete while True: b = usb.wait_int() if b[0] != 3: raise Exception('Unexpected interrupt type ...') if b[2] & 4: break ...
def capture(self, mode): assert_status(tls.app(self.build_cmd_02(mode))) # -weight: 500;">start b = usb.wait_int() if b[0] != 0: raise Exception('wait_start: Unexpected interrupt type ...') # wait for finger while True: b = usb.wait_int() if b[0] == 2: break # wait capture complete while True: b = usb.wait_int() if b[0] != 3: raise Exception('Unexpected interrupt type ...') if b[2] & 4: break ...
<int< 00 00 00 00 00 # b[0] = 0 (-weight: 500;">start)
<int< 03 20 07 00 00 # b[0] = 3 (capture event)
<int< 00 00 00 00 00 # b[0] = 0 (-weight: 500;">start)
<int< 03 20 07 00 00 # b[0] = 3 (capture event)
<int< 00 00 00 00 00 # b[0] = 0 (-weight: 500;">start)
<int< 03 20 07 00 00 # b[0] = 3 (capture event)
# wait for finger. Sensor type 0xd51 (138a:00ab, 06cb:00b7) does not
# emit b[0]=2; it jumps directly to capture events. Accept b[0]=3 as
# a substitute and save the interrupt for the next 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) == 0xd51: saved_b = b break # wait capture complete
while True: b = saved_b if saved_b is not None else usb.wait_int() saved_b = None ...
# wait for finger. Sensor type 0xd51 (138a:00ab, 06cb:00b7) does not
# emit b[0]=2; it jumps directly to capture events. Accept b[0]=3 as
# a substitute and save the interrupt for the next 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) == 0xd51: saved_b = b break # wait capture complete
while True: b = saved_b if saved_b is not None else usb.wait_int() saved_b = None ...
# wait for finger. Sensor type 0xd51 (138a:00ab, 06cb:00b7) does not
# emit b[0]=2; it jumps directly to capture events. Accept b[0]=3 as
# a substitute and save the interrupt for the next 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) == 0xd51: saved_b = b break # wait capture complete
while True: b = saved_b if saved_b is not None else usb.wait_int() saved_b = None ...
Verify result: verify-retry-scan (not done)
Verify result: verify-retry-scan (not done)
Verify result: verify-match (done)
Verify result: verify-retry-scan (not done)
Verify result: verify-retry-scan (not done)
Verify result: verify-match (done)
Verify result: verify-retry-scan (not done)
Verify result: verify-retry-scan (not done)
Verify result: verify-match (done)
Verify result: verify-no-match (done)
Verify result: verify-no-match (done)
Verify result: verify-no-match (done)
$ -weight: 600;">sudo -k && -weight: 600;">sudo whoami
[-weight: 600;">sudo] Place your finger on the fingerprint reader
root
$ -weight: 600;">sudo -k && -weight: 600;">sudo whoami
[-weight: 600;">sudo] Place your finger on the fingerprint reader
root
$ -weight: 600;">sudo -k && -weight: 600;">sudo whoami
[-weight: 600;">sudo] Place your finger on the fingerprint reader
root
-weight: 500;">git clone -b feat/sensor-type-0xd51 https://github.com/SimpleX-T/python-validity.-weight: 500;">git
-weight: 600;">sudo -weight: 500;">pip -weight: 500;">install --break-system-packages --prefix=/usr ./python-validity
-weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart python3-validity.-weight: 500;">service
-weight: 500;">git clone -b feat/sensor-type-0xd51 https://github.com/SimpleX-T/python-validity.-weight: 500;">git
-weight: 600;">sudo -weight: 500;">pip -weight: 500;">install --break-system-packages --prefix=/usr ./python-validity
-weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart python3-validity.-weight: 500;">service
-weight: 500;">git clone -b feat/sensor-type-0xd51 https://github.com/SimpleX-T/python-validity.-weight: 500;">git
-weight: 600;">sudo -weight: 500;">pip -weight: 500;">install --break-system-packages --prefix=/usr ./python-validity
-weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">restart python3-validity.-weight: 500;">service - The kernel already supports it (out of the box)
- Someone reverse-engineered the protocol and shipped a userspace driver
- The vendor ships a Linux driver (rare outside of mainstream chipsets)
- It doesn't work - Opened the device
- Sent cmd_01, cmd_19, cmd_4302 (basic info queries) — all worked
- Sent python-validity's init_hardcoded blob (a 581-byte crypto blob for the supported chips) - Wires 138a:00ab and 06cb:00b7 through the SupportedDevices enum, blob routing, firmware mapping, and udev rules
- Aliases sensor type 0xd51 to the 0x199 profile so downstream SensorTypeInfo / SensorCaptureProg lookups succeed (no native profile exists yet — empirically 0x199 is close enough that the on-chip matcher accepts real images)
- Patches Sensor.capture() to accept b[0]=3 as a substitute for b[0]=2, gated on the real device type - #181 — open since 2023
- #225 — 06cb:00b7 user with the same wall
- #238 — newer report - Hardware support on Linux is not "downloading a driver." When the vendor doesn't ship one, real people spend real weeks reverse-engineering protocols. Look at the python-validity codebase sometime; the existing supported chips have ~500-byte chip-specific crypto blobs in them that were extracted from USB captures of Windows driver sessions. That work doesn't happen by itself.
- When a hypothesis explains some of the evidence, don't -weight: 500;">stop. I had a clean story — "calibration is wrong, images are bad, matching fails" — that explained the verify failure. It did not explain why enrollment completed cleanly and why the template was growing. I should have noticed the contradiction earlier.
- Look at the actual data the chip is sending. Adding the TLS-response dump took ten minutes and changed everything. I had been reasoning about what I thought the chip was sending; the actual bytes told a different story.
- AI-assisted reverse engineering is a real workflow now. I did this with Claude Code; the model held the protocol knowledge I didn't have and ran disassemblers and code searches in parallel while I was deciding what to do next. The interrupt-handling insight was a result of "let me actually look at every byte the chip sent us and check the contradictions" — a kind of careful empiricism that's much easier with an assistant doing the bookkeeping.