Add support for 06cb:00bb (HP Spectre x360 13-ap0xxx) — most of the groundwork is done, blocked on one key-derivation step
Device
- USB ID:
06cb:00bb ("Synaptics WBDI Fingerprint Reader - USB 102")
- Laptop: HP Spectre x360 Convertible 13-ap0xxx
- Previously the only reference to this exact PID was nmikhailov/Validity90#71, open without resolution since 2019.
What I did
I captured the real Windows Hello driver (HP SoftPaqs SP92631 fingerprint driver + SP96872 Intel SGX PSW — this variant uses an SGX enclave, synaTEE102.signed.dll, for the crypto) talking to the physical sensor, via a libvirt VM with the sensor passed through as a USB hostdev, captured host-side with usbmon/tshark (~4300 packets, ~10 successful TLS 1.2 handshakes with the real chip). Then cross-checked everything against this repo's real source (not just filenames) and against static analysis of the two driver DLLs (synaWudfBioUsb102SGX.dll, synaTEE102.signed.dll).
Findings
1. init_hardcoded for 06cb:00bb, found and verified
usb.py: send_init() sends 01, 19, 4302, then init_hardcoded. The real Windows driver sends the exact same sequence, and the value it sends for the 4th step is identical across all 25 occurrences in the capture (spanning ~66 min, multiple VM reboots) — i.e. it's static, not session-randomized. I found this exact 581-byte value (06 02 00 00 01 header + 576-byte payload) verbatim inside synaTEE102.signed.dll and synaWudfBioUsb102SGX.dll, as one entry (offset 0x1ad034 / 1757236 in the former) of a table of 77 similar entries — presumably one per supported sensor variant.
# validitysensor/blobs_bb.py
from .util import unhex
init_hardcoded = unhex(
"0602000001"
"4a231406e5542fc6dc3b1aedebe68f55596ad3ca13f6e019994c6f71672fff756fbde0511d09d45978b12ba415b3694a0e76348c8cfe9dbb9abf86813fc0c67c1005519a6f87360c2fb3e12bd0a9e012b06d9f5c9b44ccc6645b0fbd47afe45c8c874fcb88fbfd18fb7a9b3241351f256acce689f9586a52b01f8fdcb66cdf3b340b1f9f386d58ca24fdfcdfbcebefb5f3a3c2a08357721040235a20ce1ee2f4f7856e0d9c27b92cd9b975c86f2c8cab1179868f795da674004b93c15e6ac8aa825a1907f2003cb9e6df096423167b2cabae98c0cd3fd200d11c7e0ee1ba5a725f7f2022886f3caa5f68b688ba61bc5cb0190db569efa0a57aa9d76ecdc7440c8920ea02768734221260d083bebb39c176d129c01d1a0f1388497171402ba041afd925d71e76ce4905e44fa5fd528759a2c9f12895864b5aa394dc71a4a17161dd82197a10742fa5f3135c5e78820e36653fa3db535f57c71897242939d7da50f81070ce9ab81c61af6ac29a6c6c4a5df73ffd08542fb540e417939ed117298051d27736c2faf1c5577a2133b6f60ea7484c692faae2a49c51c8e6f69af77774bad51a9adeea3109d3611d6a437cdc0c356e46a8f9a6d3054c5519c17c986e54f61f8329006ce184c275984799dbdf5512188fa8ff10aa2ddc25eb697ebdcb156509309ade5d7909a734bf35ec69e062cb941c2ea4af0958110da93bd2b5f17fc9b1ebdbd8020a3c36f22a68f707226cec716126d6a830219521669bd59fa0e4bd35db6ef0aa2999d1c0e7acf67e598696cd58cc4bdb1b7c037ee9a085f784c4"
)
init_hardcoded_clean_slate = None # never observed — err was always 0 on this device
reset_blob = None
db_write_enable = None
2. 06cb:00bb's on-flash parse_tls_flash() blocks decode 100% cleanly with the existing format
The 4096-byte read_tls_flash() response from a real device decodes perfectly as 3 concatenated blocks in your existing id/sz/hash/body format:
| Block |
Offset |
id |
sz |
| private key |
45 |
4 |
161 |
| cert |
242 |
3 |
184 |
| ECDH |
1502 |
6 |
400 |
The ECDH sub-fields land exactly where handle_ecdh() expects them (x @ body+0x08, y @ body+0x4c, a 71-byte valid DER ECDSA signature @ body+0x90 with its 4-byte LE length prefix). So no new flash format for this variant — it's identical to what's already implemented.
3. password_hardcoded / gwk_sign_hardcoded are shared, not per-variant
I disassembled synaTEE102.signed.dll (x86-64 PE, objdump) looking for the GWK/GWK_SIGN/HS_KEY_PAIR_GEN PRF-label strings (all present verbatim — same protocol), found the two functions that build a 32-byte buffer byte-by-byte right before each PRF call, and extracted both constants. Both are byte-for-byte identical to the values already hardcoded in tls.py. So this isn't the blocker either.
4. The actual blocker: handle_priv()'s HMAC check fails, and I can't determine what hw_key this driver variant actually uses
With everything above correct, tls.parse_tls_flash() → handle_priv() still raises Signature verification failed. This device was probably paired with another computer.
set_hwkey() assumes hw_key = product_name + '\0' + serial + '\0' from /sys/class/dmi/id/product_name/product_serial (Windows-side equivalent presumably SMBIOS too). I tried, all offline against the real captured HMAC:
- The real laptop's actual DMI identity (
HP Spectre x360 Convertible 13-ap0xxx + real serial)
- The exact identity the capturing VM reported to Windows itself (confirmed via
Get-CimInstance Win32_ComputerSystem/Win32_BIOS inside the VM that did the real capture: Manufacturer=QEMU, Model="Ubuntu 24.04 PC (Q35 + ICH9, 2009)", SerialNumber="") — the VM has no libvirt <sysinfo>/<smbios> override, so this is QEMU's generic default SMBIOS, and it's what the real driver saw when it successfully completed ~10 TLS handshakes with the physical chip.
- The Windows
MachineGuid (HKLM\SOFTWARE\Microsoft\Cryptography) from that same VM/install.
- ~10 formatting variants of the above (reversed order, UTF-16, trailing space/no null terminators, manufacturer-only, braces around the GUID, raw GUID bytes in both byte orders, empty/empty).
None matched. Notably, strings on synaWudfBioUsb102SGX.dll (the non-enclave wrapper) shows imports for RegQueryValueExA/W, RegOpenKeyExW, CryptUnprotectData, CryptUnprotectMemory — no SMBIOS/WMI-related strings at all in either DLL. That suggests this driver variant may source (part of) its key material from the registry (possibly DPAPI-protected) rather than purely from product_name/serial — but I wasn't able to trace the actual call site without a decompiler (I only have objdump; happy to share the raw disassembly or binaries if that helps).
What I'm attaching / can provide
flash_read_4096B.bin — the real, complete 4096-byte read_tls_flash() response from this physical 06cb:00bb chip (read-only capture, includes the still-encrypted private key blob, cert, and ECDH block — nothing here is secret since the private key stays encrypted).
host_cert_blob_581B.bin — the init_hardcoded value above, as raw bytes.
- The two HP driver DLLs (
synaWudfBioUsb102SGX.dll, synaTEE102.signed.dll) if useful for someone with better disassembly tooling — from the public HP SoftPaq SP92631 (https://ftp.hp.com/pub/softpaq/sp92501-93000/sp92631.exe).
- The full
.pcapng capture (~4300 packets, real Windows driver ↔ real chip, ~10 complete TLS handshakes) if useful.
Happy to test any hypothesis against the real hardware (carefully — this is a match-on-chip sensor, I want to avoid sending anything destructive) or capture more traffic if it would help.
Add support for
06cb:00bb(HP Spectre x360 13-ap0xxx) — most of the groundwork is done, blocked on one key-derivation stepDevice
06cb:00bb("Synaptics WBDI Fingerprint Reader - USB 102")What I did
I captured the real Windows Hello driver (HP SoftPaqs
SP92631fingerprint driver +SP96872Intel SGX PSW — this variant uses an SGX enclave,synaTEE102.signed.dll, for the crypto) talking to the physical sensor, via a libvirt VM with the sensor passed through as a USB hostdev, captured host-side withusbmon/tshark(~4300 packets, ~10 successful TLS 1.2 handshakes with the real chip). Then cross-checked everything against this repo's real source (not just filenames) and against static analysis of the two driver DLLs (synaWudfBioUsb102SGX.dll,synaTEE102.signed.dll).Findings
1.
init_hardcodedfor06cb:00bb, found and verifiedusb.py: send_init()sends01,19,4302, theninit_hardcoded. The real Windows driver sends the exact same sequence, and the value it sends for the 4th step is identical across all 25 occurrences in the capture (spanning ~66 min, multiple VM reboots) — i.e. it's static, not session-randomized. I found this exact 581-byte value (06 02 00 00 01header + 576-byte payload) verbatim insidesynaTEE102.signed.dllandsynaWudfBioUsb102SGX.dll, as one entry (offset0x1ad034/1757236in the former) of a table of 77 similar entries — presumably one per supported sensor variant.2.
06cb:00bb's on-flashparse_tls_flash()blocks decode 100% cleanly with the existing formatThe 4096-byte
read_tls_flash()response from a real device decodes perfectly as 3 concatenated blocks in your existingid/sz/hash/bodyformat:idszThe ECDH sub-fields land exactly where
handle_ecdh()expects them (x@ body+0x08,y@ body+0x4c, a 71-byte valid DER ECDSA signature @ body+0x90 with its 4-byte LE length prefix). So no new flash format for this variant — it's identical to what's already implemented.3.
password_hardcoded/gwk_sign_hardcodedare shared, not per-variantI disassembled
synaTEE102.signed.dll(x86-64 PE,objdump) looking for theGWK/GWK_SIGN/HS_KEY_PAIR_GENPRF-label strings (all present verbatim — same protocol), found the two functions that build a 32-byte buffer byte-by-byte right before each PRF call, and extracted both constants. Both are byte-for-byte identical to the values already hardcoded intls.py. So this isn't the blocker either.4. The actual blocker:
handle_priv()'s HMAC check fails, and I can't determine whathw_keythis driver variant actually usesWith everything above correct,
tls.parse_tls_flash()→handle_priv()still raisesSignature verification failed. This device was probably paired with another computer.set_hwkey()assumeshw_key = product_name + '\0' + serial + '\0'from/sys/class/dmi/id/product_name/product_serial(Windows-side equivalent presumably SMBIOS too). I tried, all offline against the real captured HMAC:HP Spectre x360 Convertible 13-ap0xxx+ real serial)Get-CimInstance Win32_ComputerSystem/Win32_BIOSinside the VM that did the real capture:Manufacturer=QEMU,Model="Ubuntu 24.04 PC (Q35 + ICH9, 2009)",SerialNumber="") — the VM has no libvirt<sysinfo>/<smbios>override, so this is QEMU's generic default SMBIOS, and it's what the real driver saw when it successfully completed ~10 TLS handshakes with the physical chip.MachineGuid(HKLM\SOFTWARE\Microsoft\Cryptography) from that same VM/install.None matched. Notably,
stringsonsynaWudfBioUsb102SGX.dll(the non-enclave wrapper) shows imports forRegQueryValueExA/W,RegOpenKeyExW,CryptUnprotectData,CryptUnprotectMemory— no SMBIOS/WMI-related strings at all in either DLL. That suggests this driver variant may source (part of) its key material from the registry (possibly DPAPI-protected) rather than purely fromproduct_name/serial— but I wasn't able to trace the actual call site without a decompiler (I only haveobjdump; happy to share the raw disassembly or binaries if that helps).What I'm attaching / can provide
flash_read_4096B.bin— the real, complete 4096-byteread_tls_flash()response from this physical06cb:00bbchip (read-only capture, includes the still-encrypted private key blob, cert, and ECDH block — nothing here is secret since the private key stays encrypted).host_cert_blob_581B.bin— theinit_hardcodedvalue above, as raw bytes.synaWudfBioUsb102SGX.dll,synaTEE102.signed.dll) if useful for someone with better disassembly tooling — from the public HP SoftPaqSP92631(https://ftp.hp.com/pub/softpaq/sp92501-93000/sp92631.exe)..pcapngcapture (~4300 packets, real Windows driver ↔ real chip, ~10 complete TLS handshakes) if useful.Happy to test any hypothesis against the real hardware (carefully — this is a match-on-chip sensor, I want to avoid sending anything destructive) or capture more traffic if it would help.