validity: Iteration 4 — Sensor identification and HAL tables

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
This commit is contained in:
Leonardo Francisco 2026-04-05 15:04:43 -04:00 committed by lewohart
parent e1cda8f5d8
commit 95fccfdac8
9 changed files with 1118 additions and 1 deletions

View file

@ -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);

View file

@ -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;

View file

@ -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('<LHH', rsp[:8])
* ================================================================ */
gboolean
validity_sensor_parse_identify (const guint8 *data,
gsize data_len,
ValiditySensorIdent *out)
{
FpiByteReader reader;
guint32 zeroes;
g_return_val_if_fail (data != NULL, FALSE);
g_return_val_if_fail (out != NULL, FALSE);
fpi_byte_reader_init (&reader, data, data_len);
if (!fpi_byte_reader_get_uint32_le (&reader, &zeroes))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->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('<HHL', tag, 0, 0))
* ================================================================ */
#define FACTORY_BITS_CMD_LEN 9
gsize
validity_sensor_build_factory_bits_cmd (guint16 tag,
guint8 *buf,
gsize buf_len)
{
if (buf_len < FACTORY_BITS_CMD_LEN)
return 0;
buf[0] = 0x6f; /* VCSFW_CMD_GET_FACTORY_BITS */
FP_WRITE_UINT16_LE (&buf[1], tag);
FP_WRITE_UINT16_LE (&buf[3], 0);
FP_WRITE_UINT32_LE (&buf[5], 0);
return FACTORY_BITS_CMD_LEN;
}
/* ================================================================
* Sensor state lifecycle
* ================================================================ */
void
validity_sensor_state_init (ValiditySensorState *state)
{
memset (state, 0, sizeof (*state));
}
void
validity_sensor_state_clear (ValiditySensorState *state)
{
g_clear_pointer (&state->factory_bits, g_free);
memset (state, 0, sizeof (*state));
}

View file

@ -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 <glib.h>
/*
* 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);

View file

@ -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

View file

@ -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);

View file

@ -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 = {

View file

@ -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

View file

@ -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 <glib.h>
#include <string.h>
#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 ();
}