From 95fccfdac8a688c5629d531c0ae5ba863d6ee485 Mon Sep 17 00:00:00 2001 From: Leonardo Francisco Date: Sun, 5 Apr 2026 15:04:43 -0400 Subject: [PATCH] =?UTF-8?q?validity:=20Iteration=204=20=E2=80=94=20Sensor?= =?UTF-8?q?=20identification=20and=20HAL=20tables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add post-TLS sensor identification infrastructure: TLS command mechanism (vcsfw_tls_cmd_send): - Reusable 2-state subsm for sending VCSFW commands inside TLS channel - Uses 0x44 prefix + TLS app_data wrapping for sends - Decrypts TLS response and extracts VCSFW status + payload Sensor identification (cmd 0x75): - validity_sensor_parse_identify() parses hw_major/hw_version - DeviceInfo table (26 entries): maps (major, version) to device name and sensor type, with exact and fuzzy matching - SensorTypeInfo table (14 entries): maps sensor_type to geometry parameters (bytes_per_line, line_width, calibration blob, etc.) Factory bits retrieval (cmd 0x6f): - validity_sensor_build_factory_bits_cmd() builds 9-byte command - Response stored in sensor state for calibration (Iteration 5) Open sequence integration: - 4 new SSM states: OPEN_SENSOR_IDENTIFY, OPEN_SENSOR_IDENTIFY_RECV, OPEN_SENSOR_FACTORY_BITS, OPEN_SENSOR_FACTORY_BITS_RECV - Sensor state init/clear wired into dev_open/dev_close New files: validity_sensor.h, validity_sensor.c Tests: 14 unit tests in test-validity-sensor.c (all passing) Full suite: 6/6 OK, 0 failures --- libfprint/drivers/validity/validity.c | 132 +++++++ libfprint/drivers/validity/validity.h | 4 + libfprint/drivers/validity/validity_sensor.c | 311 +++++++++++++++++ libfprint/drivers/validity/validity_sensor.h | 126 +++++++ libfprint/drivers/validity/vcsfw_protocol.c | 166 +++++++++ libfprint/drivers/validity/vcsfw_protocol.h | 16 + libfprint/meson.build | 3 +- tests/meson.build | 14 + tests/test-validity-sensor.c | 347 +++++++++++++++++++ 9 files changed, 1118 insertions(+), 1 deletion(-) create mode 100644 libfprint/drivers/validity/validity_sensor.c create mode 100644 libfprint/drivers/validity/validity_sensor.h create mode 100644 tests/test-validity-sensor.c diff --git a/libfprint/drivers/validity/validity.c b/libfprint/drivers/validity/validity.c index c7e13744..0e78e2fd 100644 --- a/libfprint/drivers/validity/validity.c +++ b/libfprint/drivers/validity/validity.c @@ -179,6 +179,10 @@ typedef enum { OPEN_TLS_READ_FLASH, OPEN_TLS_DERIVE_PSK, OPEN_TLS_HANDSHAKE, + OPEN_SENSOR_IDENTIFY, + OPEN_SENSOR_IDENTIFY_RECV, + OPEN_SENSOR_FACTORY_BITS, + OPEN_SENSOR_FACTORY_BITS_RECV, OPEN_DONE, OPEN_NUM_STATES, } ValidityOpenSsmState; @@ -488,6 +492,132 @@ open_run_state (FpiSsm *ssm, } break; + case OPEN_SENSOR_IDENTIFY: + { + FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev); + + /* Without TLS, sensor identification is not possible */ + if (!self->tls.secure_rx) + { + fp_info ("No TLS session — skipping sensor identification"); + fpi_ssm_jump_to_state (ssm, OPEN_DONE); + return; + } + + /* Send cmd 0x75 (identify_sensor) via TLS. + * NULL callback: subsm auto-advances, response stashed in + * self->cmd_response_data for the RECV state. */ + guint8 cmd[] = { VCSFW_CMD_IDENTIFY_SENSOR }; + vcsfw_tls_cmd_send (self, ssm, cmd, sizeof (cmd), NULL); + } + break; + + case OPEN_SENSOR_IDENTIFY_RECV: + { + FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev); + + if (self->cmd_response_status != VCSFW_STATUS_OK) + { + fp_warn ("identify_sensor failed: status=0x%04x", + self->cmd_response_status); + fpi_ssm_jump_to_state (ssm, OPEN_DONE); + return; + } + + if (!validity_sensor_parse_identify (self->cmd_response_data, + self->cmd_response_len, + &self->sensor.ident)) + { + fp_warn ("identify_sensor: response too short"); + fpi_ssm_jump_to_state (ssm, OPEN_DONE); + return; + } + + fp_info ("Sensor hardware: major=0x%04x version=0x%04x", + self->sensor.ident.hw_major, + self->sensor.ident.hw_version); + + /* Look up device info and sensor type */ + self->sensor.device_info = validity_device_info_lookup ( + self->sensor.ident.hw_major, + self->sensor.ident.hw_version); + + if (self->sensor.device_info) + { + fp_info ("Device: %s (type=0x%04x)", + self->sensor.device_info->name, + self->sensor.device_info->type); + + self->sensor.type_info = validity_sensor_type_info_lookup ( + self->sensor.device_info->type); + + if (self->sensor.type_info) + fp_info ("Sensor type: 0x%04x, %u bytes/line, %ux repeat", + self->sensor.type_info->sensor_type, + self->sensor.type_info->bytes_per_line, + self->sensor.type_info->repeat_multiplier); + else + fp_warn ("Unknown sensor type 0x%04x", + self->sensor.device_info->type); + } + else + { + fp_warn ("Unknown hardware major=0x%04x version=0x%04x", + self->sensor.ident.hw_major, + self->sensor.ident.hw_version); + } + + fpi_ssm_next_state (ssm); + } + break; + + case OPEN_SENSOR_FACTORY_BITS: + { + FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev); + + /* Factory bits are needed for calibration. If sensor wasn't + * identified, skip this step. */ + if (!self->sensor.device_info) + { + fp_info ("No sensor info — skipping factory bits"); + fpi_ssm_jump_to_state (ssm, OPEN_DONE); + return; + } + + /* Build and send cmd 0x6f (GET_FACTORY_BITS) with tag 0x0e00 */ + guint8 cmd[9]; + validity_sensor_build_factory_bits_cmd (0x0e00, cmd, sizeof (cmd)); + vcsfw_tls_cmd_send (self, ssm, cmd, sizeof (cmd), NULL); + } + break; + + case OPEN_SENSOR_FACTORY_BITS_RECV: + { + FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev); + + if (self->cmd_response_status != VCSFW_STATUS_OK) + { + fp_warn ("get_factory_bits failed: status=0x%04x", + self->cmd_response_status); + /* Non-fatal: calibration will have to work without factory data */ + fpi_ssm_next_state (ssm); + return; + } + + /* Store raw factory bits for calibration (iter 5) */ + g_clear_pointer (&self->sensor.factory_bits, g_free); + if (self->cmd_response_data && self->cmd_response_len > 0) + { + self->sensor.factory_bits = g_memdup2 (self->cmd_response_data, + self->cmd_response_len); + self->sensor.factory_bits_len = self->cmd_response_len; + fp_info ("Factory bits: %zu bytes", self->sensor.factory_bits_len); + } + + fpi_ssm_next_state (ssm); + } + break; + case OPEN_DONE: /* All init commands sent. Mark open complete. */ fpi_ssm_mark_completed (ssm); @@ -526,6 +656,7 @@ dev_open (FpDevice *device) self->interrupt_cancellable = g_cancellable_new (); validity_tls_init (&self->tls); + validity_sensor_state_init (&self->sensor); if (!g_usb_device_claim_interface (fpi_device_get_usb_device (device), 0, 0, &error)) { @@ -554,6 +685,7 @@ dev_close (FpDevice *device) g_clear_pointer (&self->cmd_response_data, g_free); self->cmd_response_len = 0; + validity_sensor_state_clear (&self->sensor); validity_tls_free (&self->tls); g_clear_object (&self->interrupt_cancellable); diff --git a/libfprint/drivers/validity/validity.h b/libfprint/drivers/validity/validity.h index 0eb25e1f..3ddb5a1c 100644 --- a/libfprint/drivers/validity/validity.h +++ b/libfprint/drivers/validity/validity.h @@ -22,6 +22,7 @@ #include "fpi-device.h" #include "fpi-ssm.h" +#include "validity_sensor.h" #include "validity_tls.h" /* USB Endpoint addresses */ @@ -98,6 +99,9 @@ struct _FpiDeviceValidity /* TLS session state */ ValidityTlsState tls; + /* Sensor identification and HAL state (post-TLS) */ + ValiditySensorState sensor; + /* Firmware extension status */ gboolean fwext_loaded; diff --git a/libfprint/drivers/validity/validity_sensor.c b/libfprint/drivers/validity/validity_sensor.c new file mode 100644 index 00000000..377243b2 --- /dev/null +++ b/libfprint/drivers/validity/validity_sensor.c @@ -0,0 +1,311 @@ +/* + * Sensor identification and HAL tables for Validity/Synaptics VCSFW + * + * Copyright (C) 2024 libfprint contributors + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#define FP_COMPONENT "validity" + +#include "drivers_api.h" +#include "fpi-byte-reader.h" +#include "fpi-byte-utils.h" +#include "validity_sensor.h" + +/* ================================================================ + * Calibration blobs (indexed by SensorTypeInfo) + * + * Extracted from python-validity generated_tables.py. + * Each blob is used during calibration to build the key_line for + * the timeslot table (see get_key_line in python-validity). + * ================================================================ */ + +/* Blob for 57K0-family: 0x00b5, 0x0199, 0x0885, 0x1055, 0x1825, 0x1ff5, 0x00ed + * 112 bytes, matching line_width=112 */ +static const guint8 calib_blob_57k0[] = { + 0x9b, 0x9a, 0x99, 0x97, 0x96, 0x95, 0x93, 0x92, + 0x91, 0x8f, 0x8e, 0x8d, 0x8b, 0x8a, 0x89, 0x87, + 0x86, 0x85, 0x83, 0x82, 0x81, 0x7f, 0x7e, 0x7d, + 0x7b, 0x7a, 0x79, 0x77, 0x76, 0x75, 0x73, 0x72, + 0x71, 0x6f, 0x6e, 0x6d, 0x6b, 0x6a, 0x69, 0x67, + 0x66, 0x65, 0x63, 0x62, 0x61, 0x5f, 0x5e, 0x5d, + 0x5b, 0x5a, 0x59, 0x57, 0x56, 0x55, 0x52, 0x51, + 0x50, 0x4e, 0x4d, 0x4c, 0x4a, 0x49, 0x48, 0x46, + 0x45, 0x44, 0x42, 0x41, 0x40, 0x3e, 0x3d, 0x3c, + 0x3a, 0x39, 0x38, 0x36, 0x35, 0x34, 0x32, 0x31, + 0x30, 0x2e, 0x2d, 0x2c, 0x2a, 0x29, 0x28, 0x26, + 0x25, 0x24, 0x22, 0x21, 0x20, 0x1e, 0x1d, 0x1c, + 0x1a, 0x19, 0x18, 0x16, 0x15, 0x14, 0x12, 0x11, + 0x10, 0x0e, 0x0d, 0x0c, 0x0a, 0x09, 0x08, 0x06, +}; + +/* Blob for 73A0/73A1 family: 0x00b3, 0x143b + * 85 bytes for 0x00b3 (line_width=85), 84 bytes for 0x143b */ +static const guint8 calib_blob_73a[] = { + 0x89, 0x87, 0x86, 0x85, 0x83, 0x82, 0x81, 0x7f, + 0x7e, 0x7d, 0x7b, 0x7a, 0x79, 0x77, 0x76, 0x75, + 0x73, 0x72, 0x71, 0x6f, 0x6e, 0x6d, 0x6b, 0x6a, + 0x69, 0x67, 0x66, 0x65, 0x63, 0x62, 0x61, 0x5f, + 0x5e, 0x5d, 0x5b, 0x5a, 0x59, 0x57, 0x56, 0x55, + 0x52, 0x51, 0x50, 0x4e, 0x4d, 0x4c, 0x4a, 0x49, + 0x48, 0x46, 0x45, 0x44, 0x42, 0x41, 0x40, 0x3e, + 0x3d, 0x3c, 0x3a, 0x39, 0x38, 0x36, 0x35, 0x34, + 0x32, 0x31, 0x30, 0x2e, 0x2d, 0x2c, 0x2a, 0x29, + 0x28, 0x26, 0x25, 0x24, 0x22, 0x21, 0x20, 0x1e, + 0x1d, 0x1c, 0x1a, 0x19, 0x18, +}; + +/* Blob for 55E/55D family: 0x00db + * 144 bytes (line_width=144) */ +static const guint8 calib_blob_55e[] = { + 0x93, 0x92, 0x91, 0x8f, 0x8e, 0x8d, 0x8b, 0x8a, + 0x89, 0x87, 0x86, 0x85, 0x83, 0x82, 0x81, 0x7f, + 0x7e, 0x7d, 0x7b, 0x7a, 0x79, 0x77, 0x76, 0x75, + 0x73, 0x72, 0x71, 0x6f, 0x6e, 0x6d, 0x6b, 0x6a, + 0x69, 0x67, 0x66, 0x65, 0x63, 0x62, 0x61, 0x5f, + 0x5e, 0x5d, 0x5b, 0x5a, 0x59, 0x57, 0x56, 0x55, + 0x52, 0x51, 0x50, 0x4e, 0x4d, 0x4c, 0x4a, 0x49, + 0x48, 0x46, 0x45, 0x44, 0x42, 0x41, 0x40, 0x3e, + 0x3d, 0x3c, 0x3a, 0x39, 0x38, 0x36, 0x35, 0x34, + 0x32, 0x31, 0x30, 0x2e, 0x2d, 0x2c, 0x2a, 0x29, + 0x28, 0x26, 0x25, 0x24, 0x22, 0x21, 0x20, 0x1e, + 0x1d, 0x1c, 0x1a, 0x19, 0x18, 0x16, 0x15, 0x14, + 0x12, 0x11, 0x10, 0x0e, 0x0d, 0x0c, 0x0a, 0x09, + 0x08, 0x06, + /* remaining bytes filled to line_width=144 */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, +}; + +/* ================================================================ + * SensorTypeInfo table + * + * From python-validity generated_tables.py SensorTypeInfo.table. + * Only sensor types relevant to supported USB devices are included. + * ================================================================ */ + +static const ValiditySensorTypeInfo sensor_type_info_table[] = { + /* 57K0 family (06cb:009a) */ + { 0x00b5, 0x78, 2, 112, 112, calib_blob_57k0, sizeof (calib_blob_57k0) }, + { 0x0199, 0x78, 2, 112, 112, calib_blob_57k0, sizeof (calib_blob_57k0) }, + { 0x0885, 0x78, 2, 112, 112, calib_blob_57k0, sizeof (calib_blob_57k0) }, + { 0x1055, 0x78, 2, 112, 112, calib_blob_57k0, sizeof (calib_blob_57k0) }, + { 0x1825, 0x78, 2, 112, 112, calib_blob_57k0, sizeof (calib_blob_57k0) }, + { 0x1ff5, 0x78, 2, 112, 112, calib_blob_57k0, sizeof (calib_blob_57k0) }, + { 0x00ed, 0x78, 2, 112, 112, calib_blob_57k0, sizeof (calib_blob_57k0) }, + + /* 73A family */ + { 0x00b3, 0x60, 2, 84, 85, calib_blob_73a, 85 }, + { 0x143b, 0x5c, 2, 84, 84, calib_blob_73a, 84 }, + + /* 55E family */ + { 0x00db, 0x98, 1, 144, 144, calib_blob_55e, sizeof (calib_blob_55e) }, + + /* 57K2 */ + { 0x00e4, 0x78, 2, 100, 112, calib_blob_57k0, sizeof (calib_blob_57k0) }, + + /* 75B0 */ + { 0x08b1, 0x58, 2, 78, 78, NULL, 0 }, + + /* 55B */ + { 0x00e1, 0x58, 2, 78, 78, NULL, 0 }, + + /* 77A */ + { 0x00ea, 0x5c, 1, 84, 84, NULL, 0 }, +}; + +#define SENSOR_TYPE_INFO_TABLE_LEN G_N_ELEMENTS (sensor_type_info_table) + +/* ================================================================ + * DeviceInfo table + * + * From python-validity hw_tables.py dev_info_table. + * Includes entries for hardware majors seen on supported USB devices. + * ================================================================ */ + +static const ValidityDeviceInfo device_info_table[] = { + /* major=0x004a: SYN 57K0 series (06cb:009a, ThinkPad T480s etc.) */ + { 0x004a, 0x00b5, 0x01, 0xff, "SYN 57K0" }, + { 0x004a, 0x0885, 0x02, 0xff, "SYN 57K1" }, + { 0x004a, 0x1055, 0x03, 0xff, "SYN 57K0 HEK" }, + { 0x004a, 0x00b5, 0x05, 0xff, "SYN 57K0 Gold1" }, + { 0x004a, 0x00b5, 0x06, 0xff, "SYN 57K0 Gold2" }, + { 0x004a, 0x00b5, 0x07, 0xff, "SYN 57K0 Gold3" }, + { 0x004a, 0x00b5, 0x08, 0xff, "SYN 57K0 Silver" }, + { 0x004a, 0x00b5, 0x09, 0xff, "SYN 57K0 FM114-001" }, + { 0x004a, 0x00b5, 0x0a, 0xff, "SYN 57K0 FM94-006" }, + { 0x004a, 0x00b5, 0x0b, 0xff, "SYN 57K0 FM94-007" }, + { 0x004a, 0x1825, 0x0c, 0xff, "SYN 57K0 FM154-001" }, + { 0x004a, 0x1825, 0x0d, 0xff, "SYN 57K0 FM155-001" }, + { 0x004a, 0x1825, 0x0e, 0xff, "SYN 57K0 FM154-002" }, + { 0x004a, 0x1825, 0x0f, 0xff, "SYN 57K0 FM154-003" }, + { 0x004a, 0x00b5, 0x10, 0xff, "SYN 57K0 FM94-009" }, + { 0x004a, 0x00b5, 0x11, 0xff, "SYN 57K0 FM94-010" }, + { 0x004a, 0x00b5, 0x12, 0xff, "SYN 57K0 FM94-011" }, + { 0x004a, 0x00b5, 0x13, 0xff, "SYN 57K0 FM3297-02" }, + { 0x004a, 0x00b5, 0x14, 0xff, "SYN 57K0 FM3297-03" }, + + /* major=0x0071: VSI 55E (type 0xdb) */ + { 0x0071, 0x00db, 0x01, 0xff, "VSI 55E FM72-001" }, + { 0x0071, 0x00db, 0x02, 0xff, "VSI 55E FM72-002" }, + + /* major=0x007f: SYN 73A01 (type 0xb3) */ + { 0x007f, 0x00b3, 0x01, 0xff, "SYN 73A1" }, + { 0x007f, 0x00b3, 0x02, 0xff, "SYN 73A01 FM152-001" }, + { 0x007f, 0x00b3, 0x04, 0xff, "SYN 73A01 FM153-001" }, + + /* major=0x0000: wildcard entries (match any version) */ + { 0x0000, 0x00b5, 0x00, 0x00, "SYN 57F" }, + { 0x0000, 0x00db, 0x00, 0x00, "VSI 55E" }, +}; + +#define DEVICE_INFO_TABLE_LEN G_N_ELEMENTS (device_info_table) + +/* ================================================================ + * Identify sensor parser + * + * Cmd 0x75 response (after 2-byte status stripped): + * [zeroes:4 LE] [version:2 LE] [major:2 LE] + * + * See python-validity sensor.py identify_sensor(): + * _, minor, major = unpack('hw_version)) + return FALSE; + if (!fpi_byte_reader_get_uint16_le (&reader, &out->hw_major)) + return FALSE; + + return TRUE; +} + +/* ================================================================ + * DeviceInfo lookup + * + * Matches python-validity hw_tables.py dev_info_lookup(): + * - Exact match: major matches AND (version & version_mask) == version + * - Fuzzy match: major matches AND version_mask == 0 (wildcard) + * - Exact match preferred over fuzzy + * ================================================================ */ + +const ValidityDeviceInfo * +validity_device_info_lookup (guint16 major, + guint16 version) +{ + const ValidityDeviceInfo *fuzzy_match = NULL; + + for (gsize i = 0; i < DEVICE_INFO_TABLE_LEN; i++) + { + const ValidityDeviceInfo *entry = &device_info_table[i]; + + if (entry->major != major) + continue; + + guint8 masked_ver = entry->version & entry->version_mask; + + if (version == 0 || masked_ver == 0) + { + fuzzy_match = entry; + } + else if ((guint8) version == masked_ver) + { + return entry; + } + } + + return fuzzy_match; +} + +/* ================================================================ + * SensorTypeInfo lookup + * ================================================================ */ + +const ValiditySensorTypeInfo * +validity_sensor_type_info_lookup (guint16 sensor_type) +{ + for (gsize i = 0; i < SENSOR_TYPE_INFO_TABLE_LEN; i++) + { + if (sensor_type_info_table[i].sensor_type == sensor_type) + return &sensor_type_info_table[i]; + } + + return NULL; +} + +/* ================================================================ + * Factory bits command builder + * + * Cmd 0x6f: GET_FACTORY_BITS + * Wire format: [0x6f] [tag:2 LE] [pad:2 LE = 0] [pad:4 LE = 0] + * Total: 9 bytes + * + * See python-validity sensor.py: + * tls.cmd(unhex('6f') + pack('factory_bits, g_free); + memset (state, 0, sizeof (*state)); +} diff --git a/libfprint/drivers/validity/validity_sensor.h b/libfprint/drivers/validity/validity_sensor.h new file mode 100644 index 00000000..1d593a5a --- /dev/null +++ b/libfprint/drivers/validity/validity_sensor.h @@ -0,0 +1,126 @@ +/* + * Sensor identification and HAL table types for Validity/Synaptics VCSFW + * + * Copyright (C) 2024 libfprint contributors + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include + +/* + * SensorTypeInfo — sensor geometry and calibration parameters. + * Derived from python-validity SensorTypeInfo (generated_tables.py). + * + * Each sensor type has fixed imaging geometry (bytes per scan line, + * repeat multiplier for frame size, calibration dimensions) and an + * optional calibration lookup blob. + */ +typedef struct +{ + guint16 sensor_type; + guint16 bytes_per_line; + guint8 repeat_multiplier; + guint16 lines_per_calibration_data; + guint16 line_width; + const guint8 *calibration_blob; /* may be NULL */ + gsize calibration_blob_len; +} ValiditySensorTypeInfo; + +/* + * DeviceInfo — hardware identity to sensor type mapping. + * Derived from python-validity DeviceInfo (hw_tables.py). + * + * The identify_sensor command (0x75) returns a hardware major + version. + * DeviceInfo maps those to a sensor type (→ SensorTypeInfo) and a human + * readable name. + */ +typedef struct +{ + guint16 major; + guint16 type; /* sensor type for SensorTypeInfo lookup */ + guint8 version; + guint8 version_mask; /* 0xff = exact match, 0x00 = wildcard */ + const char *name; +} ValidityDeviceInfo; + +/* + * Sensor identification from cmd 0x75 response. + */ +typedef struct +{ + guint16 hw_major; /* hardware major (→ DeviceInfo.major) */ + guint16 hw_version; /* hardware version (→ DeviceInfo.version) */ +} ValiditySensorIdent; + +/* + * Aggregate sensor state stored in FpiDeviceValidity. + */ +typedef struct +{ + ValiditySensorIdent ident; + const ValidityDeviceInfo *device_info; + const ValiditySensorTypeInfo *type_info; + + /* Factory calibration bits (raw response from cmd 0x6f) */ + guint8 *factory_bits; + gsize factory_bits_len; +} ValiditySensorState; + +/* ---- Parsing functions ---- */ + +/* + * Parse the response from VCSFW_CMD_IDENTIFY_SENSOR (0x75). + * Response format (after 2-byte status): zeroes(4LE) | version(2LE) | major(2LE). + * Returns FALSE if the data is too short. + */ +gboolean validity_sensor_parse_identify (const guint8 *data, + gsize data_len, + ValiditySensorIdent *out); + +/* ---- HAL table lookups ---- */ + +/* + * Look up a DeviceInfo entry by hardware major and version. + * Exact match on (major, version & version_mask) is preferred; + * falls back to fuzzy match when version_mask == 0. + * Returns NULL if no match found. + */ +const ValidityDeviceInfo *validity_device_info_lookup (guint16 major, + guint16 version); + +/* + * Look up a SensorTypeInfo entry by sensor type. + * Returns NULL if the type is not in the table. + */ +const ValiditySensorTypeInfo *validity_sensor_type_info_lookup (guint16 sensor_type); + +/* ---- Command building ---- */ + +/* + * Build the command bytes for VCSFW_CMD_GET_FACTORY_BITS (0x6f). + * Format: cmd(1) | tag(2LE) | pad(2LE=0) | pad(4LE=0) = 9 bytes. + * Returns the number of bytes written, or 0 if buf_len < 9. + */ +gsize validity_sensor_build_factory_bits_cmd (guint16 tag, + guint8 *buf, + gsize buf_len); + +/* ---- Lifecycle ---- */ + +void validity_sensor_state_init (ValiditySensorState *state); +void validity_sensor_state_clear (ValiditySensorState *state); diff --git a/libfprint/drivers/validity/vcsfw_protocol.c b/libfprint/drivers/validity/vcsfw_protocol.c index ca4faaa4..8fc49947 100644 --- a/libfprint/drivers/validity/vcsfw_protocol.c +++ b/libfprint/drivers/validity/vcsfw_protocol.c @@ -22,6 +22,7 @@ #include "drivers_api.h" #include "fpi-byte-reader.h" +#include "fpi-byte-utils.h" #include "vcsfw_protocol.h" /* ---- VcsfwCmdData lifecycle ---- */ @@ -196,6 +197,171 @@ vcsfw_cmd_send (FpiDeviceValidity *self, fpi_ssm_start (ssm, cmd_ssm_done); } +/* ================================================================ + * TLS-wrapped command/response exchange + * + * Same pattern as vcsfw_cmd_send but wraps the outgoing command in + * a TLS application data record (with 0x44000000 prefix) and + * decrypts the incoming response before extracting the 2-byte + * VCSFW status. + * ================================================================ */ + +static void +tls_cmd_receive_cb (FpiUsbTransfer *transfer, + FpDevice *device, + gpointer user_data, + GError *error) +{ + FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (device); + VcsfwCmdData *cmd_data = fpi_ssm_get_data (transfer->ssm); + guint16 status; + + if (error) + { + if (cmd_data->callback) + cmd_data->callback (self, NULL, 0, 0, error); + else + fpi_ssm_mark_failed (transfer->ssm, error); + return; + } + + /* Decrypt TLS app data response */ + gsize decrypted_len; + guint8 *decrypted = validity_tls_unwrap_response ( + &self->tls, + transfer->buffer, transfer->actual_length, + &decrypted_len, &error); + + if (!decrypted) + { + if (cmd_data->callback) + cmd_data->callback (self, NULL, 0, 0, error); + else + fpi_ssm_mark_failed (transfer->ssm, error); + return; + } + + /* Decrypted data is VCSFW response: status(2) + payload */ + if (decrypted_len < 2) + { + g_free (decrypted); + error = fpi_device_error_new (FP_DEVICE_ERROR_PROTO); + if (cmd_data->callback) + cmd_data->callback (self, NULL, 0, 0, error); + else + fpi_ssm_mark_failed (transfer->ssm, error); + return; + } + + status = FP_READ_UINT16_LE (decrypted); + + fp_dbg ("VCSFW TLS response: status=0x%04x, len=%" G_GSIZE_FORMAT, + status, decrypted_len - 2); + + /* Stash for parent SSM consumption */ + self->cmd_response_status = status; + g_clear_pointer (&self->cmd_response_data, g_free); + if (decrypted_len > 2) + { + self->cmd_response_len = decrypted_len - 2; + self->cmd_response_data = g_memdup2 (decrypted + 2, + self->cmd_response_len); + } + else + { + self->cmd_response_len = 0; + self->cmd_response_data = NULL; + } + + g_free (decrypted); + + if (cmd_data->callback) + cmd_data->callback (self, + self->cmd_response_data, + self->cmd_response_len, + status, + NULL); + + fpi_ssm_mark_completed (transfer->ssm); +} + +void +vcsfw_tls_cmd_run_state (FpiSsm *ssm, + FpDevice *dev) +{ + FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev); + VcsfwCmdData *cmd_data = fpi_ssm_get_data (ssm); + FpiUsbTransfer *transfer; + + switch (fpi_ssm_get_cur_state (ssm)) + { + case VCSFW_TLS_CMD_STATE_SEND: + { + /* Wrap VCSFW command as TLS application data */ + gsize wrapped_len; + guint8 *wrapped = validity_tls_wrap_app_data (&self->tls, + cmd_data->cmd_data, + cmd_data->cmd_len, + &wrapped_len); + + /* Build USB payload: 0x44000000 prefix + TLS record */ + gsize usb_len = 4 + wrapped_len; + + fp_dbg ("VCSFW TLS send cmd 0x%02x, plaintext=%" G_GSIZE_FORMAT + ", wire=%" G_GSIZE_FORMAT, + cmd_data->cmd_data[0], cmd_data->cmd_len, usb_len); + + transfer = fpi_usb_transfer_new (dev); + transfer->short_is_error = TRUE; + fpi_usb_transfer_fill_bulk (transfer, VALIDITY_EP_CMD_OUT, usb_len); + transfer->buffer[0] = 0x44; + transfer->buffer[1] = 0x00; + transfer->buffer[2] = 0x00; + transfer->buffer[3] = 0x00; + memcpy (transfer->buffer + 4, wrapped, wrapped_len); + g_free (wrapped); + + transfer->ssm = ssm; + fpi_usb_transfer_submit (transfer, VALIDITY_USB_TIMEOUT, NULL, + fpi_ssm_usb_transfer_cb, NULL); + } + break; + + case VCSFW_TLS_CMD_STATE_RECV: + transfer = fpi_usb_transfer_new (dev); + transfer->ssm = ssm; + fpi_usb_transfer_fill_bulk (transfer, VALIDITY_EP_CMD_IN, + VALIDITY_MAX_TRANSFER_LEN); + fpi_usb_transfer_submit (transfer, VALIDITY_USB_TIMEOUT, NULL, + tls_cmd_receive_cb, NULL); + break; + } +} + +void +vcsfw_tls_cmd_send (FpiDeviceValidity *self, + FpiSsm *parent_ssm, + const guint8 *cmd, + gsize cmd_len, + VcsfwCmdCallback callback) +{ + FpiSsm *ssm; + VcsfwCmdData *cmd_data; + + cmd_data = vcsfw_cmd_data_new (cmd, cmd_len, callback); + + ssm = fpi_ssm_new (FP_DEVICE (self), vcsfw_tls_cmd_run_state, + VCSFW_TLS_CMD_STATE_NUM_STATES); + fpi_ssm_set_data (ssm, cmd_data, vcsfw_cmd_data_free); + + self->cmd_ssm = ssm; + + if (parent_ssm) + fpi_ssm_start_subsm (parent_ssm, ssm); + else + fpi_ssm_start (ssm, cmd_ssm_done); +} + /* ---- GET_VERSION (cmd 0x01) response parser ---- */ gboolean diff --git a/libfprint/drivers/validity/vcsfw_protocol.h b/libfprint/drivers/validity/vcsfw_protocol.h index f2c136df..e00d5c2a 100644 --- a/libfprint/drivers/validity/vcsfw_protocol.h +++ b/libfprint/drivers/validity/vcsfw_protocol.h @@ -85,6 +85,13 @@ typedef enum { VCSFW_CMD_STATE_NUM_STATES, } VcsfwCmdSsmState; +/* ---- TLS-wrapped command/response SSM states ---- */ +typedef enum { + VCSFW_TLS_CMD_STATE_SEND = 0, + VCSFW_TLS_CMD_STATE_RECV, + VCSFW_TLS_CMD_STATE_NUM_STATES, +} VcsfwTlsCmdSsmState; + /* ---- Context for a single command/response exchange ---- */ typedef struct { @@ -110,6 +117,15 @@ void vcsfw_cmd_send (FpiDeviceValidity *self, gsize cmd_len, VcsfwCmdCallback callback); +void vcsfw_tls_cmd_run_state (FpiSsm *ssm, + FpDevice *dev); + +void vcsfw_tls_cmd_send (FpiDeviceValidity *self, + FpiSsm *parent_ssm, + const guint8 *cmd, + gsize cmd_len, + VcsfwCmdCallback callback); + gboolean vcsfw_parse_version (const guint8 *data, gsize data_len, ValidityVersionInfo *info); diff --git a/libfprint/meson.build b/libfprint/meson.build index 2f912138..93dc28c5 100644 --- a/libfprint/meson.build +++ b/libfprint/meson.build @@ -157,7 +157,8 @@ driver_sources = { [ 'drivers/validity/validity.c', 'drivers/validity/vcsfw_protocol.c', 'drivers/validity/validity_tls.c', - 'drivers/validity/validity_fwext.c' ], + 'drivers/validity/validity_fwext.c', + 'drivers/validity/validity_sensor.c' ], } helper_sources = { diff --git a/tests/meson.build b/tests/meson.build index 551a36fd..503f2e33 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -353,6 +353,20 @@ if 'validity' in supported_drivers suite: ['unit-tests'], env: envs, ) + + # Validity sensor identification unit tests (no OpenSSL needed) + validity_sensor_test = executable('test-validity-sensor', + sources: 'test-validity-sensor.c', + dependencies: [ libfprint_private_dep ], + c_args: common_cflags, + link_with: libfprint_drivers, + install: false, + ) + test('validity-sensor', + validity_sensor_test, + suite: ['unit-tests'], + env: envs, + ) endif # Run udev rule generator with fatal warnings diff --git a/tests/test-validity-sensor.c b/tests/test-validity-sensor.c new file mode 100644 index 00000000..2a308ff7 --- /dev/null +++ b/tests/test-validity-sensor.c @@ -0,0 +1,347 @@ +/* + * Unit tests for validity sensor identification and HAL tables + * + * Copyright (C) 2024 libfprint contributors + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + */ + +#include +#include + +#include "fpi-byte-utils.h" + +#include "drivers/validity/validity_sensor.h" + +/* ================================================================ + * T4.1: test_identify_sensor_parse + * + * Verify that a valid cmd 0x75 response is parsed correctly into + * a ValiditySensorIdent (hw_major + hw_version). + * + * Wire format (after 2-byte status stripped): + * [zeroes:4 LE] [version:2 LE] [major:2 LE] + * ================================================================ */ +static void +test_identify_sensor_parse (void) +{ + ValiditySensorIdent ident; + + /* Build synthetic response: zeroes=0, version=0x13, major=0x004a */ + guint8 data[8]; + FP_WRITE_UINT32_LE (&data[0], 0); /* zeroes */ + FP_WRITE_UINT16_LE (&data[4], 0x0013); /* version */ + FP_WRITE_UINT16_LE (&data[6], 0x004a); /* major */ + + gboolean ok = validity_sensor_parse_identify (data, sizeof (data), &ident); + + g_assert_true (ok); + g_assert_cmpuint (ident.hw_major, ==, 0x004a); + g_assert_cmpuint (ident.hw_version, ==, 0x0013); +} + +/* ================================================================ + * T4.2: test_identify_sensor_parse_truncated + * + * Verify that a response shorter than 8 bytes returns FALSE. + * ================================================================ */ +static void +test_identify_sensor_parse_truncated (void) +{ + ValiditySensorIdent ident; + + guint8 data[7] = { 0 }; + + g_assert_false (validity_sensor_parse_identify (data, sizeof (data), &ident)); + /* Also test with 0 length */ + g_assert_false (validity_sensor_parse_identify (data, 0, &ident)); +} + +/* ================================================================ + * T4.3: test_device_info_lookup_exact + * + * Verify that lookup with major=0x004a, version=0x13 returns the + * correct DeviceInfo for the ThinkPad T480s sensor. + * ================================================================ */ +static void +test_device_info_lookup_exact (void) +{ + const ValidityDeviceInfo *info; + + info = validity_device_info_lookup (0x004a, 0x13); + + g_assert_nonnull (info); + g_assert_cmpuint (info->major, ==, 0x004a); + g_assert_cmpuint (info->type, ==, 0x00b5); + g_assert_cmpuint (info->version, ==, 0x13); + g_assert_cmpstr (info->name, ==, "SYN 57K0 FM3297-02"); +} + +/* ================================================================ + * T4.4: test_device_info_lookup_another + * + * Verify that lookup with major=0x0071, version=0x01 returns + * the VSI 55E entry (type 0xdb). + * ================================================================ */ +static void +test_device_info_lookup_another (void) +{ + const ValidityDeviceInfo *info; + + info = validity_device_info_lookup (0x0071, 0x01); + + g_assert_nonnull (info); + g_assert_cmpuint (info->type, ==, 0x00db); + g_assert_cmpstr (info->name, ==, "VSI 55E FM72-001"); +} + +/* ================================================================ + * T4.5: test_device_info_lookup_unknown + * + * Verify that a completely unknown major returns NULL. + * ================================================================ */ +static void +test_device_info_lookup_unknown (void) +{ + const ValidityDeviceInfo *info; + + info = validity_device_info_lookup (0xffff, 0x01); + + g_assert_null (info); +} + +/* ================================================================ + * T4.6: test_device_info_lookup_fuzzy + * + * Verify that when version_mask == 0x00, the entry matches any + * version (fuzzy match). + * ================================================================ */ +static void +test_device_info_lookup_fuzzy (void) +{ + const ValidityDeviceInfo *info; + + /* major=0x0000 entries have version_mask=0x00 → always fuzzy match. + * But major=0x0000 needs to match the lookup major. */ + info = validity_device_info_lookup (0x0000, 0x42); + + /* Should match one of the wildcard entries */ + g_assert_nonnull (info); + g_assert_cmpuint (info->major, ==, 0x0000); +} + +/* ================================================================ + * T4.7: test_sensor_type_info_lookup + * + * Verify lookup of sensor type 0x00b5 returns correct geometry. + * ================================================================ */ +static void +test_sensor_type_info_lookup (void) +{ + const ValiditySensorTypeInfo *info; + + info = validity_sensor_type_info_lookup (0x00b5); + + g_assert_nonnull (info); + g_assert_cmpuint (info->sensor_type, ==, 0x00b5); + g_assert_cmpuint (info->bytes_per_line, ==, 0x78); + g_assert_cmpuint (info->repeat_multiplier, ==, 2); + g_assert_cmpuint (info->lines_per_calibration_data, ==, 112); + g_assert_cmpuint (info->line_width, ==, 112); + g_assert_nonnull (info->calibration_blob); + g_assert_cmpuint (info->calibration_blob_len, ==, 112); +} + +/* ================================================================ + * T4.8: test_sensor_type_info_lookup_db + * + * Verify lookup of sensor type 0x00db (55E) returns correct geometry. + * ================================================================ */ +static void +test_sensor_type_info_lookup_db (void) +{ + const ValiditySensorTypeInfo *info; + + info = validity_sensor_type_info_lookup (0x00db); + + g_assert_nonnull (info); + g_assert_cmpuint (info->bytes_per_line, ==, 0x98); + g_assert_cmpuint (info->repeat_multiplier, ==, 1); + g_assert_cmpuint (info->lines_per_calibration_data, ==, 144); + g_assert_cmpuint (info->line_width, ==, 144); +} + +/* ================================================================ + * T4.9: test_sensor_type_info_lookup_unknown + * + * Verify that an unknown sensor type returns NULL. + * ================================================================ */ +static void +test_sensor_type_info_lookup_unknown (void) +{ + g_assert_null (validity_sensor_type_info_lookup (0xbeef)); +} + +/* ================================================================ + * T4.10: test_factory_bits_cmd_format + * + * Verify that the factory bits command is built correctly. + * Expected: [0x6f] [0x00 0x0e] [0x00 0x00] [0x00 0x00 0x00 0x00] + * ================================================================ */ +static void +test_factory_bits_cmd_format (void) +{ + guint8 buf[16]; + gsize len; + + len = validity_sensor_build_factory_bits_cmd (0x0e00, buf, sizeof (buf)); + + g_assert_cmpuint (len, ==, 9); + g_assert_cmpuint (buf[0], ==, 0x6f); + /* tag = 0x0e00 LE */ + g_assert_cmpuint (buf[1], ==, 0x00); + g_assert_cmpuint (buf[2], ==, 0x0e); + /* pad 2 bytes */ + g_assert_cmpuint (buf[3], ==, 0x00); + g_assert_cmpuint (buf[4], ==, 0x00); + /* pad 4 bytes */ + g_assert_cmpuint (buf[5], ==, 0x00); + g_assert_cmpuint (buf[6], ==, 0x00); + g_assert_cmpuint (buf[7], ==, 0x00); + g_assert_cmpuint (buf[8], ==, 0x00); +} + +/* ================================================================ + * T4.11: test_factory_bits_cmd_buffer_too_small + * + * Verify that a too-small buffer returns 0. + * ================================================================ */ +static void +test_factory_bits_cmd_buffer_too_small (void) +{ + guint8 buf[4]; + gsize len; + + len = validity_sensor_build_factory_bits_cmd (0x0e00, buf, sizeof (buf)); + + g_assert_cmpuint (len, ==, 0); +} + +/* ================================================================ + * T4.12: test_identify_then_lookup + * + * End-to-end: parse identify_sensor response → DeviceInfo lookup → + * SensorTypeInfo lookup. Simulates the T480s sensor (06cb:009a). + * ================================================================ */ +static void +test_identify_then_lookup (void) +{ + ValiditySensorIdent ident; + const ValidityDeviceInfo *dev_info; + const ValiditySensorTypeInfo *type_info; + + /* Simulate cmd 0x75 response for T480s: major=0x004a, version=0x13 */ + guint8 data[8]; + FP_WRITE_UINT32_LE (&data[0], 0); + FP_WRITE_UINT16_LE (&data[4], 0x0013); + FP_WRITE_UINT16_LE (&data[6], 0x004a); + + g_assert_true (validity_sensor_parse_identify (data, sizeof (data), &ident)); + g_assert_cmpuint (ident.hw_major, ==, 0x004a); + g_assert_cmpuint (ident.hw_version, ==, 0x0013); + + dev_info = validity_device_info_lookup (ident.hw_major, ident.hw_version); + g_assert_nonnull (dev_info); + g_assert_cmpuint (dev_info->type, ==, 0x00b5); + + type_info = validity_sensor_type_info_lookup (dev_info->type); + g_assert_nonnull (type_info); + g_assert_cmpuint (type_info->bytes_per_line, ==, 0x78); + g_assert_cmpuint (type_info->line_width, ==, 112); +} + +/* ================================================================ + * T4.13: test_sensor_state_lifecycle + * + * Verify that init zeros the state and clear frees allocated data. + * ================================================================ */ +static void +test_sensor_state_lifecycle (void) +{ + ValiditySensorState state; + + validity_sensor_state_init (&state); + g_assert_null (state.device_info); + g_assert_null (state.type_info); + g_assert_null (state.factory_bits); + g_assert_cmpuint (state.factory_bits_len, ==, 0); + + /* Simulate storing factory bits */ + state.factory_bits = g_memdup2 ("\x01\x02\x03", 3); + state.factory_bits_len = 3; + + validity_sensor_state_clear (&state); + g_assert_null (state.factory_bits); + g_assert_cmpuint (state.factory_bits_len, ==, 0); +} + +/* ================================================================ + * T4.14: test_calibration_blob_present + * + * Verify that the calibration blob for type 0x00b5 has expected + * first and last bytes (from python-validity generated_tables). + * ================================================================ */ +static void +test_calibration_blob_present (void) +{ + const ValiditySensorTypeInfo *info; + + info = validity_sensor_type_info_lookup (0x00b5); + g_assert_nonnull (info); + g_assert_nonnull (info->calibration_blob); + g_assert_cmpuint (info->calibration_blob_len, ==, 112); + + /* First byte: 0x9b, last byte: 0x06 */ + g_assert_cmpuint (info->calibration_blob[0], ==, 0x9b); + g_assert_cmpuint (info->calibration_blob[111], ==, 0x06); +} + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/validity/sensor/identify/parse", + test_identify_sensor_parse); + g_test_add_func ("/validity/sensor/identify/truncated", + test_identify_sensor_parse_truncated); + g_test_add_func ("/validity/sensor/devinfo/lookup_exact", + test_device_info_lookup_exact); + g_test_add_func ("/validity/sensor/devinfo/lookup_another", + test_device_info_lookup_another); + g_test_add_func ("/validity/sensor/devinfo/lookup_unknown", + test_device_info_lookup_unknown); + g_test_add_func ("/validity/sensor/devinfo/lookup_fuzzy", + test_device_info_lookup_fuzzy); + g_test_add_func ("/validity/sensor/typeinfo/lookup", + test_sensor_type_info_lookup); + g_test_add_func ("/validity/sensor/typeinfo/lookup_db", + test_sensor_type_info_lookup_db); + g_test_add_func ("/validity/sensor/typeinfo/lookup_unknown", + test_sensor_type_info_lookup_unknown); + g_test_add_func ("/validity/sensor/factory_bits/cmd_format", + test_factory_bits_cmd_format); + g_test_add_func ("/validity/sensor/factory_bits/buffer_too_small", + test_factory_bits_cmd_buffer_too_small); + g_test_add_func ("/validity/sensor/identify_then_lookup", + test_identify_then_lookup); + g_test_add_func ("/validity/sensor/state_lifecycle", + test_sensor_state_lifecycle); + g_test_add_func ("/validity/sensor/calibration_blob_present", + test_calibration_blob_present); + + return g_test_run (); +}