Merge branch 'wip/validity-vcsfw-driver' into 'master'

Draft: validity: Add new driver for Validity/Synaptics VCSFW sensors

See merge request libfprint/libfprint!579
This commit is contained in:
lewohart 2026-04-22 03:57:50 +00:00
commit 2556069c47
36 changed files with 20653 additions and 9 deletions

View file

@ -319,6 +319,14 @@ usb:v05BAp000A*
ID_AUTOSUSPEND=1
ID_PERSIST=0
# Supported by libfprint driver validity
usb:v138Ap0090*
usb:v138Ap0097*
usb:v06CBp009A*
usb:v138Ap009D*
ID_AUTOSUSPEND=1
ID_PERSIST=0
# Supported by libfprint driver vcom5s
usb:v061Ap0110*
ID_AUTOSUSPEND=1
@ -388,7 +396,6 @@ usb:v06CBp0051*
usb:v06CBp0081*
usb:v06CBp0088*
usb:v06CBp008A*
usb:v06CBp009A*
usb:v06CBp009B*
usb:v06CBp00A1*
usb:v06CBp00A2*
@ -436,11 +443,8 @@ usb:v138Ap003A*
usb:v138Ap003C*
usb:v138Ap003D*
usb:v138Ap003F*
usb:v138Ap0090*
usb:v138Ap0092*
usb:v138Ap0094*
usb:v138Ap0097*
usb:v138Ap009D*
usb:v138Ap00AB*
usb:v138Ap00A6*
usb:v147Ep1002*

View file

@ -272,13 +272,14 @@ static void
fpc_cmd_ssm_done (FpiSsm *ssm, FpDevice *dev, GError *error)
{
FpiDeviceFpcMoc *self = FPI_DEVICE_FPCMOC (dev);
CommandData *data = fpi_ssm_get_data (ssm);
CommandData *data = NULL;
self->cmd_ssm = NULL;
/* Notify about the SSM failure from here instead. */
if (error)
{
fp_err ("%s error: %s ", G_STRFUNC, error->message);
data = fpi_ssm_get_data (ssm);
if (data->callback)
data->callback (self, NULL, error);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,346 @@
/*
* Validity/Synaptics VCSFW fingerprint sensor driver
*
* 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 "fpi-device.h"
#include "fpi-ssm.h"
#include "validity_capture.h"
#include "validity_data.h"
#include "validity_db.h"
#include "validity_pair.h"
#include "validity_sensor.h"
#include "validity_tls.h"
/* USB Endpoint addresses */
#define VALIDITY_EP_CMD_OUT 0x01
#define VALIDITY_EP_CMD_IN 0x81
#define VALIDITY_EP_DATA_IN 0x82
#define VALIDITY_EP_INT_IN 0x83
/* USB transfer parameters */
#define VALIDITY_USB_TIMEOUT 15000
#define VALIDITY_USB_INT_TIMEOUT 100
#define VALIDITY_MAX_TRANSFER_LEN (100 * 1024)
#define VALIDITY_USB_INT_DATA_SIZE 1024
#define VALIDITY_USB_SEND_HEADER_LEN 1
/* Number of enroll stages */
#define VALIDITY_ENROLL_STAGES 8
/* Interrupt response bits */
#define VALIDITY_INT_FINGER_DOWN 0x02
#define VALIDITY_INT_SCAN_COMPLETE 0x04
typedef enum {
VALIDITY_DEV_90 = 0, /* 138a:0090 */
VALIDITY_DEV_97, /* 138a:0097 */
VALIDITY_DEV_9A, /* 06cb:009a */
VALIDITY_DEV_9D, /* 138a:009d */
} ValidityDeviceType;
/* Firmware version info from GET_VERSION (cmd 0x01) */
typedef struct
{
guint32 build_time;
guint32 build_num;
guint8 version_major;
guint8 version_minor;
guint8 target;
guint8 product;
guint8 silicon_rev;
guint8 formal_release;
guint8 platform;
guint8 patch;
guint8 serial_number[6];
guint16 security;
guint8 iface;
guint8 device_type;
} ValidityVersionInfo;
/* Open SSM states */
typedef enum {
VALIDITY_OPEN_CLAIM_INTERFACE = 0,
VALIDITY_OPEN_SEND_INIT,
VALIDITY_OPEN_NUM_STATES,
} ValidityOpenState;
/* Close SSM states */
typedef enum {
VALIDITY_CLOSE_RELEASE_INTERFACE = 0,
VALIDITY_CLOSE_NUM_STATES,
} ValidityCloseState;
/* Calibration SSM states (runs during open, after capture_setup) */
typedef enum {
CALIB_BUILD_CMD = 0,
CALIB_SEND_CMD,
CALIB_SEND_CMD_RECV,
CALIB_READ_DATA,
CALIB_AVERAGE_FRAMES,
CALIB_PROCESS,
CALIB_LOOP_CHECK,
CALIB_BUILD_CLEAN_SLATE_CMD,
CALIB_CLEAN_SLATE_SEND,
CALIB_CLEAN_SLATE_RECV,
CALIB_CLEAN_SLATE_READ,
CALIB_SAVE_CLEAN_SLATE,
CALIB_DONE,
CALIB_NUM_STATES,
} ValidityCalibState;
/* Enrollment SSM states — matches python-validity sensor.py Sensor.enroll() */
typedef enum {
ENROLL_CLEANUP_STALE = 0, /* Close any stale enrollment session */
ENROLL_CLEANUP_STALE_RECV,
/* Pre-enrollment: delete existing user records to avoid 0x0526 */
ENROLL_PRE_GET_STORAGE,
ENROLL_PRE_GET_STORAGE_RECV,
ENROLL_PRE_DEL_USER,
ENROLL_PRE_DEL_USER_RECV,
ENROLL_START, /* create_enrollment (cmd 0x69 flag=1) */
ENROLL_START_RECV,
/* --- Per-iteration loop --- */
ENROLL_LED_ON,
ENROLL_LED_ON_RECV,
ENROLL_WAIT_FINGER_DELAY,
ENROLL_BUILD_CAPTURE,
ENROLL_CAPTURE_SEND,
ENROLL_CAPTURE_RECV,
ENROLL_WAIT_FINGER,
ENROLL_WAIT_SCAN_COMPLETE,
ENROLL_GET_PRG_STATUS,
ENROLL_GET_PRG_STATUS_RECV,
ENROLL_CAPTURE_STOP,
ENROLL_CAPTURE_STOP_RECV,
ENROLL_UPDATE_START,
ENROLL_UPDATE_START_RECV,
ENROLL_WAIT_UPDATE_START_INT, /* PY: usb.wait_int() inside enrollment_update_start */
ENROLL_DB_WRITE_ENABLE, /* PY: write_enable() before 1st enrollment_update */
ENROLL_DB_WRITE_ENABLE_RECV,
ENROLL_APPEND_IMAGE, /* 1st enrollment_update (trigger) */
ENROLL_APPEND_IMAGE_RECV,
ENROLL_CLEANUPS, /* PY: call_cleanups() after 1st enrollment_update */
ENROLL_CLEANUPS_RECV,
ENROLL_WAIT_UPDATE_INT, /* PY: usb.wait_int() between the two calls */
ENROLL_DB_WRITE_ENABLE_READ, /* PY: write_enable() before 2nd enrollment_update */
ENROLL_DB_WRITE_ENABLE_READ_RECV,
ENROLL_APPEND_IMAGE_READ, /* 2nd enrollment_update (read result) */
ENROLL_APPEND_IMAGE_READ_RECV,
ENROLL_CLEANUPS_READ, /* PY: call_cleanups() after 2nd enrollment_update */
ENROLL_CLEANUPS_READ_RECV,
ENROLL_UPDATE_END, /* PY: enrollment_update_end = cmd 0x69 flag=0 (finally) */
ENROLL_UPDATE_END_RECV,
ENROLL_LOOP_CHECK,
/* --- Post-loop: DB commit --- */
ENROLL_UPDATE_END2, /* PY: 2nd enrollment_update_end after loop */
ENROLL_UPDATE_END2_RECV,
ENROLL_GET_STORAGE,
ENROLL_GET_STORAGE_RECV,
/* If storage doesn't exist (0x04b3), create it: */
ENROLL_INIT_STORAGE_WE, /* db_write_enable for storage creation */
ENROLL_INIT_STORAGE_WE_RECV,
ENROLL_INIT_STORAGE_CREATE, /* new_record(1, 4, 3, "StgWindsor\0") */
ENROLL_INIT_STORAGE_CREATE_RECV,
ENROLL_INIT_STORAGE_CLEAN, /* call_cleanups */
ENROLL_INIT_STORAGE_CLEAN_RECV,
ENROLL_DB_WRITE_ENABLE2,
ENROLL_DB_WRITE_ENABLE2_RECV,
ENROLL_CREATE_USER,
ENROLL_CREATE_USER_RECV,
ENROLL_CREATE_USER_CLEANUPS,
ENROLL_CREATE_USER_CLEANUPS_RECV,
ENROLL_DB_WRITE_ENABLE3,
ENROLL_DB_WRITE_ENABLE3_RECV,
ENROLL_CREATE_FINGER,
ENROLL_CREATE_FINGER_RECV,
ENROLL_FINAL_CLEANUPS,
ENROLL_FINAL_CLEANUPS_RECV,
ENROLL_WAIT_FINGER_INT,
ENROLL_LED_OFF, /* PY: glow_end_scan() — LAST step per PY */
ENROLL_LED_OFF_RECV,
ENROLL_DONE,
ENROLL_NUM_STATES,
} ValidityEnrollState;
/* Verify/Identify SSM states */
typedef enum {
VERIFY_LED_ON = 0,
VERIFY_LED_ON_RECV,
VERIFY_BUILD_CAPTURE,
VERIFY_CAPTURE_SEND,
VERIFY_CAPTURE_RECV,
VERIFY_WAIT_FINGER,
VERIFY_WAIT_SCAN_COMPLETE,
VERIFY_GET_PRG_STATUS,
VERIFY_GET_PRG_STATUS_RECV,
VERIFY_CAPTURE_STOP,
VERIFY_CAPTURE_STOP_RECV,
VERIFY_MATCH_START,
VERIFY_MATCH_START_RECV,
VERIFY_WAIT_MATCH_INT,
VERIFY_GET_RESULT,
VERIFY_GET_RESULT_RECV,
VERIFY_CLEANUP,
VERIFY_CLEANUP_RECV,
VERIFY_LED_OFF,
VERIFY_LED_OFF_RECV,
VERIFY_DONE,
VERIFY_NUM_STATES,
} ValidityVerifyState;
/* List prints SSM states */
typedef enum {
LIST_GET_STORAGE = 0,
LIST_GET_STORAGE_RECV,
LIST_GET_USER,
LIST_GET_USER_RECV,
LIST_DONE,
LIST_NUM_STATES,
} ValidityListState;
/* Delete print SSM states */
typedef enum {
DELETE_GET_STORAGE = 0,
DELETE_GET_STORAGE_RECV,
DELETE_LOOKUP_USER,
DELETE_LOOKUP_USER_RECV,
DELETE_DEL_RECORD,
DELETE_DEL_RECORD_RECV,
DELETE_DONE,
DELETE_NUM_STATES,
} ValidityDeleteState;
/* Clear storage SSM states */
typedef enum {
CLEAR_GET_STORAGE = 0,
CLEAR_GET_STORAGE_RECV,
CLEAR_DEL_USER,
CLEAR_DEL_USER_RECV,
CLEAR_DONE,
CLEAR_NUM_STATES,
} ValidityClearState;
#define FPI_TYPE_DEVICE_VALIDITY (fpi_device_validity_get_type ())
G_DECLARE_FINAL_TYPE (FpiDeviceValidity, fpi_device_validity,
FPI, DEVICE_VALIDITY, FpDevice)
struct _FpiDeviceValidity
{
FpDevice parent;
ValidityDeviceType dev_type;
ValidityVersionInfo version_info;
GCancellable *interrupt_cancellable;
/* Runtime data files loaded from libfprint-validity-data package */
ValidityDataStore device_data;
ValidityDataStore common_data;
/* TLS session state */
ValidityTlsState tls;
/* Sensor identification and HAL state (post-TLS) */
ValiditySensorState sensor;
/* Capture program infrastructure and calibration state */
ValidityCaptureState capture;
/* Firmware extension status */
gboolean fwext_loaded;
/* Pairing state (for uninitialized devices) */
ValidityPairState pair_state;
/* Calibration state */
gboolean calibrated;
guint calib_iteration;
/* Enrollment state */
guint32 enroll_key;
guint8 *enroll_template;
gsize enroll_template_len;
guint enroll_stage;
guint16 enroll_user_dbid;
guint16 enroll_storage_dbid;
guint scan_incomplete_count;
/* Verify/identify mode flag: TRUE = identify, FALSE = verify */
gboolean identify_mode;
/* List prints state */
ValidityUserStorage list_storage;
guint list_user_idx;
/* Delete state */
guint16 delete_storage_dbid;
guint16 delete_finger_subtype;
guint16 delete_finger_dbid;
/* Command SSM: manages the send-cmd/recv-response cycle */
FpiSsm *cmd_ssm;
/* Parent SSM: back-pointer for non-subsm child SSMs */
FpiSsm *open_ssm;
/* Pending response data stashed for higher-level SSM consumption */
guint16 cmd_response_status;
guint8 *cmd_response_data;
gsize cmd_response_len;
/* Bulk data buffer (EP 0x82 reads during capture/calibration) */
guint8 *bulk_data;
gsize bulk_data_len;
/* Emulation mode: in-memory print storage (element-type FpPrint) */
GPtrArray *emulation_prints;
};
/* Enrollment SSM (validity_enroll.c) */
void validity_enroll (FpDevice *device);
/* Enrollment response parsing — exposed for unit testing */
#define ENROLLMENT_MAGIC_LEN 0x38
typedef struct
{
guint8 *header;
gsize header_len;
guint8 *template_data;
gsize template_len;
guint8 *tid;
gsize tid_len;
} EnrollmentUpdateResult;
void enrollment_update_result_clear (EnrollmentUpdateResult *r);
gboolean parse_enrollment_update_response (const guint8 *data,
gsize data_len,
EnrollmentUpdateResult *result);
/* Verify/Identify SSMs (validity_verify.c) */
void validity_verify (FpDevice *device);
void validity_identify (FpDevice *device);
void validity_list (FpDevice *device);
void validity_delete (FpDevice *device);
void validity_clear_storage (FpDevice *device);
/* Gallery matching helper (validity_verify.c) */
FpPrint *validity_find_gallery_match (GPtrArray *gallery,
guint16 subtype);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,396 @@
/*
* Capture program infrastructure for Validity/Synaptics VCSFW sensors
*
* Implements the capture command builder (cmd 0x02), timeslot table
* patching, factory calibration parsing, frame averaging, and
* calibration data processing.
*
* Reference: python-validity sensor.py, timeslot.py, generated_tables.py
*
* 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>
#include "validity_sensor.h"
/* ================================================================
* Capture mode passed to build_cmd_02 to select capture behavior
* Values match python-validity CaptureMode enum.
* ================================================================ */
typedef enum {
VALIDITY_CAPTURE_CALIBRATE = 1,
VALIDITY_CAPTURE_IDENTIFY = 2,
VALIDITY_CAPTURE_ENROLL = 3,
VALIDITY_CAPTURE_ENROLL_IDENTIFY = 4, /* IDENTIFY chunk (0x4e) + ENROLL recon (0x23) */
} ValidityCaptureMode;
/* ================================================================
* Capture program chunk TLV entry from the CaptureProg table.
* Format: type(2LE) | size(2LE) | data(size bytes)
* ================================================================ */
typedef struct
{
guint16 type;
guint16 size;
guint8 *data; /* owned; g_free() when done */
} ValidityCaptureChunk;
/* ================================================================
* Capture state kept in the device struct, initialized during open
* ================================================================ */
typedef struct
{
/* Capture program binary (from CaptureProg table lookup) */
const guint8 *capture_prog;
gsize capture_prog_len;
/* Geometry derived from SensorTypeInfo + CaptureProg */
guint16 lines_per_frame;
guint16 bytes_per_line;
/* Calibration parameters (derived from sensor geometry) */
guint16 key_calibration_line;
guint8 calibration_frames;
guint8 calibration_iterations;
/* Factory calibration values (from factory_bits subtag 3) */
guint8 *factory_calibration_values;
gsize factory_calibration_values_len;
/* Optional factory calibration data (from factory_bits subtag 7) */
guint8 *factory_calib_data;
gsize factory_calib_data_len;
/* Accumulated calibration data (built during calibration loop) */
guint8 *calib_data;
gsize calib_data_len;
/* Whether this is a line_update_type_1 device */
gboolean is_type1_device;
} ValidityCaptureState;
/* ================================================================
* Chunk parsing split/merge TLV-encoded capture program
* ================================================================ */
/*
* Split a capture program binary into an array of chunks.
* Returns an array of ValidityCaptureChunk (caller must free with
* validity_capture_chunks_free). Sets n_chunks to the count.
* Returns NULL on parse error.
*/
ValidityCaptureChunk *validity_capture_split_chunks (const guint8 *data,
gsize data_len,
gsize *n_chunks);
/*
* Merge an array of chunks back into a binary blob.
* Caller must g_free() the returned buffer.
* Returns NULL on error and sets out_len to 0.
*/
guint8 *validity_capture_merge_chunks (const ValidityCaptureChunk *chunks,
gsize n_chunks,
gsize *out_len);
/*
* Free an array of chunks (including each chunk's data).
*/
void validity_capture_chunks_free (ValidityCaptureChunk *chunks,
gsize n_chunks);
/* ================================================================
* Timeslot instruction decoder
*
* The timeslot table is a bytecode program for the sensor DSP.
* Each instruction is 1-3 bytes. We need to decode them to find
* specific instructions for patching.
*
* Returns: opcode in *opcode, instruction length in *insn_len,
* operands in operands[] (up to 3), count in *n_operands.
* Returns FALSE if the instruction cannot be decoded.
* ================================================================ */
gboolean validity_capture_decode_insn (const guint8 *data,
gsize data_len,
guint8 *opcode,
guint8 *insn_len,
guint32 operands[3],
guint8 *n_operands);
/* Timeslot instruction opcodes */
#define TST_OP_NOOP 0
#define TST_OP_END_OF_TABLE 1
#define TST_OP_RETURN 2
#define TST_OP_CLEAR_SO 3
#define TST_OP_END_OF_DATA 4
#define TST_OP_MACRO 5
#define TST_OP_ENABLE_RX 6
#define TST_OP_IDLE_RX 7
#define TST_OP_ENABLE_SO 8
#define TST_OP_DISABLE_SO 9
#define TST_OP_INTERRUPT 10
#define TST_OP_CALL 11
#define TST_OP_FEATURES 12
#define TST_OP_REG_WRITE 13
#define TST_OP_SAMPLE 14
#define TST_OP_SAMPLE_REPEAT 15
/*
* Find the Nth instruction with the given opcode.
* Returns the byte offset (pc) of the instruction, or -1 if not found.
*/
gssize validity_capture_find_nth_insn (const guint8 *data,
gsize data_len,
guint8 target_opcode,
guint n);
/*
* Find the Nth Register Write instruction to a specific register address.
* Returns the byte offset (pc), or -1 if not found.
*/
gssize validity_capture_find_nth_regwrite (const guint8 *data,
gsize data_len,
guint32 reg_addr,
guint n);
/* ================================================================
* Timeslot table patching
* ================================================================ */
/*
* First pass: patch the timeslot table multiplier.
* For each "Call" instruction (opcode 0x10-0x17), if repeat > 1,
* multiply it by mult and optionally increment the address.
* Modifies data in-place. Returns TRUE on success.
*/
gboolean validity_capture_patch_timeslot_table (guint8 *data,
gsize data_len,
gboolean inc_address,
guint8 mult);
/*
* Second pass: find the register write to 0x8000203C in the Call
* target subroutine and patch its value with the factory calibration
* value at key_calibration_line.
* Modifies data in-place. Returns TRUE on success; FALSE means
* no matching instruction was found (non-fatal).
*/
gboolean validity_capture_patch_timeslot_again (guint8 *data,
gsize data_len,
const guint8 *factory_calibration_values,
gsize factory_cal_len,
guint16 key_calibration_line);
/* ================================================================
* build_cmd_02 main capture command builder
* ================================================================ */
/*
* Build a capture command (cmd 0x02).
* Format: 0x02 | bytes_per_line(2LE) | req_lines(2LE) | merged_chunks
*
* The capture program is loaded from capture_state->capture_prog,
* split into chunks, patched (timeslot, line_update), and reassembled.
*
* Returns a newly-allocated buffer (caller must g_free) or NULL on error.
* Sets out_len to the buffer size.
*/
guint8 *validity_capture_build_cmd_02 (const ValidityCaptureState *capture,
const ValiditySensorTypeInfo *type_info,
ValidityCaptureMode mode,
gsize *out_len);
/* ================================================================
* Factory bits parsing
* ================================================================ */
/*
* Parse the raw factory bits response (from cmd 0x6f).
* Extracts subtag 3 (factory_calibration_values) and optionally
* subtag 7 (factory_calib_data).
*
* Response format (after 2-byte status, already stripped):
* wtf(4LE) | entries(4LE) | entry[]:
* ptr(4LE) | length(2LE) | tag(2LE) | subtag(2LE) | flags(2LE) | data(length)
*
* Returns TRUE if at least subtag 3 was found.
*/
gboolean validity_capture_parse_factory_bits (const guint8 *data,
gsize data_len,
guint8 **cal_values,
gsize *cal_values_len,
guint8 **cal_data,
gsize *cal_data_len);
/* ================================================================
* Frame averaging
* ================================================================ */
/*
* Average raw multi-frame capture data from EP 0x82.
* Deinterlaces frames and averages interleaved lines.
*
* Returns a newly-allocated buffer (one frame), or NULL on error.
* The caller must g_free() the buffer.
*/
guint8 *validity_capture_average_frames (const guint8 *raw_data,
gsize raw_len,
guint16 lines_per_frame,
guint16 bytes_per_line,
guint16 lines_per_calibration_data,
guint8 calibration_frames,
gsize *out_len);
/* ================================================================
* Calibration data processing
* ================================================================ */
/*
* Process averaged calibration frame into calibration data.
* Applies scaling and accumulates with existing calib_data.
* Updates calib_data/calib_data_len in place.
*
* If calib_data is NULL, initializes from the frame.
* If non-NULL, combines (adds signed values, clips).
*/
void validity_capture_process_calibration (guint8 **calib_data,
gsize *calib_data_len,
const guint8 *averaged_frame,
gsize frame_len,
guint16 bytes_per_line);
/*
* Build the clean slate format for flash persistence.
* Format: magic(2LE=0x5002) | inner_len(2LE) | sha256(32) | zeroes(32) |
* data_len(2LE) | data | trailing_zero(2LE=0)
*
* Returns a newly-allocated buffer or NULL on error.
*/
guint8 *validity_capture_build_clean_slate (const guint8 *averaged_frame,
gsize frame_len,
gsize *out_len);
/*
* Verify clean slate format (check magic and SHA256 hash).
* Returns TRUE if the format is valid.
*/
gboolean validity_capture_verify_clean_slate (const guint8 *data,
gsize data_len);
/* ================================================================
* Bitpack compress factory calibration values for Line Update
* ================================================================ */
/*
* Pack calibration values using minimum-bit encoding.
* Returns packed data, minimum value (v1), bit count (v0),
* and packed length.
*
* Python-validity bitpack(): find min, max delta, encode each value
* as (value - min) in minimum bits, pack as little-endian.
*
* Returns a newly-allocated buffer or NULL on error.
*/
guint8 *validity_capture_bitpack (const guint8 *values,
gsize values_len,
guint8 *out_v0,
guint8 *out_v1,
gsize *out_len);
/* ================================================================
* Finger ID mapping
* ================================================================ */
/*
* Map FpFinger enum value to VCSFW finger subtype (1-10).
* Returns 0 if the finger is not recognized.
*/
guint16 validity_finger_to_subtype (guint finger);
/*
* Map VCSFW finger subtype (1-10) to FpFinger enum value.
* Returns -1 if the subtype is not recognized.
*/
gint validity_subtype_to_finger (guint16 subtype);
/* ================================================================
* LED control commands for user feedback during capture
* ================================================================ */
/*
* Build the LED "glow start scan" command (sent via TLS app).
* Returns a static buffer and sets out_len.
*/
const guint8 *validity_capture_glow_start_cmd (gsize *out_len);
/*
* Build the LED "glow end scan" command (sent via TLS app).
* Returns a static buffer and sets out_len.
*/
const guint8 *validity_capture_glow_end_cmd (gsize *out_len);
/* ================================================================
* CaptureProg table lookup
* ================================================================ */
/*
* Look up the capture program for a given ROM version and sensor type.
* Returns a pointer to the static blob data and sets out_len.
* Returns NULL if no matching entry is found.
*/
const guint8 *validity_capture_prog_lookup (guint8 rom_major,
guint8 rom_minor,
guint16 dev_type,
gsize *out_len);
/* ================================================================
* Capture state lifecycle
* ================================================================ */
void validity_capture_state_init (ValidityCaptureState *state);
void validity_capture_state_clear (ValidityCaptureState *state);
/*
* Initialize capture state from sensor info and factory bits.
* Loads the CaptureProg, computes geometry, sets calibration params.
* Returns TRUE on success.
*/
gboolean validity_capture_state_setup (ValidityCaptureState *state,
const ValiditySensorTypeInfo *type_info,
guint16 dev_type,
guint8 rom_major,
guint8 rom_minor,
const guint8 *factory_bits,
gsize factory_bits_len);
/* Chunk type IDs used in capture programs */
#define CAPT_CHUNK_REPLY_CONFIG 0x0017
#define CAPT_CHUNK_FINGER_DETECT 0x0026
#define CAPT_CHUNK_IMAGE_RECON 0x002e
#define CAPT_CHUNK_2D_PARAMS 0x002f
#define CAPT_CHUNK_LINE_UPDATE 0x0030
#define CAPT_CHUNK_TIMESLOT_2D 0x0034
#define CAPT_CHUNK_TS_OFFSET 0x0029
#define CAPT_CHUNK_TS_FD_OFFSET 0x0035
#define CAPT_CHUNK_LINE_UPDATE_XFORM 0x0043
#define CAPT_CHUNK_INTERLEAVE 0x0044
#define CAPT_CHUNK_WTF 0x004e
/* Capture program cookie for the ACM Config chunk */
#define CAPT_CHUNK_ACM_CONFIG 0x002a
#define CAPT_CHUNK_CEM_CONFIG 0x002c

View file

@ -0,0 +1,294 @@
/*
* Runtime data file loader for Validity/Synaptics VCSFW fingerprint sensors
*
* 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 "validity_data.h"
#include "fpi-log.h"
#include "fp-device.h"
#include <glib/gstdio.h>
#include <string.h>
/* Search paths for data files */
const gchar *validity_data_search_paths[] = {
"/usr/share/libfprint/validity",
"/usr/local/share/libfprint/validity",
NULL,
};
/* HMAC-SHA256 key for integrity verification.
* This is NOT secret it is also compiled into the data package generator.
* Purpose: detect file corruption and casual tampering, not authentication.
* Plain text: "libfprint-validity-data-integrit" (32 bytes) */
static const guint8 hmac_key[32] = {
0x6c, 0x69, 0x62, 0x66, 0x70, 0x72, 0x69, 0x6e, /* "libfprin" */
0x74, 0x2d, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x69, /* "t-validi" */
0x74, 0x79, 0x2d, 0x64, 0x61, 0x74, 0x61, 0x2d, /* "ty-data-" */
0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x69, 0x74, /* "integrit" */
};
/* Per-device blob filename mapping (indexed by tag) */
static const gchar *device_blob_filenames[] = {
[VALIDITY_DATA_INIT] = "init.bin",
[VALIDITY_DATA_INIT_CLEAN_SLATE] = "init_clean_slate.bin",
[VALIDITY_DATA_RESET] = "reset.bin",
[VALIDITY_DATA_DB_WRITE_ENABLE] = "db_write_enable.bin",
};
/* Common data filename mapping (indexed by tag) */
static const struct
{
ValidityDataTag tag;
const gchar *filename;
} common_file_map[] = {
{ VALIDITY_DATA_PARTITION_SIG_STANDARD, "partition_sig_standard.bin" },
{ VALIDITY_DATA_PARTITION_SIG_0090, "partition_sig_0090.bin" },
{ VALIDITY_DATA_CA_PUBKEY, "ca_pubkey.bin" },
{ VALIDITY_DATA_TLS_PASSWORD, "tls_password.bin" },
{ VALIDITY_DATA_GWK_SIGN, "gwk_sign.bin" },
{ VALIDITY_DATA_FW_PUBKEY_X, "fw_pubkey_x.bin" },
{ VALIDITY_DATA_FW_PUBKEY_Y, "fw_pubkey_y.bin" },
};
/* ================================================================
* HMAC-SHA256 verification
* ================================================================ */
static gboolean
verify_hmac (const guint8 *data,
gsize data_len,
const guint8 *expected_hmac)
{
guint8 computed[32];
GHmac *hmac = g_hmac_new (G_CHECKSUM_SHA256, hmac_key, sizeof (hmac_key));
g_hmac_update (hmac, data, data_len);
gsize digest_len = sizeof (computed);
g_hmac_get_digest (hmac, computed, &digest_len);
g_hmac_unref (hmac);
/* Constant-time comparison */
guint diff = 0;
for (gsize i = 0; i < 32; i++)
diff |= computed[i] ^ expected_hmac[i];
return diff == 0;
}
/* ================================================================
* Public API
* ================================================================ */
void
validity_data_store_init (ValidityDataStore *store)
{
memset (store, 0, sizeof (*store));
}
void
validity_data_store_free (ValidityDataStore *store)
{
for (gsize i = 0; i < VALIDITY_DATA_NUM_TAGS; i++)
g_clear_pointer (&store->entries[i], g_bytes_unref);
}
gboolean
validity_data_load_file (ValidityDataStore *store,
ValidityDataTag tag,
const gchar *filepath,
GError **error)
{
g_autofree guint8 *contents = NULL;
gsize length = 0;
g_return_val_if_fail (store != NULL, FALSE);
g_return_val_if_fail (tag < VALIDITY_DATA_NUM_TAGS, FALSE);
g_return_val_if_fail (filepath != NULL, FALSE);
if (!g_file_get_contents (filepath, (gchar **) &contents, &length, error))
return FALSE;
if (length < VALIDITY_DATA_MIN_FILE_SIZE)
{
g_set_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_INVALID,
"Data file '%s' too short (%zu bytes, minimum %d)",
filepath, length, VALIDITY_DATA_MIN_FILE_SIZE);
return FALSE;
}
gsize data_len = length - VALIDITY_DATA_HMAC_SIZE;
const guint8 *hmac_trailer = contents + data_len;
if (!verify_hmac (contents, data_len, hmac_trailer))
{
g_set_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_INVALID,
"Data file '%s' failed HMAC verification — "
"file may be corrupt or from an incompatible version",
filepath);
return FALSE;
}
/* Store only the data portion (without HMAC trailer) */
g_clear_pointer (&store->entries[tag], g_bytes_unref);
store->entries[tag] = g_bytes_new (contents, data_len);
fp_dbg ("Loaded data file: %s (%zu bytes)", filepath, data_len);
return TRUE;
}
gboolean
validity_data_load_device (ValidityDataStore *store,
guint16 vid,
guint16 pid,
GError **error)
{
g_autofree gchar *subdir = g_strdup_printf ("%04x_%04x", vid, pid);
gboolean found_any = FALSE;
g_return_val_if_fail (store != NULL, FALSE);
for (const gchar **base = validity_data_search_paths; *base != NULL; base++)
{
g_autofree gchar *dir_path = g_build_filename (*base, subdir, NULL);
if (!g_file_test (dir_path, G_FILE_TEST_IS_DIR))
continue;
found_any = TRUE;
fp_info ("Loading device data from %s", dir_path);
for (gsize i = 0; i < G_N_ELEMENTS (device_blob_filenames); i++)
{
if (device_blob_filenames[i] == NULL)
continue;
g_autofree gchar *fpath =
g_build_filename (dir_path, device_blob_filenames[i], NULL);
if (!g_file_test (fpath, G_FILE_TEST_IS_REGULAR))
{
fp_dbg ("Optional device blob not found: %s", fpath);
continue;
}
if (!validity_data_load_file (store, (ValidityDataTag) i,
fpath, error))
return FALSE;
}
break; /* Found directory — don't search further */
}
if (!found_any)
{
g_set_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_NOT_FOUND,
"Device data files not found for %04x:%04x. "
"Install the libfprint-validity-data package. "
"Search paths: /usr/share/libfprint/validity/, "
"/usr/local/share/libfprint/validity/",
vid, pid);
return FALSE;
}
/* Verify that init.bin was loaded (it is mandatory) */
if (!store->entries[VALIDITY_DATA_INIT])
{
g_set_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_NOT_FOUND,
"Required file init.bin not found for %04x:%04x",
vid, pid);
return FALSE;
}
return TRUE;
}
gboolean
validity_data_load_common (ValidityDataStore *store,
GError **error)
{
g_return_val_if_fail (store != NULL, FALSE);
for (gsize i = 0; i < G_N_ELEMENTS (common_file_map); i++)
{
ValidityDataTag tag = common_file_map[i].tag;
const gchar *filename = common_file_map[i].filename;
gboolean found = FALSE;
for (const gchar **base = validity_data_search_paths;
*base != NULL; base++)
{
g_autofree gchar *fpath =
g_build_filename (*base, filename, NULL);
if (!g_file_test (fpath, G_FILE_TEST_IS_REGULAR))
continue;
if (!validity_data_load_file (store, tag, fpath, error))
return FALSE;
found = TRUE;
break;
}
if (!found)
{
g_set_error (error, FP_DEVICE_ERROR,
FP_DEVICE_ERROR_DATA_NOT_FOUND,
"Common data file '%s' not found. "
"Install the libfprint-validity-data package. "
"Search paths: /usr/share/libfprint/validity/, "
"/usr/local/share/libfprint/validity/",
filename);
return FALSE;
}
}
return TRUE;
}
GBytes *
validity_data_get (const ValidityDataStore *store,
ValidityDataTag tag)
{
g_return_val_if_fail (store != NULL, NULL);
g_return_val_if_fail (tag < VALIDITY_DATA_NUM_TAGS, NULL);
return store->entries[tag];
}
const guint8 *
validity_data_get_bytes (const ValidityDataStore *store,
ValidityDataTag tag,
gsize *len)
{
g_return_val_if_fail (len != NULL, NULL);
GBytes *bytes = validity_data_get (store, tag);
if (!bytes)
{
*len = 0;
return NULL;
}
return g_bytes_get_data (bytes, len);
}

View file

@ -0,0 +1,102 @@
/*
* Runtime data file loader for Validity/Synaptics VCSFW fingerprint sensors
*
* Loads per-device blob data and common pairing/TLS constants from
* external .bin files installed by the libfprint-validity-data package.
*
* File format: [raw_data: N bytes][HMAC-SHA256: 32 bytes]
* Install path: /usr/share/libfprint/validity/
*
* 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>
/* HMAC-SHA256 trailer size appended to each .bin file */
#define VALIDITY_DATA_HMAC_SIZE 32
/* Minimum file size: at least 1 byte of data + 32 bytes HMAC */
#define VALIDITY_DATA_MIN_FILE_SIZE (1 + VALIDITY_DATA_HMAC_SIZE)
/* Data file tags — identifies which blob is being loaded */
typedef enum {
/* Per-device blobs (in <vid>_<pid>/ subdirectory) */
VALIDITY_DATA_INIT = 0,
VALIDITY_DATA_INIT_CLEAN_SLATE,
VALIDITY_DATA_RESET,
VALIDITY_DATA_DB_WRITE_ENABLE,
/* Common data files (in base directory) */
VALIDITY_DATA_PARTITION_SIG_STANDARD,
VALIDITY_DATA_PARTITION_SIG_0090,
VALIDITY_DATA_CA_PUBKEY,
VALIDITY_DATA_TLS_PASSWORD,
VALIDITY_DATA_GWK_SIGN,
VALIDITY_DATA_FW_PUBKEY_X,
VALIDITY_DATA_FW_PUBKEY_Y,
VALIDITY_DATA_NUM_TAGS,
} ValidityDataTag;
/* Container for loaded data files */
typedef struct
{
GBytes *entries[VALIDITY_DATA_NUM_TAGS];
} ValidityDataStore;
/* Search paths for data files (defined in validity_data.c) */
extern const gchar *validity_data_search_paths[];
/* Initialize a data store (all entries NULL) */
void validity_data_store_init (ValidityDataStore *store);
/* Free all loaded data in a store */
void validity_data_store_free (ValidityDataStore *store);
/* Load a single .bin file, verify its HMAC, and store in the given tag slot.
* Returns TRUE on success. On failure, sets error and returns FALSE. */
gboolean validity_data_load_file (ValidityDataStore *store,
ValidityDataTag tag,
const gchar *filepath,
GError **error);
/* Load all per-device blob files for the given VID/PID.
* Searches validity_data_search_paths for <vid>_<pid>/ subdirectory.
* Returns TRUE if all required blobs found, FALSE with error otherwise. */
gboolean validity_data_load_device (ValidityDataStore *store,
guint16 vid,
guint16 pid,
GError **error);
/* Load all common data files (partition sigs, CA cert, TLS keys, etc.).
* Returns TRUE if all files found, FALSE with error otherwise. */
gboolean validity_data_load_common (ValidityDataStore *store,
GError **error);
/* Get the raw data bytes for a given tag (without HMAC trailer).
* Returns NULL if the tag has not been loaded. The returned GBytes
* is owned by the store do not unref. */
GBytes *validity_data_get (const ValidityDataStore *store,
ValidityDataTag tag);
/* Convenience: get raw data pointer and length for a tag.
* Returns NULL with *len=0 if not loaded. */
const guint8 *validity_data_get_bytes (const ValidityDataStore *store,
ValidityDataTag tag,
gsize *len);

View file

@ -0,0 +1,571 @@
/*
* Database operations for Validity/Synaptics VCSFW sensors
*
* Implements on-chip template database management: command builders,
* response parsers, identity encoding, and finger data formatting.
*
* Reference: python-validity db.py, flash.py, sensor.py
*
* 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 "validity_pack.h"
#include "fpi-byte-utils.h"
#include "validity_db.h"
#include "validity.h"
#include "validity_hal.h"
#include "vcsfw_protocol.h"
/* ================================================================
* Structure cleanup helpers
* ================================================================ */
void
validity_db_info_clear (ValidityDbInfo *info)
{
g_clear_pointer (&info->roots, g_free);
memset (info, 0, sizeof (*info));
}
void
validity_user_storage_clear (ValidityUserStorage *storage)
{
g_clear_pointer (&storage->name, g_free);
g_clear_pointer (&storage->user_dbids, g_free);
g_clear_pointer (&storage->user_val_sizes, g_free);
memset (storage, 0, sizeof (*storage));
}
void
validity_match_result_clear (ValidityMatchResult *result)
{
g_clear_pointer (&result->hash, g_free);
memset (result, 0, sizeof (*result));
}
void
validity_user_clear (ValidityUser *user)
{
g_clear_pointer (&user->identity, g_free);
g_clear_pointer (&user->fingers, g_free);
memset (user, 0, sizeof (*user));
}
void
validity_db_record_clear (ValidityDbRecord *record)
{
g_clear_pointer (&record->value, g_free);
memset (record, 0, sizeof (*record));
}
void
validity_record_children_clear (ValidityRecordChildren *children)
{
g_clear_pointer (&children->children, g_free);
memset (children, 0, sizeof (*children));
}
/* ================================================================
* Command builders
*
* Each function allocates and returns a binary command payload.
* The caller must g_free() the result.
* ================================================================ */
/* cmd 0x4B: Get user storage
* Format: 0x4B | dbid(2LE) | name_len(2LE) | name(NUL-terminated) */
guint8 *
validity_db_build_cmd_get_user_storage (const gchar *name,
gsize *out_len)
{
gsize name_len = 0;
if (name && name[0] != '\0')
name_len = strlen (name) + 1; /* include NUL terminator */
return validity_pack_new (out_len, "bhhd",
VCSFW_CMD_GET_USER_STORAGE,
0, /* dbid = 0 (lookup by name) */
(int) name_len,
(const guint8 *) name, name_len);
}
/* cmd 0x4A: Get user by dbid
* Format: 0x4A | dbid(2LE) | 0(2LE) | 0(2LE) */
guint8 *
validity_db_build_cmd_get_user (guint16 dbid,
gsize *out_len)
{
return validity_pack_new (out_len, "bhhh",
VCSFW_CMD_GET_USER, dbid, 0, 0);
}
/* cmd 0x47: New record
* Format: 0x47 | parent(2LE) | type(2LE) | storage(2LE) | data_len(2LE) | data */
guint8 *
validity_db_build_cmd_new_record (guint16 parent,
guint16 type,
guint16 storage,
const guint8 *data,
gsize data_len,
gsize *out_len)
{
return validity_pack_new (out_len, "bhhhhd",
VCSFW_CMD_NEW_RECORD,
(int) parent, (int) type,
(int) storage, (int) data_len,
data, data_len);
}
/* cmd 0x48: Delete record
* Format: 0x48 | dbid(2LE) */
guint8 *
validity_db_build_cmd_del_record (guint16 dbid,
gsize *out_len)
{
return validity_pack_new (out_len, "bh",
VCSFW_CMD_DEL_RECORD, dbid);
}
/* cmd 0x1a: Call cleanups (commit pending writes) */
guint8 *
validity_db_build_cmd_call_cleanups (gsize *out_len)
{
return validity_pack_new (out_len, "b", 0x1a);
}
/* cmd 0x69: Create enrollment / End enrollment
* Format: 0x69 | flag(4LE)
* start=TRUE: flag=1, start=FALSE: flag=0 */
guint8 *
validity_db_build_cmd_create_enrollment (gboolean start,
gsize *out_len)
{
return validity_pack_new (out_len, "bw",
VCSFW_CMD_CREATE_ENROLLMENT,
(guint32) (start ? 1 : 0));
}
/* cmd 0x6B: Enrollment update (with template data)
* Format: 0x6B | prev_data */
guint8 *
validity_db_build_cmd_enrollment_update (const guint8 *prev_data,
gsize prev_len,
gsize *out_len)
{
return validity_pack_new (out_len, "bd",
VCSFW_CMD_ENROLLMENT_UPDATE,
prev_data, prev_len);
}
/* ================================================================
* Response parsers
*
* All data parameters point PAST the 2-byte status.
* ================================================================ */
/* Parse DB info response (cmd 0x45) */
gboolean
validity_db_parse_info (const guint8 *data,
gsize data_len,
ValidityDbInfo *out)
{
FpiByteReader reader;
g_return_val_if_fail (data != NULL, FALSE);
g_return_val_if_fail (out != NULL, FALSE);
memset (out, 0, sizeof (*out));
fpi_byte_reader_init (&reader, data, data_len);
/* Header: unknown1(4LE) unknown0(4LE) total(4LE) used(4LE) free(4LE) records(2LE) nroots(2LE) = 0x18 bytes */
if (data_len < 0x18)
return FALSE;
if (!fpi_byte_reader_get_uint32_le (&reader, &out->unknown1))
return FALSE;
if (!fpi_byte_reader_get_uint32_le (&reader, &out->unknown0))
return FALSE;
if (!fpi_byte_reader_get_uint32_le (&reader, &out->total))
return FALSE;
if (!fpi_byte_reader_get_uint32_le (&reader, &out->used))
return FALSE;
if (!fpi_byte_reader_get_uint32_le (&reader, &out->free_space))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->records))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->n_roots))
return FALSE;
/* Root record IDs */
if (out->n_roots > 0)
{
out->roots = g_new0 (guint16, out->n_roots);
for (guint16 i = 0; i < out->n_roots; i++)
{
if (!fpi_byte_reader_get_uint16_le (&reader, &out->roots[i]))
{
validity_db_info_clear (out);
return FALSE;
}
}
}
return TRUE;
}
/* Parse user storage response (cmd 0x4B)
* Format: recid(2LE) usercnt(2LE) namesz(2LE) unknown(2LE)
* usrtab[usercnt * 4 bytes] name[namesz bytes] */
gboolean
validity_db_parse_user_storage (const guint8 *data,
gsize data_len,
ValidityUserStorage *out)
{
FpiByteReader reader;
guint16 name_sz, unknown;
g_return_val_if_fail (data != NULL, FALSE);
g_return_val_if_fail (out != NULL, FALSE);
memset (out, 0, sizeof (*out));
fpi_byte_reader_init (&reader, data, data_len);
/* Header: 8 bytes */
if (data_len < 8)
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->dbid))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->user_count))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &name_sz))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &unknown))
return FALSE;
/* User table: 4 bytes per entry (dbid(2LE) + value_size(2LE)) */
if (out->user_count > 0)
{
out->user_dbids = g_new0 (guint16, out->user_count);
out->user_val_sizes = g_new0 (guint16, out->user_count);
for (guint16 i = 0; i < out->user_count; i++)
{
if (!fpi_byte_reader_get_uint16_le (&reader, &out->user_dbids[i]) ||
!fpi_byte_reader_get_uint16_le (&reader, &out->user_val_sizes[i]))
{
validity_user_storage_clear (out);
return FALSE;
}
}
}
/* Name */
if (name_sz > 0)
{
const guint8 *name_data;
if (!fpi_byte_reader_get_data (&reader, name_sz, &name_data))
{
validity_user_storage_clear (out);
return FALSE;
}
out->name = g_strndup ((const gchar *) name_data, name_sz);
}
return TRUE;
}
/* Parse user response (cmd 0x4A)
* Format: recid(2LE) fingercnt(2LE) unknown(2LE) identitysz(2LE)
* fingertab[8 * fingercnt] identity[identitysz] */
gboolean
validity_db_parse_user (const guint8 *data,
gsize data_len,
ValidityUser *out)
{
FpiByteReader reader;
guint16 unknown, identity_sz;
g_return_val_if_fail (data != NULL, FALSE);
g_return_val_if_fail (out != NULL, FALSE);
memset (out, 0, sizeof (*out));
fpi_byte_reader_init (&reader, data, data_len);
/* Header: 8 bytes */
if (data_len < 8)
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->dbid))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->finger_count))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &unknown))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &identity_sz))
return FALSE;
/* Finger table: 8 bytes per entry */
if (out->finger_count > 0)
{
out->fingers = g_new0 (ValidityFingerEntry, out->finger_count);
for (guint16 i = 0; i < out->finger_count; i++)
{
if (!fpi_byte_reader_get_uint16_le (&reader, &out->fingers[i].dbid) ||
!fpi_byte_reader_get_uint16_le (&reader, &out->fingers[i].subtype) ||
!fpi_byte_reader_get_uint16_le (&reader, &out->fingers[i].storage) ||
!fpi_byte_reader_get_uint16_le (&reader, &out->fingers[i].value_size))
{
validity_user_clear (out);
return FALSE;
}
}
}
/* Identity bytes */
if (identity_sz > 0)
{
const guint8 *id_data;
if (!fpi_byte_reader_get_data (&reader, identity_sz, &id_data))
{
validity_user_clear (out);
return FALSE;
}
out->identity = g_memdup2 (id_data, identity_sz);
out->identity_len = identity_sz;
}
return TRUE;
}
/* Parse record value response (cmd 0x49)
* Format: dbid(2LE) type(2LE) storage(2LE) sz(2LE) pad(2bytes) value[sz] */
gboolean
validity_db_parse_record_value (const guint8 *data,
gsize data_len,
ValidityDbRecord *out)
{
FpiByteReader reader;
guint16 sz;
guint16 pad;
g_return_val_if_fail (data != NULL, FALSE);
g_return_val_if_fail (out != NULL, FALSE);
memset (out, 0, sizeof (*out));
fpi_byte_reader_init (&reader, data, data_len);
/* Header: 10 bytes (python-validity: unpack('<xxHHHHxx', rsp[:12])
* but our data is already past status, so: HHHHxx = 10 bytes) */
if (data_len < 10)
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->dbid))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->type))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->storage))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &sz))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &pad))
return FALSE;
/* Value data */
if (sz > 0)
{
const guint8 *val_data;
if (!fpi_byte_reader_get_data (&reader, sz, &val_data))
{
validity_db_record_clear (out);
return FALSE;
}
out->value = g_memdup2 (val_data, sz);
out->value_len = sz;
}
return TRUE;
}
/* Parse record children response (cmd 0x46)
* Format: dbid(2LE) type(2LE) storage(2LE) sz(2LE) cnt(2LE) pad(2bytes)
* children[cnt * 4 bytes: dbid(2LE) type(2LE)] */
gboolean
validity_db_parse_record_children (const guint8 *data,
gsize data_len,
ValidityRecordChildren *out)
{
FpiByteReader reader;
guint16 sz, pad;
g_return_val_if_fail (data != NULL, FALSE);
g_return_val_if_fail (out != NULL, FALSE);
memset (out, 0, sizeof (*out));
fpi_byte_reader_init (&reader, data, data_len);
/* Header: 12 bytes (python-validity: unpack('<xxHHHHHxx', rsp[:14])
* past status: HHHH H xx = 12 bytes) */
if (data_len < 12)
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->dbid))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->type))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->storage))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &sz))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &out->child_count))
return FALSE;
if (!fpi_byte_reader_get_uint16_le (&reader, &pad))
return FALSE;
/* Child entries */
if (out->child_count > 0)
{
out->children = g_new0 (ValidityRecordChild, out->child_count);
for (guint16 i = 0; i < out->child_count; i++)
{
if (!fpi_byte_reader_get_uint16_le (&reader, &out->children[i].dbid) ||
!fpi_byte_reader_get_uint16_le (&reader, &out->children[i].type))
{
validity_record_children_clear (out);
return FALSE;
}
}
}
return TRUE;
}
/* Parse new record response (cmd 0x47)
* Format: record_id(2LE) */
gboolean
validity_db_parse_new_record_id (const guint8 *data,
gsize data_len,
guint16 *out_record_id)
{
g_return_val_if_fail (data != NULL, FALSE);
g_return_val_if_fail (out_record_id != NULL, FALSE);
return validity_unpack (data, data_len, "h", out_record_id);
}
/* ================================================================
* Identity helpers
* ================================================================ */
/* Build a UUID-based identity for the sensor DB.
*
* python-validity uses Windows SIDs. For libfprint we use a UUID
* encoded as a type-3 (SID-type) identity with zero-padding to
* the Windows union minimum size of 0x4C bytes.
*
* Format: type(4LE) | len(4LE) | uuid_bytes(len) | padding */
guint8 *
validity_db_build_identity (const gchar *uuid_str,
gsize *out_len)
{
FpiByteWriter writer;
gsize uuid_len;
gsize payload_len;
gsize total_len;
g_return_val_if_fail (uuid_str != NULL, NULL);
uuid_len = strlen (uuid_str);
payload_len = 4 + 4 + uuid_len;
total_len = MAX (payload_len, VALIDITY_IDENTITY_MIN_SIZE);
fpi_byte_writer_init_with_size (&writer, total_len, FALSE);
fpi_byte_writer_put_uint32_le (&writer, VALIDITY_IDENTITY_TYPE_SID);
fpi_byte_writer_put_uint32_le (&writer, uuid_len);
fpi_byte_writer_put_data (&writer, (const guint8 *) uuid_str, uuid_len);
/* Pad to minimum identity size */
if (total_len > payload_len)
fpi_byte_writer_fill (&writer, 0, total_len - payload_len);
*out_len = fpi_byte_writer_get_pos (&writer);
return fpi_byte_writer_reset_and_get_data (&writer);
}
/* Build finger data for new_finger (from python-validity make_finger_data)
*
* Format:
* subtype(2LE) | flags=3(2LE) | tinfo_len(2LE) | 0x20(2LE)
* tag1=1(2LE) | len(2LE) | template_data
* tag2=2(2LE) | len(2LE) | tid
* padding[0x20 bytes] */
guint8 *
validity_db_build_finger_data (guint16 subtype,
const guint8 *template_data,
gsize template_len,
const guint8 *tid,
gsize tid_len,
gsize *out_len)
{
FpiByteWriter writer;
gsize template_block = 4 + template_len; /* tag(2) + len(2) + data */
gsize tid_block = 4 + tid_len;
gsize tinfo_len = template_block + tid_block;
gsize total = 8 + tinfo_len + 0x20; /* header + data + padding */
fpi_byte_writer_init_with_size (&writer, total, FALSE);
/* Header */
fpi_byte_writer_put_uint16_le (&writer, subtype);
fpi_byte_writer_put_uint16_le (&writer, 3); /* flags */
fpi_byte_writer_put_uint16_le (&writer, tinfo_len);
fpi_byte_writer_put_uint16_le (&writer, 0x20);
/* Template block */
fpi_byte_writer_put_uint16_le (&writer, 1); /* tag */
fpi_byte_writer_put_uint16_le (&writer, template_len);
if (template_len > 0)
fpi_byte_writer_put_data (&writer, template_data, template_len);
/* TID block */
fpi_byte_writer_put_uint16_le (&writer, 2); /* tag */
fpi_byte_writer_put_uint16_le (&writer, tid_len);
if (tid_len > 0)
fpi_byte_writer_put_data (&writer, tid, tid_len);
/* Padding */
fpi_byte_writer_fill (&writer, 0, 0x20);
*out_len = fpi_byte_writer_get_pos (&writer);
return fpi_byte_writer_reset_and_get_data (&writer);
}
/* ================================================================
* db_write_enable blob access
* ================================================================ */
const guint8 *
validity_db_get_write_enable_blob (FpiDeviceValidity *self, gsize *out_len)
{
return validity_data_get_bytes (&self->device_data,
VALIDITY_DATA_DB_WRITE_ENABLE,
out_len);
}

View file

@ -0,0 +1,269 @@
/*
* Database operations for Validity/Synaptics VCSFW sensors
*
* Implements on-chip template database management: listing users,
* storing/deleting fingerprint records, and db_write_enable.
*
* The sensor has a hierarchical record DB stored on flash partition 4:
* Storage User Finger Data
*
* Reference: python-validity db.py, flash.py
*
* 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>
/* ================================================================
* Database record types (from python-validity observation)
* ================================================================ */
#define VALIDITY_DB_RECORD_TYPE_STORAGE 4
#define VALIDITY_DB_RECORD_TYPE_USER 5
#define VALIDITY_DB_RECORD_TYPE_FINGER 6
#define VALIDITY_DB_RECORD_TYPE_DATA 8
/* Identity type used in record payloads */
#define VALIDITY_IDENTITY_TYPE_SID 3
/* Minimum size for encoded identity (Windows union minimum = 0x4c) */
#define VALIDITY_IDENTITY_MIN_SIZE 0x4C
/* Storage name used by the sensor's DB */
#define VALIDITY_STORAGE_NAME "StgWindsor"
/* Flash partition for calibration data */
#define VALIDITY_FLASH_CALIBRATION_PARTITION 6
/* Clean slate flash header magic */
#define VALIDITY_CLEAN_SLATE_MAGIC 0x5002
/* ================================================================
* DB Info returned by cmd 0x45
* ================================================================ */
typedef struct
{
guint32 unknown1; /* Always 1 */
guint32 unknown0; /* Always 0 */
guint32 total; /* Partition size */
guint32 used; /* Used (not deleted) */
guint32 free_space; /* Unallocated space */
guint16 records; /* Total number, including deleted */
guint16 n_roots; /* Number of root records */
guint16 *roots; /* Root record IDs (owned, g_free) */
} ValidityDbInfo;
/* ================================================================
* User Storage returned by cmd 0x4B
* Represents a named storage container that holds users.
* ================================================================ */
typedef struct
{
guint16 dbid;
guint16 user_count;
gchar *name; /* owned, g_free */
/* Per-user entries: array of {dbid, value_size} pairs */
guint16 *user_dbids;
guint16 *user_val_sizes;
} ValidityUserStorage;
/* ================================================================
* Finger record entry part of a User record
* ================================================================ */
typedef struct
{
guint16 dbid;
guint16 subtype; /* WINBIO finger constant (1-10) */
guint16 storage;
guint16 value_size;
} ValidityFingerEntry;
/* ================================================================
* User returned by cmd 0x4A
* ================================================================ */
typedef struct
{
guint16 dbid;
guint16 finger_count;
guint8 *identity; /* Raw identity bytes, owned */
gsize identity_len;
ValidityFingerEntry *fingers; /* owned array of finger_count entries */
} ValidityUser;
/* ================================================================
* DB Record returned by cmd 0x49 (get_record_value)
* ================================================================ */
typedef struct
{
guint16 dbid;
guint16 type;
guint16 storage;
guint8 *value; /* owned, g_free */
gsize value_len;
} ValidityDbRecord;
/* ================================================================
* Record child entry from cmd 0x46 (get_record_children)
* ================================================================ */
typedef struct
{
guint16 dbid;
guint16 type;
} ValidityRecordChild;
typedef struct
{
guint16 dbid;
guint16 type;
guint16 storage;
guint16 child_count;
ValidityRecordChild *children; /* owned array */
} ValidityRecordChildren;
/* ================================================================
* Match result parsed from cmd 0x60 (get_match_result) response
* ================================================================ */
typedef struct
{
gboolean matched; /* TRUE if user_dbid (tag 1) was found */
guint32 user_dbid; /* tag 1: matched user record dbid */
guint16 subtype; /* tag 3: matched finger subtype */
guint8 *hash; /* tag 4: match hash (owned, g_free) */
gsize hash_len;
} ValidityMatchResult;
void validity_match_result_clear (ValidityMatchResult *result);
gboolean validity_parse_match_result (const guint8 *data,
gsize data_len,
ValidityMatchResult *result);
/* ================================================================
* Command builders produce binary TLS command payloads
*
* All returned buffers are g_malloc'd and must be g_free'd by caller.
* ================================================================ */
/* cmd 0x4B: Get user storage by name */
guint8 *validity_db_build_cmd_get_user_storage (const gchar *name,
gsize *out_len);
/* cmd 0x4A: Get user by dbid */
guint8 *validity_db_build_cmd_get_user (guint16 dbid,
gsize *out_len);
/* cmd 0x47: New record */
guint8 *validity_db_build_cmd_new_record (guint16 parent,
guint16 type,
guint16 storage,
const guint8 *data,
gsize data_len,
gsize *out_len);
/* cmd 0x48: Delete record */
guint8 *validity_db_build_cmd_del_record (guint16 dbid,
gsize *out_len);
/* cmd 0x1a: Call cleanups (commit pending writes) */
guint8 *validity_db_build_cmd_call_cleanups (gsize *out_len);
/* ================================================================
* Response parsers parse binary TLS response payloads
*
* All parse functions take data AFTER the 2-byte status has been stripped.
* Return TRUE on success, FALSE on parse error.
* ================================================================ */
gboolean validity_db_parse_info (const guint8 *data,
gsize data_len,
ValidityDbInfo *out);
gboolean validity_db_parse_user_storage (const guint8 *data,
gsize data_len,
ValidityUserStorage *out);
gboolean validity_db_parse_user (const guint8 *data,
gsize data_len,
ValidityUser *out);
gboolean validity_db_parse_record_value (const guint8 *data,
gsize data_len,
ValidityDbRecord *out);
gboolean validity_db_parse_record_children (const guint8 *data,
gsize data_len,
ValidityRecordChildren *out);
/* cmd 0x47 response: parse new record ID */
gboolean validity_db_parse_new_record_id (const guint8 *data,
gsize data_len,
guint16 *out_record_id);
/* ================================================================
* Identity helpers
* ================================================================ */
/* Build a UUID-based identity suitable for the sensor DB.
* Returns a buffer of at least VALIDITY_IDENTITY_MIN_SIZE bytes. */
guint8 *validity_db_build_identity (const gchar *uuid_str,
gsize *out_len);
/* Build finger data payload for new_finger (format from make_finger_data) */
guint8 *validity_db_build_finger_data (guint16 subtype,
const guint8 *template_data,
gsize template_len,
const guint8 *tid,
gsize tid_len,
gsize *out_len);
/* ================================================================
* Structure cleanup helpers
* ================================================================ */
void validity_db_info_clear (ValidityDbInfo *info);
void validity_user_storage_clear (ValidityUserStorage *storage);
void validity_user_clear (ValidityUser *user);
void validity_db_record_clear (ValidityDbRecord *record);
void validity_record_children_clear (ValidityRecordChildren *children);
/* ================================================================
* Enrollment commands
* ================================================================ */
/* cmd 0x69: Create enrollment (start) / End enrollment */
guint8 *validity_db_build_cmd_create_enrollment (gboolean start,
gsize *out_len);
/* cmd 0x6B: Enrollment update (with template data) */
guint8 *validity_db_build_cmd_enrollment_update (const guint8 *prev_data,
gsize prev_len,
gsize *out_len);
/* ================================================================
* Match commands (inlined at call sites in verify/enroll SSMs)
* ================================================================ */
/* ================================================================
* db_write_enable blob access
* ================================================================ */
typedef struct _FpiDeviceValidity FpiDeviceValidity;
const guint8 *validity_db_get_write_enable_blob (FpiDeviceValidity *self,
gsize *out_len);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,697 @@
/*
* Validity/Synaptics VCSFW firmware extension upload
*
* 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-utils.h"
#include "validity.h"
#include "validity_fwext.h"
#include "validity_hal.h"
#include "vcsfw_protocol.h"
#include "fpi-byte-writer.h"
#include "validity_pack.h"
#include <gio/gio.h>
#include <string.h>
/* ---- Constants ---- */
#define FWEXT_CHUNK_SIZE 0x1000 /* 4 KB per write_flash chunk */
#define FWEXT_SIGNATURE_SIZE 256 /* RSA signature length */
#define FWEXT_HEADER_DELIMITER 0x1A /* .xpfwext header end marker */
#define FWEXT_HW_REG_WRITE_ADDR 0x8000205C
#define FWEXT_HW_REG_WRITE_VALUE 7
#define FWEXT_HW_REG_READ_ADDR 0x80002080
/* Firmware partition */
#define FWEXT_PARTITION 2
/* Reboot command: 0x05 0x02 0x00 */
#define VCSFW_CMD_REBOOT 0x05
#define VCSFW_REBOOT_SUBCMD 0x02
/* Cleanup command (call_cleanups): 0x1a */
#define VCSFW_CMD_CLEANUP 0x1A
/* ---- Firmware file search paths ---- */
static const gchar *firmware_search_paths[] = {
"/usr/share/libfprint/validity",
"/var/lib/python-validity",
"/var/run/python-validity",
"/usr/share/python-validity",
NULL,
};
/* db_write_enable blobs now live in HAL (validity_hal.c) */
/* ================================================================
* Firmware info parsing
* ================================================================ */
gboolean
validity_fwext_parse_fw_info (const guint8 *data,
gsize data_len,
guint16 status,
ValidityFwInfo *info)
{
memset (info, 0, sizeof (*info));
/* Status 0xB004 (bytes: 0xb0 0x04) means no firmware loaded.
* python-validity checks: rsp[0]==0xb0 and rsp[1]==4
* Our vcsfw_cmd_send reads status as uint16 LE from the first 2 bytes,
* so wire bytes {0xb0, 0x04} -> status = 0x04B0 in LE. */
if (status == VCSFW_STATUS_NO_FW)
{
info->loaded = FALSE;
return TRUE;
}
if (status != VCSFW_STATUS_OK)
{
fp_warn ("GET_FW_INFO unexpected status: 0x%04x", status);
info->loaded = FALSE;
return FALSE;
}
/* Response data (after 2-byte status stripped by vcsfw_cmd_send):
* major(2) + minor(2) + modcnt(2) + buildtime(4) = 10 bytes header
* + 12 bytes per module */
if (data_len < 10)
{
fp_warn ("GET_FW_INFO response too short: %zu", data_len);
info->loaded = FALSE;
return FALSE;
}
info->loaded = TRUE;
if (!validity_unpack (data, data_len, "hhhw",
&info->major, &info->minor,
&info->module_count, &info->buildtime))
{
info->loaded = FALSE;
return FALSE;
}
if (info->module_count > 32)
info->module_count = 32;
for (guint16 i = 0; i < info->module_count; i++)
{
if (!validity_unpack (data + 10 + i * 12, data_len - 10 - i * 12,
"hhhhw",
&info->modules[i].type,
&info->modules[i].subtype,
&info->modules[i].major,
&info->modules[i].minor,
&info->modules[i].size))
break;
}
return TRUE;
}
/* ================================================================
* Firmware filename mapping
* ================================================================ */
const gchar *
validity_fwext_get_firmware_name (guint16 vid,
guint16 pid)
{
if (vid == 0x138a && pid == 0x0090)
return "6_07f_Lenovo.xpfwext";
/* All other supported PIDs use the same firmware */
if ((vid == 0x138a && (pid == 0x0097 || pid == 0x009d)) ||
(vid == 0x06cb && pid == 0x009a))
return "6_07f_lenovo_mis_qm.xpfwext";
return NULL;
}
gchar *
validity_fwext_find_firmware (guint16 vid,
guint16 pid,
GError **error)
{
const gchar *filename = validity_fwext_get_firmware_name (vid, pid);
if (filename == NULL)
{
g_set_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_NOT_SUPPORTED,
"No firmware filename known for %04x:%04x", vid, pid);
return NULL;
}
for (const gchar **path = firmware_search_paths; *path != NULL; path++)
{
g_autofree gchar *full_path = g_build_filename (*path, filename, NULL);
if (g_file_test (full_path, G_FILE_TEST_IS_REGULAR))
{
fp_info ("Found firmware file: %s", full_path);
return g_steal_pointer (&full_path);
}
}
g_set_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_NOT_FOUND,
"Firmware file '%s' not found. "
"Search paths: /usr/share/libfprint/validity/, "
"/var/lib/python-validity/, /var/run/python-validity/. "
"Extract from Lenovo driver installer (nz3gf09w.exe).",
filename);
return NULL;
}
/* ================================================================
* .xpfwext file parser
* ================================================================ */
gboolean
validity_fwext_load_file (const gchar *path,
ValidityFwextFile *fwext,
GError **error)
{
g_autofree guint8 *contents = NULL;
gsize length = 0;
memset (fwext, 0, sizeof (*fwext));
if (!g_file_get_contents (path, (gchar **) &contents, &length, error))
return FALSE;
/* Find 0x1A delimiter that ends the header */
const guint8 *delim = memchr (contents, FWEXT_HEADER_DELIMITER, length);
if (delim == NULL)
{
g_set_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_INVALID,
"Firmware file has no 0x1A header delimiter");
return FALSE;
}
gsize header_len = (delim - contents) + 1; /* includes 0x1A itself */
gsize data_len = length - header_len;
if (data_len < FWEXT_SIGNATURE_SIZE + 1)
{
g_set_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_INVALID,
"Firmware file too short after header (%zu bytes)", data_len);
return FALSE;
}
/* Split: payload = data[:-256], signature = data[-256:] */
fwext->payload_len = data_len - FWEXT_SIGNATURE_SIZE;
fwext->payload = g_memdup2 (contents + header_len, fwext->payload_len);
memcpy (fwext->signature,
contents + header_len + fwext->payload_len,
FWEXT_SIGNATURE_SIZE);
fp_info ("Firmware file loaded: %zu bytes payload, %d bytes signature",
fwext->payload_len, FWEXT_SIGNATURE_SIZE);
return TRUE;
}
void
validity_fwext_file_clear (ValidityFwextFile *fwext)
{
g_clear_pointer (&fwext->payload, g_free);
fwext->payload_len = 0;
}
/* ================================================================
* Command builders
* ================================================================ */
void
validity_fwext_build_write_hw_reg32 (guint32 addr,
guint32 value,
guint8 *cmd,
gsize *cmd_len)
{
*cmd_len = validity_pack (cmd, 10, "bwwb",
VCSFW_CMD_WRITE_HW_REG32, addr, value, 4);
}
void
validity_fwext_build_read_hw_reg32 (guint32 addr,
guint8 *cmd,
gsize *cmd_len)
{
*cmd_len = validity_pack (cmd, 6, "bwb",
VCSFW_CMD_READ_HW_REG32, addr, 4);
}
gboolean
validity_fwext_parse_read_hw_reg32 (const guint8 *data,
gsize data_len,
guint32 *value)
{
return validity_unpack (data, data_len, "w", value);
}
void
validity_fwext_build_write_flash (guint8 partition,
guint32 offset,
const guint8 *data,
gsize data_len,
guint8 *cmd,
gsize *cmd_len)
{
*cmd_len = validity_pack (cmd, 13 + data_len, "bbbhwwd",
VCSFW_CMD_WRITE_FLASH, partition, 1,
(guint16) 0, offset, (guint32) data_len,
data, data_len);
}
void
validity_fwext_build_write_fw_sig (guint8 partition,
const guint8 *signature,
gsize sig_len,
guint8 *cmd,
gsize *cmd_len)
{
*cmd_len = validity_pack (cmd, 5 + sig_len, "bbxhd",
VCSFW_CMD_WRITE_FW_SIG, partition,
(guint16) sig_len, signature, sig_len);
}
void
validity_fwext_build_reboot (guint8 *cmd,
gsize *cmd_len)
{
*cmd_len = validity_pack (cmd, 3, "bbb",
VCSFW_CMD_REBOOT, VCSFW_REBOOT_SUBCMD, 0x00);
}
/* ================================================================
* db_write_enable blob lookup
* ================================================================ */
const guint8 *
validity_fwext_get_db_write_enable (FpiDeviceValidity *self,
gsize *len)
{
return validity_data_get_bytes (&self->device_data,
VALIDITY_DATA_DB_WRITE_ENABLE,
len);
}
/* ================================================================
* Upload SSM -- firmware extension upload state machine
*
* This SSM is started as a standalone child from the open sequence
* when fwext_loaded == FALSE. It uses the subsm pattern:
* SEND states call vcsfw_tls_cmd_send(self, ssm, ..., NULL) with a
* NULL callback. The child SSM auto-advances the parent to the
* RECV state on completion. RECV states read
* self->cmd_response_status and self->cmd_response_data.
*
* All commands are sent via TLS because the sensor requires
* encrypted writes to flash partition 2.
* ================================================================ */
/* SSM data for the upload state machine */
typedef struct
{
ValidityFwextFile fwext;
gsize write_offset; /* current offset into payload */
guint16 vid;
guint16 pid;
} FwextUploadData;
static void
fwext_upload_data_free (gpointer data)
{
FwextUploadData *ud = data;
validity_fwext_file_clear (&ud->fwext);
g_free (ud);
}
static void
fwext_recv_read_hw_reg (FpiSsm *ssm,
FpiDeviceValidity *self)
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
"READ_HW_REG failed: status=0x%04x",
self->cmd_response_status));
return;
}
guint32 value;
if (!validity_fwext_parse_read_hw_reg32 (self->cmd_response_data,
self->cmd_response_len,
&value))
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
"READ_HW_REG response too short"));
return;
}
if (value != 2 && value != 3)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
"Unexpected HW register value: %u "
"(expected 2 or 3)", value));
return;
}
fp_dbg ("FWEXT: HW register 0x%08x = %u (OK)", FWEXT_HW_REG_READ_ADDR, value);
fpi_ssm_next_state (ssm);
}
static void
fwext_load_file (FpiSsm *ssm,
FpiDeviceValidity *self)
{
FpDevice *dev = FP_DEVICE (self);
FwextUploadData *ud = fpi_ssm_get_data (ssm);
GError *error = NULL;
GUsbDevice *usb_dev = fpi_device_get_usb_device (dev);
guint16 vid = g_usb_device_get_vid (usb_dev);
guint16 pid = g_usb_device_get_pid (usb_dev);
ud->vid = vid;
ud->pid = pid;
g_autofree gchar *fw_path = validity_fwext_find_firmware (vid, pid, &error);
if (fw_path == NULL)
{
fpi_ssm_mark_failed (ssm, error);
return;
}
if (!validity_fwext_load_file (fw_path, &ud->fwext, &error))
{
fpi_ssm_mark_failed (ssm, error);
return;
}
ud->write_offset = 0;
fp_info ("FWEXT: Loaded firmware file, %zu bytes to write",
ud->fwext.payload_len);
fpi_ssm_next_state (ssm);
}
static void
fwext_send_db_write_enable (FpiSsm *ssm,
FpiDeviceValidity *self)
{
gsize dbe_len;
const guint8 *dbe = validity_fwext_get_db_write_enable (self, &dbe_len);
if (dbe == NULL || dbe_len == 0)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_NOT_SUPPORTED,
"No db_write_enable data for "
"this device"));
return;
}
vcsfw_tls_cmd_send (self, ssm, dbe, dbe_len, NULL);
}
static void
fwext_send_write_chunk (FpiSsm *ssm,
FpiDeviceValidity *self)
{
FwextUploadData *ud = fpi_ssm_get_data (ssm);
gsize remaining = ud->fwext.payload_len - ud->write_offset;
gsize chunk_size = MIN (remaining, FWEXT_CHUNK_SIZE);
/* cmd buffer: 13-byte header + payload */
g_autofree guint8 *cmd = g_malloc (13 + chunk_size);
gsize cmd_len;
validity_fwext_build_write_flash (FWEXT_PARTITION,
(guint32) ud->write_offset,
ud->fwext.payload + ud->write_offset,
chunk_size,
cmd, &cmd_len);
fp_dbg ("FWEXT: Writing chunk at offset 0x%zx (%zu/%zu bytes)",
ud->write_offset, ud->write_offset + chunk_size,
ud->fwext.payload_len);
ud->write_offset += chunk_size;
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
}
static void
fwext_recv_cleanup (FpiSsm *ssm,
FpiDeviceValidity *self)
{
FwextUploadData *ud = fpi_ssm_get_data (ssm);
/* Status 0x0491 means "nothing to commit" -- not an error */
if (self->cmd_response_status != VCSFW_STATUS_OK &&
self->cmd_response_status != 0x0491)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
"Cleanup cmd failed: "
"status=0x%04x",
self->cmd_response_status));
return;
}
if (ud->write_offset < ud->fwext.payload_len)
/* More chunks to write -- loop back to db_write_enable */
fpi_ssm_jump_to_state (ssm, FWEXT_SEND_DB_WRITE_ENABLE);
else
/* All chunks written -- proceed to signature */
fpi_ssm_next_state (ssm);
}
static void
fwext_send_write_signature (FpiSsm *ssm,
FpiDeviceValidity *self)
{
FwextUploadData *ud = fpi_ssm_get_data (ssm);
guint8 cmd[5 + FWEXT_SIGNATURE_SIZE];
gsize cmd_len;
validity_fwext_build_write_fw_sig (FWEXT_PARTITION,
ud->fwext.signature,
FWEXT_SIGNATURE_SIZE,
cmd, &cmd_len);
fp_info ("FWEXT: Writing firmware signature (%d bytes)",
FWEXT_SIGNATURE_SIZE);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
}
static void
fwext_recv_verify (FpiSsm *ssm,
FpiDeviceValidity *self)
{
ValidityFwInfo info;
if (!validity_fwext_parse_fw_info (self->cmd_response_data,
self->cmd_response_len,
self->cmd_response_status,
&info) ||
!info.loaded)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
"Firmware not detected after upload"));
return;
}
fp_info ("FWEXT: Upload verified -- firmware v%d.%d, %d modules",
info.major, info.minor, info.module_count);
fpi_ssm_next_state (ssm);
}
void
validity_fwext_upload_run_state (FpiSsm *ssm,
FpDevice *dev)
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev);
switch (fpi_ssm_get_cur_state (ssm))
{
case FWEXT_SEND_WRITE_HW_REG:
{
guint8 cmd[10];
gsize cmd_len;
validity_fwext_build_write_hw_reg32 (FWEXT_HW_REG_WRITE_ADDR,
FWEXT_HW_REG_WRITE_VALUE,
cmd, &cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
}
break;
case FWEXT_RECV_WRITE_HW_REG:
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
"WRITE_HW_REG failed: status=0x%04x",
self->cmd_response_status));
break;
}
fpi_ssm_next_state (ssm);
break;
case FWEXT_SEND_READ_HW_REG:
{
guint8 cmd[6];
gsize cmd_len;
validity_fwext_build_read_hw_reg32 (FWEXT_HW_REG_READ_ADDR,
cmd, &cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
}
break;
case FWEXT_RECV_READ_HW_REG:
fwext_recv_read_hw_reg (ssm, self);
break;
case FWEXT_LOAD_FILE:
fwext_load_file (ssm, self);
break;
case FWEXT_SEND_DB_WRITE_ENABLE:
fwext_send_db_write_enable (ssm, self);
break;
case FWEXT_RECV_DB_WRITE_ENABLE:
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
"db_write_enable failed: "
"status=0x%04x",
self->cmd_response_status));
break;
}
fpi_ssm_next_state (ssm);
break;
case FWEXT_SEND_WRITE_CHUNK:
fwext_send_write_chunk (ssm, self);
break;
case FWEXT_RECV_WRITE_CHUNK:
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
"WRITE_FLASH chunk failed: "
"status=0x%04x",
self->cmd_response_status));
break;
}
fpi_ssm_next_state (ssm);
break;
case FWEXT_SEND_CLEANUP:
{
guint8 cmd[] = { VCSFW_CMD_CLEANUP };
vcsfw_tls_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
case FWEXT_RECV_CLEANUP:
fwext_recv_cleanup (ssm, self);
break;
case FWEXT_SEND_WRITE_SIGNATURE:
fwext_send_write_signature (ssm, self);
break;
case FWEXT_RECV_WRITE_SIGNATURE:
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fpi_ssm_mark_failed (ssm,
fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
"WRITE_FW_SIG failed: "
"status=0x%04x",
self->cmd_response_status));
break;
}
fpi_ssm_next_state (ssm);
break;
case FWEXT_SEND_VERIFY:
{
guint8 cmd[] = { VCSFW_CMD_GET_FW_INFO, FWEXT_PARTITION };
vcsfw_tls_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
case FWEXT_RECV_VERIFY:
fwext_recv_verify (ssm, self);
break;
case FWEXT_SEND_REBOOT:
{
guint8 cmd[3];
gsize cmd_len;
validity_fwext_build_reboot (cmd, &cmd_len);
fp_info ("FWEXT: Rebooting sensor to activate new firmware");
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
}
break;
case FWEXT_RECV_REBOOT:
fp_info ("FWEXT: Reboot sent. Device will re-enumerate.");
fpi_ssm_mark_completed (ssm);
break;
}
}
/* ---- SSM factory ---- */
FpiSsm *
validity_fwext_upload_ssm_new (FpDevice *dev)
{
FpiSsm *ssm;
FwextUploadData *ud;
ssm = fpi_ssm_new (dev, validity_fwext_upload_run_state,
FWEXT_NUM_STATES);
ud = g_new0 (FwextUploadData, 1);
fpi_ssm_set_data (ssm, ud, fwext_upload_data_free);
return ssm;
}

View file

@ -0,0 +1,145 @@
/*
* Validity/Synaptics VCSFW firmware extension upload
*
* 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>
/* ---- Firmware info from GET_FW_INFO response ---- */
typedef struct
{
guint16 type;
guint16 subtype;
guint16 major;
guint16 minor;
guint32 size;
} ValidityFwModule;
typedef struct
{
gboolean loaded;
guint16 major;
guint16 minor;
guint32 buildtime;
guint16 module_count;
ValidityFwModule modules[32];
} ValidityFwInfo;
/* ---- Parsed firmware extension file ---- */
typedef struct
{
guint8 *payload; /* firmware data (without header, without signature) */
gsize payload_len;
guint8 signature[256]; /* 256-byte RSA signature */
} ValidityFwextFile;
/* ---- Firmware upload SSM states ----
*
* Each command has a SEND and RECV pair:
* - SEND: calls vcsfw_cmd_send(self, ssm, cmd, len, NULL)
* which starts a child SSM (subsm). The parent is paused.
* - RECV: entered automatically when child completes. Checks
* self->cmd_response_status and self->cmd_response_data.
* Advances or loops as needed.
*/
typedef enum {
FWEXT_SEND_WRITE_HW_REG = 0,
FWEXT_RECV_WRITE_HW_REG,
FWEXT_SEND_READ_HW_REG,
FWEXT_RECV_READ_HW_REG,
FWEXT_LOAD_FILE,
FWEXT_SEND_DB_WRITE_ENABLE,
FWEXT_RECV_DB_WRITE_ENABLE,
FWEXT_SEND_WRITE_CHUNK,
FWEXT_RECV_WRITE_CHUNK,
FWEXT_SEND_CLEANUP,
FWEXT_RECV_CLEANUP,
FWEXT_SEND_WRITE_SIGNATURE,
FWEXT_RECV_WRITE_SIGNATURE,
FWEXT_SEND_VERIFY,
FWEXT_RECV_VERIFY,
FWEXT_SEND_REBOOT,
FWEXT_RECV_REBOOT,
FWEXT_NUM_STATES,
} ValidityFwextSsmState;
/* ---- API ---- */
gboolean validity_fwext_parse_fw_info (const guint8 *data,
gsize data_len,
guint16 status,
ValidityFwInfo *info);
gboolean validity_fwext_load_file (const gchar *filename,
ValidityFwextFile *fwext,
GError **error);
void validity_fwext_file_clear (ValidityFwextFile *fwext);
const gchar *validity_fwext_get_firmware_name (guint16 vid,
guint16 pid);
gchar *validity_fwext_find_firmware (guint16 vid,
guint16 pid,
GError **error);
void validity_fwext_build_write_hw_reg32 (guint32 addr,
guint32 value,
guint8 *cmd,
gsize *cmd_len);
void validity_fwext_build_read_hw_reg32 (guint32 addr,
guint8 *cmd,
gsize *cmd_len);
gboolean validity_fwext_parse_read_hw_reg32 (const guint8 *data,
gsize data_len,
guint32 *value);
void validity_fwext_build_write_flash (guint8 partition,
guint32 offset,
const guint8 *data,
gsize data_len,
guint8 *cmd,
gsize *cmd_len);
void validity_fwext_build_write_fw_sig (guint8 partition,
const guint8 *signature,
gsize sig_len,
guint8 *cmd,
gsize *cmd_len);
void validity_fwext_build_reboot (guint8 *cmd,
gsize *cmd_len);
typedef struct _FpiDeviceValidity FpiDeviceValidity;
const guint8 *validity_fwext_get_db_write_enable (FpiDeviceValidity *self,
gsize *len);
/* SSM entry point for upload state machine */
void validity_fwext_upload_run_state (FpiSsm *ssm,
FpDevice *dev);
/* Create the fwext upload SSM with data attached.
* Caller starts it via fpi_ssm_start(). */
FpiSsm *validity_fwext_upload_ssm_new (FpDevice *dev);

View file

@ -0,0 +1,122 @@
/*
* Hardware Abstraction Layer for Validity/Synaptics VCSFW fingerprint sensors
*
* Per-device blob data, flash partition layouts, and pairing constants.
*
* 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 "validity_hal.h"
#include "fpi-log.h"
/* ================================================================
* Flash partition layouts
*
* Standard layout: used by PID 0097, 009a, 009d
* Partition 4 (template DB) = 0x80000 bytes
*
* PID 0090 layout: smaller DB partition
* Partition 4 (template DB) = 0x30000 bytes
* ================================================================ */
/* Standard partition table (0097, 009a, 009d) */
static const ValidityPartition flash_partitions_standard[] = {
{ .id = 1, .type = 4, .access_lvl = 7, .offset = 0x00001000, .size = 0x00001000 }, /* cert store */
{ .id = 2, .type = 1, .access_lvl = 2, .offset = 0x00002000, .size = 0x0003e000 }, /* xpfwext */
{ .id = 5, .type = 5, .access_lvl = 3, .offset = 0x00040000, .size = 0x00008000 }, /* reserved */
{ .id = 6, .type = 6, .access_lvl = 3, .offset = 0x00048000, .size = 0x00008000 }, /* calibration */
{ .id = 4, .type = 3, .access_lvl = 5, .offset = 0x00050000, .size = 0x00080000 }, /* template DB */
};
/* PID 0090 partition table (smaller DB) */
static const ValidityPartition flash_partitions_0090[] = {
{ .id = 1, .type = 4, .access_lvl = 7, .offset = 0x00001000, .size = 0x00001000 }, /* cert store */
{ .id = 2, .type = 1, .access_lvl = 2, .offset = 0x00002000, .size = 0x0003e000 }, /* xpfwext */
{ .id = 5, .type = 5, .access_lvl = 3, .offset = 0x00040000, .size = 0x00008000 }, /* reserved */
{ .id = 6, .type = 6, .access_lvl = 3, .offset = 0x00048000, .size = 0x00008000 }, /* calibration */
{ .id = 4, .type = 3, .access_lvl = 5, .offset = 0x00050000, .size = 0x00030000 }, /* template DB */
};
/* Layout descriptors */
static const ValidityFlashLayout flash_layout_standard = {
.partitions = flash_partitions_standard,
.num_partitions = G_N_ELEMENTS (flash_partitions_standard),
};
static const ValidityFlashLayout flash_layout_0090 = {
.partitions = flash_partitions_0090,
.num_partitions = G_N_ELEMENTS (flash_partitions_0090),
};
/* ================================================================
* Per-device descriptors
*
* Blob data (init, reset, db_write_enable) has been moved to external
* .bin files loaded at runtime by validity_data.c. Only VID/PID and
* flash layout remain here.
* ================================================================ */
static const ValidityDeviceDesc device_table[] = {
[VALIDITY_HAL_DEV_90] = {
.vid = 0x138a,
.pid = 0x0090,
.flash_layout = &flash_layout_0090,
},
[VALIDITY_HAL_DEV_97] = {
.vid = 0x138a,
.pid = 0x0097,
.flash_layout = &flash_layout_standard,
},
[VALIDITY_HAL_DEV_9A] = {
.vid = 0x06cb,
.pid = 0x009a,
.flash_layout = &flash_layout_standard,
},
[VALIDITY_HAL_DEV_9D] = {
.vid = 0x138a,
.pid = 0x009d,
.flash_layout = &flash_layout_standard,
},
};
/* ================================================================
* Lookup functions
* ================================================================ */
const ValidityDeviceDesc *
validity_hal_device_lookup (guint dev_type)
{
if (dev_type >= G_N_ELEMENTS (device_table))
return NULL;
return &device_table[dev_type];
}
const ValidityDeviceDesc *
validity_hal_device_lookup_by_pid (guint16 vid, guint16 pid)
{
for (gsize i = 0; i < G_N_ELEMENTS (device_table); i++)
if (device_table[i].vid == vid && device_table[i].pid == pid)
return &device_table[i];
return NULL;
}

View file

@ -0,0 +1,81 @@
/*
* Hardware Abstraction Layer for Validity/Synaptics VCSFW fingerprint sensors
*
* Per-device blob data, flash partition layouts, and pairing constants.
*
* 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>
/* Forward declaration from validity.h */
typedef enum {
VALIDITY_HAL_DEV_90 = 0,
VALIDITY_HAL_DEV_97,
VALIDITY_HAL_DEV_9A,
VALIDITY_HAL_DEV_9D,
} ValidityHalDeviceType;
/* Flash partition entry — matches python-validity PartitionInfo:
* id, type, access_lvl, offset, size */
typedef struct
{
guint8 id;
guint8 type;
guint16 access_lvl;
guint32 offset;
guint32 size;
} ValidityPartition;
/* Per-device flash layout with partition table.
* The RSA partition signature is now loaded at runtime from external
* data files (validity_data.c) instead of being compiled in. */
typedef struct
{
const ValidityPartition *partitions;
gsize num_partitions;
} ValidityFlashLayout;
/* Per-device flash layout descriptor.
* Blob data has been moved to external .bin files loaded at runtime
* by validity_data.c. This struct retains only hardware identity
* and flash partition layout. */
typedef struct
{
guint16 vid;
guint16 pid;
/* Flash partition layout for this device variant */
const ValidityFlashLayout *flash_layout;
} ValidityDeviceDesc;
/* Number of flash partition entries in the standard layout */
#define VALIDITY_FLASH_NUM_PARTITIONS 5
/* Partition signature size (RSA-2048) */
#define VALIDITY_PARTITION_SIG_SIZE 256
/* Look up device descriptor by ValidityDeviceType enum.
* Returns NULL if dev_type is out of range. */
const ValidityDeviceDesc *validity_hal_device_lookup (guint dev_type);
/* Look up device descriptor by USB VID/PID.
* Returns NULL if no matching entry. */
const ValidityDeviceDesc *validity_hal_device_lookup_by_pid (guint16 vid,
guint16 pid);

View file

@ -0,0 +1,252 @@
/*
* Validity VCSFW driver pack/unpack utilities
*
* Varargs helpers for packing bytes into buffers and unpacking bytes from
* buffers. Built on top of FpiByteWriter / FpiByteReader.
*
* Format codes (same for pack and unpack):
* 'b' uint8
* 'h' uint16 little-endian
* 'H' uint16 big-endian
* 'w' uint32 little-endian
* 'W' uint32 big-endian
* 't' uint24 big-endian (TLS lengths)
* 'x' 1 pad / skip byte
* 'd' data blob (pack: const guint8 *, gsize)
* (unpack: const guint8 **, gsize pointer set to internal)
*
* Pack into caller-provided buffer returns bytes written:
* gsize n = validity_pack (buf, sizeof buf, "bwwb", cmd, addr, val, len);
*
* Pack into newly allocated buffer returns g_malloc'd pointer:
* guint8 *p = validity_pack_new (&out_len, "bhd", cmd, id, data, data_len);
*
* Unpack from buffer returns TRUE on success:
* gboolean ok = validity_unpack (data, len, "hhxxh", &a, &b, &c);
*/
#pragma once
#include "fpi-byte-writer.h" /* includes fpi-byte-reader.h */
#include <stdarg.h>
/* ---- internal: write one format char ---- */
static inline gboolean
validity_pack_one (FpiByteWriter *w,
char code,
va_list *ap)
{
switch (code)
{
case 'b':
return fpi_byte_writer_put_uint8 (w, (guint8) va_arg (*ap, int));
case 'h':
return fpi_byte_writer_put_uint16_le (w, (guint16) va_arg (*ap, int));
case 'H':
return fpi_byte_writer_put_uint16_be (w, (guint16) va_arg (*ap, int));
case 'w':
return fpi_byte_writer_put_uint32_le (w, va_arg (*ap, guint32));
case 'W':
return fpi_byte_writer_put_uint32_be (w, va_arg (*ap, guint32));
case 't':
return fpi_byte_writer_put_uint24_be (w, va_arg (*ap, guint32));
case 'x':
return fpi_byte_writer_put_uint8 (w, 0);
case 'd':
{
const guint8 *d = va_arg (*ap, const guint8 *);
gsize len = va_arg (*ap, gsize);
if (len == 0)
return TRUE;
return fpi_byte_writer_put_data (w, d, len);
}
default:
return FALSE;
}
}
/* ---- internal: read one format char ---- */
static inline gboolean
validity_unpack_one (FpiByteReader *r,
char code,
va_list *ap)
{
switch (code)
{
case 'b':
return fpi_byte_reader_get_uint8 (r, va_arg (*ap, guint8 *));
case 'h':
return fpi_byte_reader_get_uint16_le (r, va_arg (*ap, guint16 *));
case 'H':
return fpi_byte_reader_get_uint16_be (r, va_arg (*ap, guint16 *));
case 'w':
return fpi_byte_reader_get_uint32_le (r, va_arg (*ap, guint32 *));
case 'W':
return fpi_byte_reader_get_uint32_be (r, va_arg (*ap, guint32 *));
case 't':
return fpi_byte_reader_get_uint24_be (r, va_arg (*ap, guint32 *));
case 'x':
return fpi_byte_reader_skip (r, 1);
case 'd':
{
const guint8 **out = va_arg (*ap, const guint8 * *);
gsize len = va_arg (*ap, gsize);
return fpi_byte_reader_get_data (r, len, out);
}
default:
return FALSE;
}
}
/**
* validity_pack:
* @buf: destination buffer (caller-provided)
* @buf_size: size of @buf in bytes
* @fmt: format string (see header comment)
* @...: values matching each format code
*
* Packs fields into @buf according to @fmt.
*
* Returns: number of bytes written.
*/
G_GNUC_UNUSED static gsize
validity_pack (guint8 *buf,
gsize buf_size,
const char *fmt,
...)
{
FpiByteWriter w;
va_list ap;
fpi_byte_writer_init_with_data (&w, buf, buf_size, FALSE);
va_start (ap, fmt);
for (const char *p = fmt; *p; p++)
validity_pack_one (&w, *p, &ap);
va_end (ap);
return fpi_byte_writer_get_pos (&w);
}
/**
* validity_pack_new:
* @out_len: (out): set to the number of bytes written
* @fmt: format string (see header comment)
* @...: values matching each format code
*
* Packs fields into a newly allocated buffer.
*
* Returns: (transfer full): a g_malloc'd buffer. Free with g_free().
*/
G_GNUC_UNUSED static guint8 *
validity_pack_new (gsize *out_len,
const char *fmt,
...)
{
FpiByteWriter w;
va_list ap;
/* Compute total size from format so we allocate exactly once. */
gsize size = 0;
va_start (ap, fmt);
for (const char *p = fmt; *p; p++)
{
switch (*p)
{
case 'b':
size += 1;
(void) va_arg (ap, int);
break;
case 'x':
size += 1;
break;
case 'h':
case 'H':
size += 2;
(void) va_arg (ap, int);
break;
case 't':
size += 3;
(void) va_arg (ap, guint32);
break;
case 'w':
case 'W':
size += 4;
(void) va_arg (ap, guint32);
break;
case 'd':
(void) va_arg (ap, const guint8 *);
size += va_arg (ap, gsize);
break;
default:
break;
}
}
va_end (ap);
fpi_byte_writer_init_with_size (&w, size, FALSE);
va_start (ap, fmt);
for (const char *p = fmt; *p; p++)
validity_pack_one (&w, *p, &ap);
va_end (ap);
*out_len = fpi_byte_writer_get_pos (&w);
return fpi_byte_writer_reset_and_get_data (&w);
}
/**
* validity_unpack:
* @data: source buffer
* @data_len: length of @data
* @fmt: format string (see header comment)
* @...: pointers matching each format code
*
* Unpacks fields from @data according to @fmt. Stops on the first
* bounds error.
*
* Returns: %TRUE if every field was read, %FALSE on short data.
*/
G_GNUC_UNUSED static gboolean
validity_unpack (const guint8 *data,
gsize data_len,
const char *fmt,
...)
{
FpiByteReader r;
va_list ap;
fpi_byte_reader_init (&r, data, data_len);
va_start (ap, fmt);
for (const char *p = fmt; *p; p++)
{
if (!validity_unpack_one (&r, *p, &ap))
{
va_end (ap);
return FALSE;
}
}
va_end (ap);
return TRUE;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,331 @@
/*
* Device pairing for Validity/Synaptics VCSFW fingerprint sensors
*
* Handles pairing of uninitialized devices: flash partitioning,
* ECDH key exchange, certificate creation, and TLS flash persistence.
*
* 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>
#include <openssl/evp.h>
#include "validity_hal.h"
/* Forward declaration */
typedef struct _FpiDeviceValidity FpiDeviceValidity;
typedef struct _FpiSsm FpiSsm;
/* Flash IC parameters — returned by CMD 0x3e (GET_FLASH_INFO) */
typedef struct
{
guint32 size;
guint32 sector_size;
guint8 sector_erase_cmd;
} ValidityFlashIcParams;
/* Pairing state kept across SSM states */
typedef struct
{
/* Flash IC params from CMD 0x3e */
ValidityFlashIcParams flash_ic;
guint16 num_partitions; /* 0 = needs pairing */
/* ECDH client key pair (generated during pairing) */
EVP_PKEY *client_key;
/* Server certificate returned by partition_flash (CMD 0x4f) */
guint8 *server_cert;
gsize server_cert_len;
/* ECDH blob from CMD 0x50 (server's DH params) */
guint8 *ecdh_blob;
gsize ecdh_blob_len;
/* Encrypted private key blob (client → flash) */
guint8 *priv_blob;
gsize priv_blob_len;
/* Device descriptor for this PID */
const ValidityDeviceDesc *dev_desc;
/* Flash erase progress counter */
guint erase_step;
/* Set TRUE when reboot command has been sent (normal pairing or factory reset) */
gboolean reboot_pending;
} ValidityPairState;
/* Partition entry serialized format: 12 bytes data + 4 zero + 32 SHA-256 = 48 */
#define VALIDITY_PARTITION_ENTRY_SIZE 48
/* Client certificate size */
#define VALIDITY_CLIENT_CERT_SIZE 444
/* CMD 0x4f header IDs */
#define VALIDITY_HDR_FLASH_IC 0
#define VALIDITY_HDR_PARTITION_TABLE 1
#define VALIDITY_HDR_CA_CERT 3
#define VALIDITY_HDR_CLIENT_CERT 5
/* Encrypted private key format: 0x02 prefix + IV(16) + ciphertext(112) + HMAC(32) = 161 */
#define VALIDITY_ENCRYPTED_KEY_PREFIX 0x02
#define VALIDITY_ENCRYPTED_KEY_IV_SIZE 16
#define VALIDITY_EC_COORD_SIZE 32
/* Flash partition IDs for erase during pairing */
static const guint8 pair_erase_partition_ids[] = { 1, 2, 5, 6, 4 };
#define VALIDITY_PAIR_NUM_ERASE_STEPS G_N_ELEMENTS (pair_erase_partition_ids)
/* ---- Helper functions (testable independently) ---- */
/**
* validity_pair_serialize_partition:
* @part: partition entry to serialize
* @out: output buffer (must be at least VALIDITY_PARTITION_ENTRY_SIZE bytes)
*
* Serialize a partition entry to binary format:
* [id:1][type:1][access_lvl:2LE][offset:4LE][size:4LE][zeros:4][SHA256:32]
*/
void validity_pair_serialize_partition (const ValidityPartition *part,
guint8 *out);
/**
* validity_pair_build_partition_flash_cmd:
* @flash_ic: flash IC parameters from CMD 0x3e
* @layout: flash layout for this device
* @client_public_x: ECDH client public key X coordinate (32 bytes, little-endian)
* @client_public_y: ECDH client public key Y coordinate (32 bytes, little-endian)
* @out_len: output command length
*
* Build the CMD 0x4f (PARTITION_FLASH) payload:
* [4f 0000 0000]
* [hdr0: flash IC params]
* [hdr1: partition table + RSA signature]
* [hdr5: client certificate (444 bytes)]
* [hdr3: CA certificate]
*
* Returns: newly allocated command buffer, free with g_free().
*/
guint8 *validity_pair_build_partition_flash_cmd (const ValidityFlashIcParams *flash_ic,
const ValidityFlashLayout *layout,
const guint8 *partition_sig,
gsize partition_sig_len,
const guint8 *client_public_x,
const guint8 *client_public_y,
const guint8 *password,
gsize password_len,
const guint8 *ca_cert,
gsize ca_cert_len,
gsize *out_len);
/**
* validity_pair_make_cert:
* @client_public_x: ECDH client public key X (32 bytes, little-endian)
* @client_public_y: ECDH client public key Y (32 bytes, little-endian)
* @out_len: set to VALIDITY_CLIENT_CERT_SIZE on success
*
* Build a 444-byte client certificate with ECDSA signature.
* The HS key is derived from the hardcoded password.
*
* Returns: newly allocated cert buffer, or NULL on error. Free with g_free().
*/
guint8 *validity_pair_make_cert (const guint8 *client_public_x,
const guint8 *client_public_y,
const guint8 *password,
gsize password_len,
gsize *out_len);
/**
* validity_pair_encrypt_key:
* @client_private: ECDH client private key scalar (32 bytes, little-endian)
* @client_public_x: ECDH client public key X (32 bytes, little-endian)
* @client_public_y: ECDH client public key Y (32 bytes, little-endian)
* @psk_encryption_key: PSK encryption key (32 bytes)
* @psk_validation_key: PSK validation key (32 bytes)
* @out_len: output blob length
*
* Encrypt the ECDH private key for flash storage:
* plaintext = x(32) + y(32) + d(32) + PKCS7 padding
* ciphertext = AES-256-CBC(psk_encryption_key, random_iv, plaintext)
* blob = 0x02 + iv(16) + ciphertext + HMAC-SHA256(psk_validation_key, iv+ciphertext)
*
* Returns: newly allocated blob, or NULL on error. Free with g_free().
*/
guint8 *validity_pair_encrypt_key (const guint8 *client_private,
const guint8 *client_public_x,
const guint8 *client_public_y,
const guint8 *psk_encryption_key,
const guint8 *psk_validation_key,
gsize *out_len);
/**
* validity_pair_parse_flash_info:
* @data: response payload from CMD 0x3e (after 2-byte status)
* @data_len: length of payload
* @ic_out: output flash IC params
* @num_partitions_out: output number of existing partitions
*
* Parse GET_FLASH_INFO (CMD 0x3e) response.
* If num_partitions_out is 0, device needs pairing.
*
* Returns: TRUE on success, FALSE on parse error.
*/
gboolean validity_pair_parse_flash_info (const guint8 *data,
gsize data_len,
ValidityFlashIcParams *ic_out,
guint16 *num_partitions_out);
/**
* validity_pair_state_init:
* @state: pairing state to initialize
*
* Zero-initialize the pairing state.
*/
void validity_pair_state_init (ValidityPairState *state);
/**
* validity_pair_state_free:
* @state: pairing state to free
*
* Free any allocated resources in the pairing state.
*/
void validity_pair_state_free (ValidityPairState *state);
/* ================================================================
* Pairing SSM
*
* The pairing SSM runs when an uninitialized device is detected
* (CMD 0x3e returns 0 partitions). It formats the flash, exchanges
* ECDH keys, establishes a TLS session, persists the TLS keys to
* flash, and reboots the device.
*
* Phase 1 (pre-TLS, raw USB):
* - GET_FLASH_INFO check if pairing needed
* - SEND_RESET_BLOB reset device state
* - GENERATE_KEYS ECDH key generation
* - PARTITION_FLASH format flash + write certs (CMD 0x4f)
* - RECV_PARTITION parse server cert from response
* - CMD50_SEND request ECDH server params
* - CMD50_RECV parse ECDH blob + encrypt private key
* - CLEANUPS_SEND call cleanups (0x1a) after CMD 0x50
* - CLEANUPS_RECV receive cleanups response
*
* Phase 2 (TLS):
* - TLS_HANDSHAKE establish TLS session
* - ERASE_DBE_SEND send db_write_enable before erase
* - ERASE_DBE_RECV receive dbe response
* - ERASE_SEND erase partition (CMD 0x3f)
* - ERASE_RECV receive erase response
* - ERASE_CLEAN_SEND call cleanups after erase
* - ERASE_CLEAN_RECV receive cleanups response
* - ERASE_LOOP loop over 5 partitions
* - WRITE_DBE_SEND send db_write_enable before write
* - WRITE_DBE_RECV receive dbe response
* - WRITE_FLASH_SEND write TLS flash (CMD 0x41)
* - WRITE_FLASH_RECV receive write response
* - WRITE_CLEAN_SEND call cleanups after write
* - WRITE_CLEAN_RECV receive cleanups response
* - REBOOT_SEND reboot device (0x05 0x02 0x00)
* - REBOOT_RECV receive reboot response
* - DONE pairing complete
* ================================================================ */
typedef enum {
PAIR_GET_FLASH_INFO = 0,
PAIR_GET_FLASH_INFO_RECV,
PAIR_CHECK_NEEDED,
PAIR_VERIFY_TLS_SEND,
PAIR_VERIFY_TLS_RECV,
PAIR_SEND_RESET_BLOB,
PAIR_SEND_RESET_BLOB_RECV,
PAIR_GENERATE_KEYS,
PAIR_PARTITION_FLASH_SEND,
PAIR_PARTITION_FLASH_RECV,
PAIR_FACTORY_RESET_SEND,
PAIR_FACTORY_RESET_RECV,
PAIR_CMD50_SEND,
PAIR_CMD50_RECV,
PAIR_CMD50_PROCESS,
PAIR_CLEANUPS_SEND,
PAIR_CLEANUPS_RECV,
PAIR_TLS_HANDSHAKE,
PAIR_ERASE_DBE_SEND,
PAIR_ERASE_DBE_RECV,
PAIR_ERASE_SEND,
PAIR_ERASE_RECV,
PAIR_ERASE_CLEAN_SEND,
PAIR_ERASE_CLEAN_RECV,
PAIR_ERASE_LOOP,
PAIR_WRITE_DBE_SEND,
PAIR_WRITE_DBE_RECV,
PAIR_WRITE_FLASH_SEND,
PAIR_WRITE_FLASH_RECV,
PAIR_WRITE_CLEAN_SEND,
PAIR_WRITE_CLEAN_RECV,
PAIR_REBOOT_SEND,
PAIR_REBOOT_RECV,
PAIR_DONE,
PAIR_NUM_STATES,
} ValidityPairSsmState;
/* CMD 0x1a (cleanups/commit) */
#define VCSFW_CMD_CLEANUPS 0x1a
/* CMD 0x50 (get ECDH server params after partition_flash) */
#define VCSFW_CMD_GET_ECDH 0x50
/* Reboot: 0x05 0x02 0x00 */
#define VCSFW_CMD_REBOOT 0x05
/**
* validity_pair_ssm_new:
* @dev: the FpDevice
*
* Create a new pairing SSM. The caller must set up
* self->pair_state.dev_desc before starting the SSM.
*
* Returns: a new FpiSsm.
*/
FpiSsm *validity_pair_ssm_new (FpDevice *dev);
/**
* validity_pair_run_state:
* @ssm: the pairing SSM
* @dev: the FpDevice
*
* State machine runner for the pairing SSM.
*/
void validity_pair_run_state (FpiSsm *ssm,
FpDevice *dev);
/**
* validity_pair_build_tls_flash:
* @state: pairing state (must have priv_blob, server_cert, ecdh_blob set)
* @out_len: output data length (always 0x1000 = 4096)
*
* Build the TLS flash image for persistence. Format matches
* python-validity make_tls_flash(): a sequence of flash blocks
* zero-padded to 4096 bytes.
*
* Returns: newly allocated 4096-byte buffer. Free with g_free().
*/
guint8 *validity_pair_build_tls_flash (const ValidityPairState *state,
const guint8 *ca_cert,
gsize ca_cert_len,
gsize *out_len);

View file

@ -0,0 +1,443 @@
/*
* 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 "validity_pack.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=0x0190: post-firmware-update major (06cb:009a etc.) */
{ 0x0190, 0x2449, 0x01, 0xff, "86C FM-3290-002" },
{ 0x0190, 0x2449, 0x02, 0xff, "86C FM-3324-001" },
{ 0x0190, 0x057b, 0x03, 0xff, "88B0 FM-3316-001" },
{ 0x0190, 0x1ff5, 0x04, 0xff, "57K0 FM-3328-001" },
{ 0x0190, 0x00b5, 0x05, 0xff, "57K0 FM-3297-004" },
{ 0x0190, 0x0c6d, 0x06, 0xff, "57K0 FM-3297-005" },
{ 0x0190, 0x00b5, 0x07, 0xff, "57K0 FM-3297-006" },
{ 0x0190, 0x00b5, 0x08, 0xff, "57K0 FM-3297-007" },
{ 0x0190, 0x00b5, 0x09, 0xff, "57K0 FM-3297-008" },
{ 0x0190, 0x00b5, 0x0a, 0xff, "57K0 FM-3297-009" },
{ 0x0190, 0x00b5, 0x0b, 0xff, "57K0 FM-3297-010" },
{ 0x0190, 0x00b5, 0x0c, 0xff, "57K0 FM-3297-011" },
{ 0x0190, 0x057b, 0x0d, 0xff, "88B0 FM-3300-001" },
{ 0x0190, 0x04c3, 0x0e, 0xff, "55E FM3327-FM3342" },
{ 0x0190, 0x0191, 0x0f, 0xff, "57L0 FM-3331-001" },
{ 0x0190, 0x0191, 0x10, 0xff, "57L0 FM-3331-002" },
{ 0x0190, 0x0191, 0x11, 0xff, "57L0 FM-3331-003" },
{ 0x0190, 0x0580, 0x12, 0xff, "88B0 FM-3310-001" },
{ 0x0190, 0x0580, 0x13, 0xff, "88B0 FM-3310-002" },
{ 0x0190, 0x0191, 0x14, 0xff, "57L0 FM-151-003" },
{ 0x0190, 0x0191, 0x15, 0xff, "57L0 FM-211-002" },
{ 0x0190, 0x0191, 0x16, 0xff, "57L0 FM-3299-002" },
{ 0x0190, 0x0d49, 0x17, 0xff, "57L0 FM-3331-004" },
{ 0x0190, 0x1131, 0x18, 0xff, "57L0 FM-3331-005" },
{ 0x0190, 0x0197, 0x19, 0xff, "73A0 FM-3332-001" },
{ 0x0190, 0x0195, 0x1a, 0xff, "86D TM3329-001-003" },
{ 0x0190, 0x0195, 0x1b, 0xff, "86D TM3329-002-006" },
{ 0x0190, 0x0196, 0x1c, 0xff, "57K0 FM-155-003" },
{ 0x0190, 0x2449, 0x1d, 0xff, "86C TM-3315-001" },
{ 0x0190, 0x2449, 0x1e, 0xff, "86C TM-3315-002" },
{ 0x0190, 0x2449, 0x1f, 0xff, "86C TM-3322-001" },
{ 0x0190, 0x2449, 0x20, 0xff, "86C FM-3326-001" },
{ 0x0190, 0x2449, 0x21, 0xff, "86C FM-3208-002" },
{ 0x0190, 0x2449, 0x22, 0xff, "86C FM-3340-001" },
{ 0x0190, 0x0196, 0x23, 0xff, "57K0 FM-155-004" },
{ 0x0190, 0x00b5, 0x24, 0xff, "57K0 FM-3297-012" },
{ 0x0190, 0x00b5, 0x25, 0xff, "57K0 FM-3297-013" },
{ 0x0190, 0x0197, 0x26, 0xff, "73A0 FM-3341-001" },
{ 0x0190, 0x2449, 0x28, 0xff, "86C TM-3315-003" },
{ 0x0190, 0x00b5, 0x29, 0xff, "57K0 FM-3297-020" },
{ 0x0190, 0x00b5, 0x2a, 0xff, "57K0 FM-3297-021" },
{ 0x0190, 0x00b5, 0x2b, 0xff, "57K0 FM-3297-022" },
{ 0x0190, 0x00b5, 0x2c, 0xff, "57K0 FM-3297-023" },
{ 0x0190, 0x00b5, 0x2d, 0xff, "57K0 FM-3297-024" },
{ 0x0190, 0x00b5, 0x2e, 0xff, "57K0 FM-3297-025" },
{ 0x0190, 0x00b5, 0x2f, 0xff, "57K0 FM-3297-026" },
{ 0x0190, 0x00b5, 0x30, 0xff, "57K0 FM-3297-027" },
{ 0x0190, 0x00b5, 0x31, 0xff, "57K0 FM-3297-028" },
{ 0x0190, 0x00b5, 0x32, 0xff, "57K0 FM-3297-029" },
{ 0x0190, 0x00b5, 0x33, 0xff, "57K0 FM-3297-030" },
{ 0x0190, 0x00b5, 0x34, 0xff, "57K0 FM-3297-031" },
{ 0x0190, 0x00b5, 0x35, 0xff, "57K0 FM-3297-014" },
{ 0x0190, 0x00b5, 0x36, 0xff, "57K0 FM-3297-015" },
{ 0x0190, 0x00b5, 0x37, 0xff, "57K0 FM-3297-032" },
{ 0x0190, 0x00b5, 0x38, 0xff, "57K0 FM-3297-033" },
{ 0x0190, 0x057b, 0x39, 0xff, "88B0 FM-3300-002" },
{ 0x0190, 0x00de, 0x3a, 0xff, "109A FM-3302-001" },
{ 0x0190, 0x057e, 0x3b, 0xff, "57K0 FM-154-020" },
{ 0x0190, 0x0581, 0x3c, 0xff, "57K0 FM-154-021" },
{ 0x0190, 0x2449, 0x3d, 0xff, "86C TM-3226-001" },
{ 0x0190, 0x0195, 0x3e, 0xff, "86D TM3329-004-007" },
{ 0x0190, 0x0196, 0x3f, 0xff, "57K0 FM-155-002" },
{ 0x0190, 0x1825, 0x41, 0xff, "57K0 FM-154-001" },
{ 0x0190, 0x0581, 0x42, 0xff, "57K0 FM-154-022" },
{ 0x0190, 0x057b, 0x43, 0xff, "88B0 FM3358-3359" },
{ 0x0190, 0x057b, 0x44, 0xff, "88B0 FM-3358-002" },
{ 0x0190, 0x057b, 0x45, 0xff, "88B0 FM-3359-001" },
{ 0x0190, 0x00b5, 0x46, 0xff, "57K0 FM-3297-100" },
{ 0x0190, 0x057e, 0x47, 0xff, "57K0 FM-154-200" },
{ 0x0190, 0x0198, 0x49, 0xff, "88B0 FM-3366-001" },
{ 0x0190, 0x0199, 0x4a, 0xff, "57K0 FM-3367-001" },
{ 0x0190, 0x2449, 0x4b, 0xff, "86C TM-3368-001" },
{ 0x0190, 0x00db, 0x4c, 0xff, "55E FM-209-005" },
{ 0x0190, 0x0969, 0x4f, 0xff, "57K0 FM-154-023" },
{ 0x0190, 0x0580, 0x50, 0xff, "88B0 FM-3373-001" },
{ 0x0190, 0x00db, 0x51, 0xff, "55E FM-209-006" },
{ 0x0190, 0x0581, 0x52, 0xff, "57K0 FM-154-001" },
{ 0x0190, 0x0581, 0x53, 0xff, "57K0 FM-154-002" },
{ 0x0190, 0x0581, 0x54, 0xff, "57K0 FM-154-003" },
{ 0x0190, 0x0d51, 0x55, 0xff, "57K0 FM-154-020" },
{ 0x0190, 0x0581, 0x56, 0xff, "57K0 FM-155-001" },
{ 0x0190, 0x0199, 0x57, 0xff, "57K0 FM-155-002" },
{ 0x0190, 0x0195, 0x58, 0xff, "86D TM-3329-005" },
{ 0x0190, 0x0199, 0x59, 0xff, "57K0 FM-3367-002" },
{ 0x0190, 0x0199, 0x5a, 0xff, "57K0 FM-3367-003" },
{ 0x0190, 0x0199, 0x5b, 0xff, "57K0 FM-3367-004" },
{ 0x0190, 0x00db, 0x5c, 0xff, "55E FM-160-004" },
{ 0x0190, 0x0968, 0x5d, 0xff, "88B0 FM-3366-002" },
{ 0x0190, 0x2449, 0x5e, 0xff, "86C TM-P3376-P3404" },
{ 0x0190, 0x00b5, 0x5f, 0xff, "57K0 FM-3380-001" },
{ 0x0190, 0x0199, 0x60, 0xff, "57K0 FM-3380-002" },
{ 0x0190, 0x0199, 0x61, 0xff, "57K0 FM-3380-003" },
{ 0x0190, 0x0199, 0x62, 0xff, "57K0 FM-3380-004" },
{ 0x0190, 0x2449, 0x63, 0xff, "86C FM-3290-003" },
{ 0x0190, 0x057b, 0x64, 0xff, "88B0 FM-3358-003" },
{ 0x0190, 0x2449, 0x65, 0xff, "86C FM-3389-001" },
{ 0x0190, 0x0199, 0x68, 0xff, "57K0 FM-3367-005" },
{ 0x0190, 0x0199, 0x69, 0xff, "57K0 FM-3367-006" },
{ 0x0190, 0x0199, 0x6a, 0xff, "57K0 FM-3380-001b" },
{ 0x0190, 0x0191, 0x6b, 0xff, "57L0 FM-3396-001" },
{ 0x0190, 0x0191, 0x6c, 0xff, "57L0 FM-3397-001" },
{ 0x0190, 0x2449, 0x6e, 0xff, "86C TM3261-003-004" },
{ 0x0190, 0x0581, 0x6f, 0xff, "57K0 FM-3395-001" },
{ 0x0190, 0x0d51, 0x70, 0xff, "57K0 FM-154-120" },
{ 0x0190, 0x0969, 0x71, 0xff, "57K0 FM-154-123" },
{ 0x0190, 0x00b5, 0x72, 0xff, "57K0 FM-3401-001" },
{ 0x0190, 0x00b5, 0x73, 0xff, "57K0 FM-3401-004" },
{ 0x0190, 0x00b5, 0x74, 0xff, "57K0 FM-3401-005" },
{ 0x0190, 0x00b5, 0x75, 0xff, "57K0 FM-3401-006" },
{ 0x0190, 0x0199, 0x76, 0xff, "57K0 FM-155-005" },
{ 0x0190, 0x0c6d, 0x79, 0xff, "57K0 FM-3297-034" },
{ 0x0190, 0x00b5, 0x7a, 0xff, "57K0 FM-3297-035" },
{ 0x0190, 0x057b, 0x7b, 0xff, "88B0 FM-3358-004" },
{ 0x0190, 0x057b, 0x7c, 0xff, "88B0 FM-3358-005" },
{ 0x0190, 0x0199, 0x7e, 0xff, "57K0 FM-155-007" },
{ 0x0190, 0x0199, 0x82, 0xff, "57K0 FM-155-102" },
{ 0x0190, 0x0d51, 0x83, 0xff, "57K0 FM-3439-001" },
{ 0x0190, 0x2449, 0x84, 0xff, "86C FM-3324-002" },
{ 0x0190, 0x0969, 0x85, 0xff, "57K0 FM-3439-002" },
{ 0x0190, 0x2449, 0x86, 0xff, "86C FM-3324-003" },
{ 0x0190, 0x0969, 0x87, 0xff, "57K0 FM-3439-003" },
{ 0x0190, 0x0969, 0x88, 0xff, "57K0 FM-3439-004" },
{ 0x0190, 0x0199, 0x89, 0xff, "57K0 FM-155-008" },
{ 0x0190, 0x0581, 0x8a, 0xff, "57K0 FM-154-124" },
{ 0x0190, 0x057b, 0x8b, 0xff, "88B0 FM-3358-007" },
{ 0x0190, 0x0581, 0x8c, 0xff, "57K0 FM-3439-005" },
{ 0x0190, 0x0969, 0x8d, 0xff, "57K0 FM-3439-006" },
{ 0x0190, 0x0199, 0x8e, 0xff, "57K0 FM-155-103" },
{ 0x0190, 0x0581, 0x8f, 0xff, "57K0 FM-154-125" },
{ 0x0190, 0x0581, 0x90, 0xff, "57K0 FM-3439-007" },
{ 0x0190, 0x0969, 0x91, 0xff, "57K0 FM-3439-008" },
{ 0x0190, 0x0969, 0x92, 0xff, "57K0 FM-3439-009" },
{ 0x0190, 0x0969, 0x93, 0xff, "57K0 FM-3439-010" },
{ 0x0190, 0x0969, 0x94, 0xff, "57K0 FM-3439-011" },
{ 0x0190, 0x0969, 0x95, 0xff, "57K0 FM-3439-108" },
{ 0x0190, 0x0969, 0x96, 0xff, "57K0 FM-3439-109" },
{ 0x0190, 0x0969, 0x97, 0xff, "57K0 FM-3439-110" },
{ 0x0190, 0x057b, 0x98, 0xff, "88B0 FM-3358-008" },
{ 0x0190, 0x057b, 0x99, 0xff, "88B0 FM-3358-009" },
{ 0x0190, 0x0d51, 0x9a, 0xff, "57K0 FM-3439-101" },
{ 0x0190, 0x0969, 0x9b, 0xff, "57K0 FM-3439-102" },
{ 0x0190, 0x0969, 0x9c, 0xff, "57K0 FM-3439-012" },
{ 0x0190, 0x1139, 0x9d, 0xff, "57K0 FM-3439-013" },
{ 0x0190, 0x0969, 0x9e, 0xff, "57K0 FM-3439-014" },
{ 0x0190, 0x0c6d, 0x9f, 0xff, "57K0 FM-3297-036" },
{ 0x0190, 0x057b, 0xa0, 0xff, "88B0 FM-3358-010" },
{ 0x0190, 0x057b, 0xa1, 0xff, "88B0 FM-3316-002" },
{ 0x0190, 0x2449, 0xa2, 0xff, "86C TM-P3568-001" },
{ 0x0190, 0x2449, 0xa3, 0xff, "86C TM-P3569-001" },
/* 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)
{
guint32 zeroes;
g_return_val_if_fail (data != NULL, FALSE);
g_return_val_if_fail (out != NULL, FALSE);
return validity_unpack (data, data_len, "whh",
&zeroes, &out->hw_version, &out->hw_major);
}
/* ================================================================
* 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;
return validity_pack (buf, buf_len, "bhhw",
0x6f, /* VCSFW_CMD_GET_FACTORY_BITS */
tag, (guint16) 0, (guint32) 0);
}
/* ================================================================
* 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);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,227 @@
/*
* TLS session management for Validity/Synaptics VCSFW fingerprint sensors
*
* 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>
#include <openssl/evp.h>
#include <openssl/ec.h>
/* TLS record content types */
#define TLS_CONTENT_CHANGE_CIPHER 0x14
#define TLS_CONTENT_HANDSHAKE 0x16
#define TLS_CONTENT_APP_DATA 0x17
/* TLS version 1.2 */
#define TLS_VERSION_MAJOR 0x03
#define TLS_VERSION_MINOR 0x03
/* TLS handshake message types */
#define TLS_HS_CLIENT_HELLO 0x01
#define TLS_HS_SERVER_HELLO 0x02
#define TLS_HS_CERTIFICATE 0x0B
#define TLS_HS_CERT_REQUEST 0x0D
#define TLS_HS_SERVER_HELLO_DONE 0x0E
#define TLS_HS_CERT_VERIFY 0x0F
#define TLS_HS_CLIENT_KEY_EXCHANGE 0x10
#define TLS_HS_FINISHED 0x14
/* Cipher suite */
#define TLS_CS_ECDH_ECDSA_AES256_CBC_SHA 0xC005
/* Key/block sizes */
#define TLS_AES_KEY_SIZE 32
#define TLS_IV_SIZE 16
#define TLS_HMAC_SIZE 32
#define TLS_AES_BLOCK_SIZE 16
#define TLS_MASTER_SECRET_SIZE 48
#define TLS_KEY_BLOCK_SIZE 0x120
#define TLS_RANDOM_SIZE 32
#define TLS_VERIFY_DATA_SIZE 12
/* VCSFW TLS command prefix */
#define TLS_CMD_PREFIX_SIZE 4
/* Flash block IDs */
#define TLS_FLASH_BLOCK_EMPTY0 0x0000
#define TLS_FLASH_BLOCK_EMPTY1 0x0001
#define TLS_FLASH_BLOCK_EMPTY2 0x0002
#define TLS_FLASH_BLOCK_CERT 0x0003
#define TLS_FLASH_BLOCK_PRIVKEY 0x0004
#define TLS_FLASH_BLOCK_CA_CERT 0x0005
#define TLS_FLASH_BLOCK_ECDH 0x0006
#define TLS_FLASH_BLOCK_END 0xFFFF
/* Flash block header: [id:2 LE][size:2 LE][sha256:32] */
#define TLS_FLASH_BLOCK_HEADER_SIZE (2 + 2 + 32)
/* ECDH key blob offsets */
#define TLS_ECDH_BLOB_SIZE 0x90
#define TLS_ECDH_X_OFFSET 0x08
#define TLS_ECDH_Y_OFFSET 0x4C
#define TLS_EC_COORD_SIZE 0x20
/* Forward declaration */
typedef struct _FpiDeviceValidity FpiDeviceValidity;
/* TLS session state */
typedef struct
{
/* Session keys (derived during handshake) */
guint8 sign_key[TLS_AES_KEY_SIZE];
guint8 validation_key[TLS_AES_KEY_SIZE];
guint8 encryption_key[TLS_AES_KEY_SIZE];
guint8 decryption_key[TLS_AES_KEY_SIZE];
/* Pre-shared keys (derived from hardware identity) */
guint8 psk_encryption_key[TLS_AES_KEY_SIZE];
guint8 psk_validation_key[TLS_AES_KEY_SIZE];
/* Handshake state */
GChecksum *handshake_hash; /* running SHA-256 of handshake messages */
guint8 client_random[TLS_RANDOM_SIZE];
guint8 server_random[TLS_RANDOM_SIZE];
guint8 master_secret[TLS_MASTER_SECRET_SIZE];
/* ECDH session ephemeral key pair (generated per handshake) */
EVP_PKEY *session_key;
/* TLS client certificate from flash (block ID 3) */
guint8 *tls_cert;
gsize tls_cert_len;
/* Client private key from flash (block ID 4, decrypted with PSK) */
EVP_PKEY *priv_key;
/* ECDH server public key from flash (block ID 6) */
EVP_PKEY *ecdh_q;
/* Raw flash blobs (needed for pairing later) */
guint8 *priv_blob;
gsize priv_blob_len;
guint8 *ecdh_blob;
gsize ecdh_blob_len;
/* TLS channel state */
gboolean secure_rx;
gboolean secure_tx;
gboolean keys_loaded;
/* Runtime data keys (populated from data store during open) */
const guint8 *password; /* TLS password (32 bytes) */
gsize password_len;
const guint8 *gwk_sign; /* GWK signing key (32 bytes) */
gsize gwk_sign_len;
const guint8 *fw_pubkey_x; /* firmware ECDSA public key X (32 bytes) */
gsize fw_pubkey_x_len;
const guint8 *fw_pubkey_y; /* firmware ECDSA public key Y (32 bytes) */
gsize fw_pubkey_y_len;
} ValidityTlsState;
/* TLS handshake SSM states */
typedef enum {
TLS_HS_SEND_CLIENT_HELLO = 0,
TLS_HS_RECV_SERVER_HELLO,
TLS_HS_SEND_CLIENT_FINISH,
TLS_HS_RECV_SERVER_FINISH,
TLS_HS_PARSE_SERVER_FINISH,
TLS_HS_NUM_STATES,
} ValidityTlsHandshakeState;
/* TLS flash read SSM states */
typedef enum {
TLS_FLASH_READ_CMD = 0,
TLS_FLASH_READ_RECV,
TLS_FLASH_READ_NUM_STATES,
} ValidityTlsFlashReadState;
/* ---- Public API ---- */
void validity_tls_init (ValidityTlsState *tls);
void validity_tls_free (ValidityTlsState *tls);
void validity_tls_derive_psk (ValidityTlsState *tls);
gboolean validity_tls_parse_flash (ValidityTlsState *tls,
const guint8 *data,
gsize data_len,
GError **error);
/* PRF — exported for testing */
void validity_tls_prf (const guint8 *secret,
gsize secret_len,
const guint8 *seed,
gsize seed_len,
guint8 *output,
gsize output_len);
/* Encrypt/decrypt for TLS app data */
guint8 *validity_tls_encrypt (ValidityTlsState *tls,
const guint8 *plaintext,
gsize plaintext_len,
gsize *out_len);
guint8 *validity_tls_decrypt (ValidityTlsState *tls,
const guint8 *ciphertext,
gsize ciphertext_len,
gsize *out_len,
GError **error);
/* Build TLS app_data record wrapping a VCSFW command */
guint8 *validity_tls_wrap_app_data (ValidityTlsState *tls,
const guint8 *cmd,
gsize cmd_len,
gsize *out_len);
/* Parse TLS response, returning decrypted app_data */
guint8 *validity_tls_unwrap_response (ValidityTlsState *tls,
const guint8 *response,
gsize response_len,
gsize *out_len,
GError **error);
/* Build the full handshake USB commands.
* Returns the first USB command (ClientHello with 0x44000000 prefix). */
guint8 *validity_tls_build_client_hello (ValidityTlsState *tls,
gsize *out_len);
/* Parse ServerHello + CertReq + ServerHelloDone from USB response */
gboolean validity_tls_parse_server_hello (ValidityTlsState *tls,
const guint8 *data,
gsize data_len,
GError **error);
/* Build Certificate + ClientKeyExchange + CertVerify + ChangeCipherSpec + Finished */
guint8 *validity_tls_build_client_finish (ValidityTlsState *tls,
gsize *out_len);
/* Parse server ChangeCipherSpec + Finished */
gboolean validity_tls_parse_server_finish (ValidityTlsState *tls,
const guint8 *data,
gsize data_len,
GError **error);
/* SSM runner for TLS handshake */
void validity_tls_handshake_run_state (FpiSsm *ssm,
FpDevice *dev);
/* SSM runner for reading flash TLS data */
void validity_tls_flash_read_run_state (FpiSsm *ssm,
FpDevice *dev);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,392 @@
/*
* VCSFW protocol implementation for Validity/Synaptics fingerprint sensors
*
* 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 "validity_pack.h"
#include "vcsfw_protocol.h"
/* ---- VcsfwCmdData lifecycle ---- */
VcsfwCmdData *
vcsfw_cmd_data_new (const guint8 *cmd,
gsize cmd_len,
VcsfwCmdCallback callback)
{
VcsfwCmdData *data = g_new0 (VcsfwCmdData, 1);
data->cmd_data = g_memdup2 (cmd, cmd_len);
data->cmd_len = cmd_len;
data->callback = callback;
return data;
}
void
vcsfw_cmd_data_free (gpointer data)
{
VcsfwCmdData *cmd_data = data;
if (cmd_data == NULL)
return;
g_free (cmd_data->cmd_data);
g_free (cmd_data);
}
static void
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;
}
if (transfer->actual_length < 2)
{
g_warning ("VCSFW response too short: %" G_GSSIZE_FORMAT " bytes",
transfer->actual_length);
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;
}
validity_unpack (transfer->buffer, transfer->actual_length, "h", &status);
fp_dbg ("VCSFW response: status=0x%04x, len=%" G_GSSIZE_FORMAT,
status, transfer->actual_length - 2);
/* Stash status and data for direct access in RECV states */
self->cmd_response_status = status;
g_clear_pointer (&self->cmd_response_data, g_free);
if (transfer->actual_length > 2)
{
self->cmd_response_len = transfer->actual_length - 2;
self->cmd_response_data = g_memdup2 (transfer->buffer + 2,
self->cmd_response_len);
}
else
{
self->cmd_response_len = 0;
self->cmd_response_data = NULL;
}
if (cmd_data->callback)
{
cmd_data->callback (self,
self->cmd_response_data,
self->cmd_response_len,
status,
NULL);
}
/* If the callback didn't fail the SSM, advance it */
fpi_ssm_mark_completed (transfer->ssm);
}
void
vcsfw_cmd_run_state (FpiSsm *ssm,
FpDevice *dev)
{
VcsfwCmdData *cmd_data = fpi_ssm_get_data (ssm);
FpiUsbTransfer *transfer;
switch (fpi_ssm_get_cur_state (ssm))
{
case VCSFW_CMD_STATE_SEND:
fp_dbg ("VCSFW send cmd 0x%02x, len=%" G_GSIZE_FORMAT,
cmd_data->cmd_data[0], cmd_data->cmd_len);
transfer = fpi_usb_transfer_new (dev);
transfer->short_is_error = TRUE;
fpi_usb_transfer_fill_bulk (transfer, VALIDITY_EP_CMD_OUT,
cmd_data->cmd_len);
memcpy (transfer->buffer, cmd_data->cmd_data, cmd_data->cmd_len);
transfer->ssm = ssm;
fpi_usb_transfer_submit (transfer, VALIDITY_USB_TIMEOUT, NULL,
fpi_ssm_usb_transfer_cb, NULL);
break;
case VCSFW_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,
cmd_receive_cb, NULL);
break;
}
}
static void
cmd_ssm_done (FpiSsm *ssm,
FpDevice *dev,
GError *error)
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev);
self->cmd_ssm = NULL;
if (error)
fp_dbg ("VCSFW command SSM failed: %s", error->message);
/* Error is handled by the callback, nothing else to do here */
g_clear_error (&error);
}
void
vcsfw_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_cmd_run_state,
VCSFW_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);
}
/* ================================================================
* 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 */
{
GString *hex = g_string_new ("VCSFW TLS raw response: ");
for (gsize i = 0; i < MIN ((gsize) transfer->actual_length, (gsize) 40); i++)
g_string_append_printf (hex, "%02x ", transfer->buffer[i]);
if (transfer->actual_length > 40)
g_string_append_printf (hex, "... (%" G_GSSIZE_FORMAT " total)",
transfer->actual_length);
fp_dbg ("%s", hex->str);
g_string_free (hex, TRUE);
}
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;
}
validity_unpack (decrypted, decrypted_len, "h", &status);
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: TLS record directly (no 0x44000000 prefix
* for post-handshake app data, per python-validity) */
gsize usb_len = 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);
memcpy (transfer->buffer, 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
vcsfw_parse_version (const guint8 *data,
gsize data_len,
ValidityVersionInfo *info)
{
const guint8 *serial;
g_return_val_if_fail (data != NULL, FALSE);
g_return_val_if_fail (info != NULL, FALSE);
if (!validity_unpack (data, data_len, "wwbbbbbbbbdhbb",
&info->build_time, &info->build_num,
&info->version_major, &info->version_minor,
&info->target, &info->product,
&info->silicon_rev, &info->formal_release,
&info->platform, &info->patch,
&serial, (gsize) sizeof (info->serial_number),
&info->security,
&info->iface, &info->device_type))
return FALSE;
memcpy (info->serial_number, serial, sizeof (info->serial_number));
return TRUE;
}

View file

@ -0,0 +1,131 @@
/*
* VCSFW protocol definitions for Validity/Synaptics fingerprint sensors
*
* 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 "validity.h"
/* ---- VCSFW Command IDs (pre-TLS) ---- */
#define VCSFW_CMD_GET_VERSION 0x01
#define VCSFW_CMD_READ_HW_REG32 0x07
#define VCSFW_CMD_WRITE_HW_REG32 0x08
#define VCSFW_CMD_UNKNOWN_INIT 0x19
#define VCSFW_CMD_GET_FLASH_INFO 0x3E
#define VCSFW_CMD_READ_FLASH 0x40
#define VCSFW_CMD_WRITE_FLASH 0x41
#define VCSFW_CMD_WRITE_FW_SIG 0x42
#define VCSFW_CMD_GET_FW_INFO 0x43
#define VCSFW_CMD_PARTITION_FLASH 0x4F
/* ---- VCSFW Command IDs (post-TLS, via tls.app()) ---- */
#define VCSFW_CMD_CAPTURE 0x02
#define VCSFW_CMD_CAPTURE_STOP 0x04
#define VCSFW_CMD_GLOW_START 0x39
#define VCSFW_CMD_ERASE_FLASH 0x3F
#define VCSFW_CMD_DB_INFO 0x45
#define VCSFW_CMD_GET_RECORD_CHILDREN 0x46
#define VCSFW_CMD_NEW_RECORD 0x47
#define VCSFW_CMD_DEL_RECORD 0x48
#define VCSFW_CMD_GET_RECORD_VALUE 0x49
#define VCSFW_CMD_GET_USER 0x4A
#define VCSFW_CMD_GET_USER_STORAGE 0x4B
#define VCSFW_CMD_GET_PRG_STATUS 0x51
#define VCSFW_CMD_MATCH_FINGER 0x5E
#define VCSFW_CMD_GET_MATCH_RESULT 0x60
#define VCSFW_CMD_MATCH_CLEANUP 0x62
#define VCSFW_CMD_ENROLLMENT_UPDATE_START 0x68
#define VCSFW_CMD_CREATE_ENROLLMENT 0x69
#define VCSFW_CMD_ENROLLMENT_UPDATE 0x6B
#define VCSFW_CMD_GET_FACTORY_BITS 0x6F
#define VCSFW_CMD_IDENTIFY_SENSOR 0x75
/* ---- VCSFW Response Status Codes ---- */
#define VCSFW_STATUS_OK 0x0000
#define VCSFW_STATUS_NO_FW 0xB004
/* ---- Callback types ---- */
/**
* VcsfwCmdCallback:
* @self: the validity device
* @data: response data (after 2-byte status, NULL on error)
* @data_len: length of response data (excluding 2-byte status)
* @status: the 2-byte VCSFW status code
* @error: a GError if the transfer failed, or NULL
*
* Callback invoked after a VCSFW command/response exchange completes.
*/
typedef void (*VcsfwCmdCallback) (FpiDeviceValidity *self,
const guint8 *data,
gsize data_len,
guint16 status,
GError *error);
/* ---- Command/response SSM states ---- */
typedef enum {
VCSFW_CMD_STATE_SEND = 0,
VCSFW_CMD_STATE_RECV,
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
{
guint8 *cmd_data;
gsize cmd_len;
VcsfwCmdCallback callback;
} VcsfwCmdData;
/* ---- Functions ---- */
void vcsfw_cmd_run_state (FpiSsm *ssm,
FpDevice *dev);
VcsfwCmdData *vcsfw_cmd_data_new (const guint8 *cmd,
gsize cmd_len,
VcsfwCmdCallback callback);
void vcsfw_cmd_data_free (gpointer data);
void vcsfw_cmd_send (FpiDeviceValidity *self,
FpiSsm *parent_ssm,
const guint8 *cmd,
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

@ -62,7 +62,6 @@ static const FpIdEntry allowlist_id_table[] = {
{ .vid = 0x06cb, .pid = 0x0081 },
{ .vid = 0x06cb, .pid = 0x0088 },
{ .vid = 0x06cb, .pid = 0x008a },
{ .vid = 0x06cb, .pid = 0x009a },
{ .vid = 0x06cb, .pid = 0x009b },
{ .vid = 0x06cb, .pid = 0x00a1 },
{ .vid = 0x06cb, .pid = 0x00a2 },
@ -110,11 +109,8 @@ static const FpIdEntry allowlist_id_table[] = {
{ .vid = 0x138a, .pid = 0x003c },
{ .vid = 0x138a, .pid = 0x003d },
{ .vid = 0x138a, .pid = 0x003f },
{ .vid = 0x138a, .pid = 0x0090 },
{ .vid = 0x138a, .pid = 0x0092 },
{ .vid = 0x138a, .pid = 0x0094 },
{ .vid = 0x138a, .pid = 0x0097 },
{ .vid = 0x138a, .pid = 0x009d },
{ .vid = 0x138a, .pid = 0x00ab },
{ .vid = 0x138a, .pid = 0x00a6 },
{ .vid = 0x147e, .pid = 0x1002 },

View file

@ -153,6 +153,19 @@ driver_sources = {
[ 'drivers/realtek/realtek.c' ],
'focaltech_moc' :
[ 'drivers/focaltech_moc/focaltech_moc.c' ],
'validity' :
[ 'drivers/validity/validity.c',
'drivers/validity/vcsfw_protocol.c',
'drivers/validity/validity_tls.c',
'drivers/validity/validity_fwext.c',
'drivers/validity/validity_sensor.c',
'drivers/validity/validity_capture.c',
'drivers/validity/validity_db.c',
'drivers/validity/validity_enroll.c',
'drivers/validity/validity_verify.c',
'drivers/validity/validity_hal.c',
'drivers/validity/validity_pair.c',
'drivers/validity/validity_data.c' ],
}
helper_sources = {

View file

@ -144,6 +144,7 @@ default_drivers = [
'fpcmoc',
'realtek',
'focaltech_moc',
'validity',
]
spi_drivers = [
@ -178,6 +179,7 @@ endian_independent_drivers = virtual_drivers + [
'vcom5s',
'vfs101',
'vfs7552',
'validity',
]
all_drivers = default_drivers + virtual_drivers
@ -210,6 +212,7 @@ driver_helper_mapping = {
'aes3500' : [ 'aeslib', 'aes3k' ],
'aes4000' : [ 'aeslib', 'aes3k' ],
'uru4000' : [ 'openssl' ],
'validity' : [ 'openssl' ],
'elanspi' : [ 'udev' ],
'virtual_image' : [ 'virtual' ],
'virtual_device' : [ 'virtual' ],

View file

@ -22,6 +22,15 @@ for m in devices_re.finditer(data):
pid = m.group(2)
devices.append((vid, pid))
# TODO:remove when the devices are removed from the wiki unsupported list.
allow = []
allow.append(("06cb", "009a"))
allow.append(("138a", "0090"))
allow.append(("138a", "0097"))
allow.append(("138a", "009d"))
devices = [d for d in devices if d not in allow]
generator = open(os.path.join(os.path.dirname(__file__), '..', 'libfprint', 'fprint-list-udev-hwdb.c')).read()
id_re = re.compile(' { .vid = 0x([a-fA-F0-9]*), .pid = 0x([a-fA-F0-9]*) }')

View file

@ -33,3 +33,19 @@
...
fun:libusb_init_context
}
{
<ignore-openssl-sha256-stack-uninitialised-value8>
Memcheck:Value8
fun:gchecksum_digest_to_string
fun:*sha256*
fun:g_checksum_get_digest
}
{
<ignore-openssl-sha256-stack-uninitialised-cond>
Memcheck:Cond
fun:gchecksum_digest_to_string
fun:*sha256*
fun:g_checksum_get_digest
}

View file

@ -59,6 +59,7 @@ drivers_tests = [
'realtek',
'realtek-5816',
'focaltech_moc',
'validity',
]
if get_option('introspection')
@ -321,6 +322,26 @@ foreach test_name: unit_tests
)
endforeach
# Validity unit tests (needs driver library + OpenSSL)
if 'validity' in supported_drivers
openssl_dep = dependency('openssl', version: '>= 3.0', required: false)
if openssl_dep.found()
validity_test = executable('test-validity',
sources: 'test-validity.c',
dependencies: [ libfprint_private_dep, openssl_dep ],
c_args: common_cflags,
link_with: libfprint_drivers,
link_whole: test_utils,
install: false,
)
test('validity',
validity_test,
suite: ['unit-tests'],
env: envs,
)
endif
endif
# Run udev rule generator with fatal warnings
envs.set('UDEV_HWDB', udev_hwdb.full_path())
envs.set('UDEV_HWDB_CHECK_CONTENTS', default_drivers_are_enabled ? '1' : '0')

5748
tests/test-validity.c Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.

103
tests/validity/custom.py Normal file
View file

@ -0,0 +1,103 @@
#!/usr/bin/python3
import traceback
import sys
import gi
gi.require_version('FPrint', '2.0')
from gi.repository import FPrint, GLib
# Exit with error on any exception, including those happening in callbacks
sys.excepthook = lambda *args: (traceback.print_exception(*args), sys.exit(1))
ctx = GLib.main_context_default()
c = FPrint.Context()
c.enumerate()
devices = c.get_devices()
assert len(devices) == 1, f"Expected 1 device, got {len(devices)}"
d = devices[0]
del devices
# Verify driver name
assert d.get_driver() == "validity", f"Expected 'validity', got '{d.get_driver()}'"
# Verify features detected by auto_initialize_features
assert not d.has_feature(FPrint.DeviceFeature.CAPTURE)
assert d.has_feature(FPrint.DeviceFeature.VERIFY)
assert d.has_feature(FPrint.DeviceFeature.IDENTIFY)
assert not d.has_feature(FPrint.DeviceFeature.DUPLICATES_CHECK)
assert d.has_feature(FPrint.DeviceFeature.STORAGE)
assert d.has_feature(FPrint.DeviceFeature.STORAGE_LIST)
assert d.has_feature(FPrint.DeviceFeature.STORAGE_DELETE)
assert d.has_feature(FPrint.DeviceFeature.STORAGE_CLEAR)
assert d.has_feature(FPrint.DeviceFeature.ALWAYS_ON)
d.open_sync()
# 1. Clear storage — ensure the sensor is in a clean state
print("clearing storage")
d.clear_storage_sync()
print("clear done")
# 2. Enroll a finger
template = FPrint.Print.new(d)
def enroll_progress(*args):
print('enroll progress: ' + str(args))
print("enrolling")
assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE
p = d.enroll_sync(template, None, enroll_progress, None)
assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE
print("enroll done")
# 3. List enrolled prints — should have exactly one
print("listing")
stored = d.list_prints_sync()
print("listing done")
assert len(stored) == 1
# 4. Verify against the enrolled print
print("verifying")
assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE
verify_res, verify_print = d.verify_sync(p)
assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE
print("verify done")
del p
assert verify_res == True
# 5. Identify (async) with deserialized prints
identified = False
def identify_done(dev, res):
global identified
identified = True
identify_match, identify_print = dev.identify_finish(res)
print('identification done: ', identify_match, identify_print)
assert identify_match.equal(identify_print)
deserialized_prints = []
for p in stored:
deserialized_prints.append(FPrint.Print.deserialize(p.serialize()))
assert deserialized_prints[-1].equal(p)
del stored
print("async identifying")
d.identify(deserialized_prints, callback=identify_done)
del deserialized_prints
while not identified:
ctx.iteration(True)
# 6. Delete the enrolled print
print("deleting")
d.delete_print_sync(p)
print("delete done")
d.close_sync()
del d
del c

264
tests/validity/device Normal file
View file

@ -0,0 +1,264 @@
P: /devices/pci0000:00/0000:00:14.0/usb1/1-9
N: bus/usb/001/006=12010002FF10FF08CB069A0064010000010109023500010100A0320904000005FF000000070501024000000705810240000007058202400000070583030800040705840310000A
E: BUSNUM=001
E: DEVNAME=/dev/bus/usb/001/006
E: DEVNUM=006
E: DEVTYPE=usb_device
E: DRIVER=usb
E: ID_AUTOSUSPEND=1
E: ID_BUS=usb
E: ID_INTEGRATION=internal
E: ID_MODEL=009a
E: ID_MODEL_ENC=009a
E: ID_MODEL_FROM_DATABASE=Metallica MIS Touch Fingerprint Reader
E: ID_MODEL_ID=009a
E: ID_PATH=pci-0000:00:14.0-usb-0:9
E: ID_PATH_TAG=pci-0000_00_14_0-usb-0_9
E: ID_PATH_WITH_USB_REVISION=pci-0000:00:14.0-usbv2-0:9
E: ID_PERSIST=0
E: ID_REVISION=0164
E: ID_SERIAL=06cb_009a_c7e2948627cb
E: ID_SERIAL_SHORT=c7e2948627cb
E: ID_USB_INTERFACES=:ff0000:
E: ID_USB_MODEL=009a
E: ID_USB_MODEL_ENC=009a
E: ID_USB_MODEL_ID=009a
E: ID_USB_REVISION=0164
E: ID_USB_SERIAL=06cb_009a_c7e2948627cb
E: ID_USB_SERIAL_SHORT=c7e2948627cb
E: ID_USB_VENDOR=06cb
E: ID_USB_VENDOR_ENC=06cb
E: ID_USB_VENDOR_ID=06cb
E: ID_VENDOR=06cb
E: ID_VENDOR_ENC=06cb
E: ID_VENDOR_FROM_DATABASE=Synaptics, Inc.
E: ID_VENDOR_ID=06cb
E: MAJOR=189
E: MINOR=5
E: PRODUCT=6cb/9a/164
E: SUBSYSTEM=usb
E: TYPE=255/16/255
A: authorized=1\n
A: avoid_reset_quirk=0\n
A: bConfigurationValue=1\n
A: bDeviceClass=ff\n
A: bDeviceProtocol=ff\n
A: bDeviceSubClass=10\n
A: bMaxPacketSize0=8\n
A: bMaxPower=100mA\n
A: bNumConfigurations=1\n
A: bNumInterfaces= 1\n
A: bcdDevice=0164\n
A: bmAttributes=a0\n
A: busnum=1\n
A: configuration=
H: descriptors=12010002FF10FF08CB069A0064010000010109023500010100A0320904000005FF000000070501024000000705810240000007058202400000070583030800040705840310000A
A: dev=189:5\n
A: devnum=6\n
A: devpath=9\n
L: driver=../../../../../bus/usb/drivers/usb
L: firmware_node=../../../../LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:1d/device:1e/device:29
A: idProduct=009a\n
A: idVendor=06cb\n
A: ltm_capable=no\n
A: maxchild=0\n
A: physical_location/dock=no\n
A: physical_location/horizontal_position=left\n
A: physical_location/lid=no\n
A: physical_location/panel=unknown\n
A: physical_location/vertical_position=upper\n
L: port=../1-0:1.0/usb1-port9
A: power/active_duration=39041\n
A: power/autosuspend=2\n
A: power/autosuspend_delay_ms=2000\n
A: power/connected_duration=8463727\n
A: power/control=auto\n
A: power/level=auto\n
A: power/persist=1\n
A: power/runtime_active_time=39927\n
A: power/runtime_status=active\n
A: power/runtime_suspended_time=8423564\n
A: power/wakeup=disabled\n
A: power/wakeup_abort_count=\n
A: power/wakeup_active=\n
A: power/wakeup_active_count=\n
A: power/wakeup_count=\n
A: power/wakeup_expire_count=\n
A: power/wakeup_last_time_ms=\n
A: power/wakeup_max_time_ms=\n
A: power/wakeup_total_time_ms=\n
A: quirks=0x0\n
A: removable=fixed\n
A: rx_lanes=1\n
A: serial=c7e2948627cb\n
A: speed=12\n
A: tx_lanes=1\n
A: urbnum=124\n
A: version= 2.00\n
P: /devices/pci0000:00/0000:00:14.0/usb1
N: bus/usb/001/001=12010002090001406B1D020019060302010109021900010100E0000904000001090000000705810304000C
E: BUSNUM=001
E: CURRENT_TAGS=:seat:
E: DEVNAME=/dev/bus/usb/001/001
E: DEVNUM=001
E: DEVTYPE=usb_device
E: DRIVER=usb
E: ID_AUTOSUSPEND=1
E: ID_BUS=usb
E: ID_FOR_SEAT=usb-pci-0000_00_14_0
E: ID_MODEL=xHCI_Host_Controller
E: ID_MODEL_ENC=xHCI\x20Host\x20Controller
E: ID_MODEL_FROM_DATABASE=2.0 root hub
E: ID_MODEL_ID=0002
E: ID_PATH=pci-0000:00:14.0
E: ID_PATH_TAG=pci-0000_00_14_0
E: ID_REVISION=0619
E: ID_SERIAL=Linux_6.19.10-zen1-1-zen_xhci-hcd_xHCI_Host_Controller_0000:00:14.0
E: ID_SERIAL_SHORT=0000:00:14.0
E: ID_USB_INTERFACES=:090000:
E: ID_USB_MODEL=xHCI_Host_Controller
E: ID_USB_MODEL_ENC=xHCI\x20Host\x20Controller
E: ID_USB_MODEL_ID=0002
E: ID_USB_REVISION=0619
E: ID_USB_SERIAL=Linux_6.19.10-zen1-1-zen_xhci-hcd_xHCI_Host_Controller_0000:00:14.0
E: ID_USB_SERIAL_SHORT=0000:00:14.0
E: ID_USB_VENDOR=Linux_6.19.10-zen1-1-zen_xhci-hcd
E: ID_USB_VENDOR_ENC=Linux\x206.19.10-zen1-1-zen\x20xhci-hcd
E: ID_USB_VENDOR_ID=1d6b
E: ID_VENDOR=Linux_6.19.10-zen1-1-zen_xhci-hcd
E: ID_VENDOR_ENC=Linux\x206.19.10-zen1-1-zen\x20xhci-hcd
E: ID_VENDOR_FROM_DATABASE=Linux Foundation
E: ID_VENDOR_ID=1d6b
E: MAJOR=189
E: MINOR=0
E: PRODUCT=1d6b/2/619
E: SUBSYSTEM=usb
E: TAGS=:seat:
E: TYPE=9/0/1
A: authorized=1\n
A: authorized_default=1\n
A: avoid_reset_quirk=0\n
A: bConfigurationValue=1\n
A: bDeviceClass=09\n
A: bDeviceProtocol=01\n
A: bDeviceSubClass=00\n
A: bMaxPacketSize0=64\n
A: bMaxPower=0mA\n
A: bNumConfigurations=1\n
A: bNumInterfaces= 1\n
A: bcdDevice=0619\n
A: bmAttributes=e0\n
A: busnum=1\n
A: configuration=
H: descriptors=12010002090001406B1D020019060302010109021900010100E0000904000001090000000705810304000C
A: dev=189:0\n
A: devnum=1\n
A: devpath=0\n
L: driver=../../../../bus/usb/drivers/usb
L: firmware_node=../../../LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:1d/device:1e
A: idProduct=0002\n
A: idVendor=1d6b\n
A: interface_authorized_default=1\n
A: ltm_capable=no\n
A: manufacturer=Linux 6.19.10-zen1-1-zen xhci-hcd\n
A: maxchild=12\n
A: power/active_duration=8464967\n
A: power/autosuspend=0\n
A: power/autosuspend_delay_ms=0\n
A: power/connected_duration=8464967\n
A: power/control=auto\n
A: power/level=auto\n
A: power/runtime_active_time=8464965\n
A: power/runtime_status=active\n
A: power/runtime_suspended_time=0\n
A: power/wakeup=disabled\n
A: power/wakeup_abort_count=\n
A: power/wakeup_active=\n
A: power/wakeup_active_count=\n
A: power/wakeup_count=\n
A: power/wakeup_expire_count=\n
A: power/wakeup_last_time_ms=\n
A: power/wakeup_max_time_ms=\n
A: power/wakeup_total_time_ms=\n
A: product=xHCI Host Controller\n
A: quirks=0x0\n
A: removable=unknown\n
A: rx_lanes=1\n
A: serial=0000:00:14.0\n
A: speed=480\n
A: tx_lanes=1\n
A: urbnum=1481\n
A: version= 2.00\n
P: /devices/pci0000:00/0000:00:14.0
E: DRIVER=xhci_hcd
E: ID_AUTOSUSPEND=1
E: ID_MODEL_FROM_DATABASE=Sunrise Point-LP USB 3.0 xHCI Controller
E: ID_PATH=pci-0000:00:14.0
E: ID_PATH_TAG=pci-0000_00_14_0
E: ID_PCI_CLASS_FROM_DATABASE=Serial bus controller
E: ID_PCI_INTERFACE_FROM_DATABASE=XHCI
E: ID_PCI_SUBCLASS_FROM_DATABASE=USB controller
E: ID_VENDOR_FROM_DATABASE=Intel Corporation
E: MODALIAS=pci:v00008086d00009D2Fsv000017AAsd00002258bc0Csc03i30
E: PCI_CLASS=C0330
E: PCI_ID=8086:9D2F
E: PCI_SLOT_NAME=0000:00:14.0
E: PCI_SUBSYS_ID=17AA:2258
E: SUBSYSTEM=pci
A: ari_enabled=0\n
A: broken_parity_status=0\n
A: class=0x0c0330\n
H: config=86802F9D060490022130030C00008000040032E8000000000000000000000000000000000000000000000000AA175822000000007000000000000000FF010000FD01348088C60F8000000000000000005B6ECE0F000000000000000000000000306000000000000000000000000000000180C2C10800000000000000000000000500B7001803E0FE0000000000000000090014F01000400100000000C10A080000080000001800008F40020000010400010000000200000000000000000000000000000000000000000000000000000001000000020000000000000000000000000000000000000000000000000000000000000000000000B30F410800000000
A: consistent_dma_mask_bits=64\n
A: d3cold_allowed=1\n
A: dbc=disabled\n
A: dbc_bInterfaceProtocol=01\n
A: dbc_bcdDevice=0010\n
A: dbc_idProduct=0010\n
A: dbc_idVendor=1d6b\n
A: dbc_poll_interval_ms=64\n
A: device=0x9d2f\n
A: dma_mask_bits=64\n
L: driver=../../../bus/pci/drivers/xhci_hcd
A: driver_override=(null)\n
A: enable=1\n
L: firmware_node=../../LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:1d
L: iommu=../../virtual/iommu/dmar1
L: iommu_group=../../../kernel/iommu_groups/4
A: irq=128\n
A: local_cpulist=0-7\n
A: local_cpus=ff\n
A: modalias=pci:v00008086d00009D2Fsv000017AAsd00002258bc0Csc03i30\n
A: msi_bus=1\n
A: msi_irqs/128=msi\n
A: msi_irqs/129=msi\n
A: msi_irqs/130=msi\n
A: msi_irqs/131=msi\n
A: msi_irqs/132=msi\n
A: msi_irqs/133=msi\n
A: msi_irqs/134=msi\n
A: msi_irqs/135=msi\n
A: numa_node=-1\n
A: pools=poolinfo - 0.1\nbuffer-2048 0 0 2048 0\nbuffer-512 0 0 512 0\nbuffer-128 1 32 128 1\nbuffer-32 0 0 32 0\nxHCI 256 port bw ctx arrays 0 0 256 0\nxHCI 1KB stream ctx arrays 0 0 1024 0\nxHCI 256 byte stream ctx arrays 0 0 256 0\nxHCI input/output contexts 12 13 2112 13\nxHCI ring segments 45 45 4096 45\nbuffer-2048 0 0 2048 0\nbuffer-512 0 0 512 0\nbuffer-128 3 32 128 1\nbuffer-32 0 0 32 0\n
A: power/control=auto\n
A: power/runtime_active_time=8465365\n
A: power/runtime_status=active\n
A: power/runtime_suspended_time=0\n
A: power/wakeup=enabled\n
A: power/wakeup_abort_count=0\n
A: power/wakeup_active=0\n
A: power/wakeup_active_count=0\n
A: power/wakeup_count=0\n
A: power/wakeup_expire_count=0\n
A: power/wakeup_last_time_ms=0\n
A: power/wakeup_max_time_ms=0\n
A: power/wakeup_total_time_ms=0\n
A: power_state=D0\n
A: resource=0x00000000e8320000 0x00000000e832ffff 0x0000000000140204\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n
A: revision=0x21\n
A: subsystem_device=0x2258\n
A: subsystem_vendor=0x17aa\n
A: vendor=0x8086\n

View file

@ -0,0 +1,259 @@
#!/usr/bin/python3
"""
Hardware test for Validity TLS session management (Iteration 2).
Requires a real Validity/Synaptics sensor (06cb:009a or similar) that has
been paired at least once (e.g. via python-validity or Windows driver).
Run with:
sudo LD_LIBRARY_PATH=builddir/libfprint \
GI_TYPELIB_PATH=builddir/libfprint \
FP_DEVICE_EMULATION=0 \
FP_DRIVERS_ALLOWLIST=validity \
G_MESSAGES_DEBUG=all \
python3 tests/validity/test_tls_hardware.py 2>&1
The test will:
1. Enumerate and detect the validity sensor
2. Open the device (triggers: GET_VERSION, CMD19, GET_FW_INFO,
flash read, PSK derivation, flash parse, TLS handshake)
3. Report whether TLS handshake succeeded or failed
4. Close the device cleanly
"""
import os
import re
import sys
import traceback
import gi
gi.require_version('FPrint', '2.0')
from gi.repository import FPrint, GLib
# Exit with error on any exception, including in callbacks
sys.excepthook = lambda *args: (traceback.print_exception(*args), sys.exit(1))
# Ensure we're not in emulation mode
if os.environ.get('FP_DEVICE_EMULATION') == '1':
print('ERROR: FP_DEVICE_EMULATION=1 is set, this test needs real hardware')
sys.exit(1)
# Ensure running as root (USB access)
if os.geteuid() != 0:
print('WARNING: Not running as root — USB access may fail')
# Collect debug log lines for analysis
log_lines = []
original_handler = None
def log_handler(log_domain, log_level, message, user_data):
log_lines.append(message)
# Also print to stderr for real-time visibility
print(f' [{log_domain}] {message}', file=sys.stderr)
# Install log handler to capture libfprint debug output
log_flags = (GLib.LogLevelFlags.LEVEL_DEBUG |
GLib.LogLevelFlags.LEVEL_INFO |
GLib.LogLevelFlags.LEVEL_MESSAGE |
GLib.LogLevelFlags.LEVEL_WARNING |
GLib.LogLevelFlags.LEVEL_CRITICAL)
for domain in ['libfprint', 'libfprint-SSM', 'libfprint-validity',
'libfprint-device', 'libfprint-context']:
GLib.log_set_handler(domain, log_flags, log_handler, None)
print('=== Validity TLS Hardware Test ===')
print()
# Step 1: Enumerate devices
c = FPrint.Context()
c.enumerate()
devices = c.get_devices()
if len(devices) == 0:
print('FAIL: No fingerprint devices found')
sys.exit(1)
d = devices[0]
del devices
driver = d.get_driver()
print(f'Found device: driver={driver}')
if driver != 'validity':
print(f'SKIP: Expected validity driver, got {driver}')
sys.exit(77) # meson skip code
# Step 2: Open device (this triggers the full TLS flow)
print()
print('Opening device (GET_VERSION → CMD19 → FW_INFO → Flash Read → PSK → TLS handshake)...')
try:
d.open_sync()
print('Device opened successfully')
except GLib.Error as e:
print(f'FAIL: open_sync() failed: {e.message}')
sys.exit(1)
# Step 3: Analyze debug log for TLS progress
print()
print('=== TLS Progress Analysis ===')
checks = {
'fwext_loaded': False,
'fwext_not_loaded': False,
'flash_read': False,
'flash_bytes': None,
'psk_derived': False,
'psk_product': None,
'flash_cert': False,
'flash_privkey': False,
'flash_ecdh': False,
'keys_loaded': False,
'tls_started': False,
'server_hello': False,
'handshake_done': False,
'secure_session': False,
'handshake_failed': None,
'flash_parse_failed': None,
'no_fwext_skip': False,
}
for line in log_lines:
if 'Firmware extension is loaded' in line:
checks['fwext_loaded'] = True
if 'Firmware extension not loaded' in line:
checks['fwext_not_loaded'] = True
if 'No firmware extension' in line:
checks['no_fwext_skip'] = True
if 'TLS flash read: got' in line:
checks['flash_read'] = True
m = re.search(r'got (\d+) bytes', line)
if m:
checks['flash_bytes'] = int(m.group(1))
if 'PSK derived from DMI' in line:
checks['psk_derived'] = True
m = re.search(r'product=(\S+)', line)
if m:
checks['psk_product'] = m.group(1)
if 'TLS flash: certificate loaded' in line:
checks['flash_cert'] = True
if 'TLS flash: private key loaded' in line:
checks['flash_privkey'] = True
if 'TLS flash: ECDH public key loaded' in line:
checks['flash_ecdh'] = True
if 'TLS flash: all keys loaded' in line:
checks['keys_loaded'] = True
if 'TLS ServerHello: cipher 0xC005' in line:
checks['server_hello'] = True
if 'TLS handshake completed' in line:
checks['handshake_done'] = True
if 'TLS session established' in line:
checks['secure_session'] = True
checks['tls_started'] = True
if 'TLS handshake failed' in line:
checks['handshake_failed'] = line
if 'TLS flash parse failed' in line:
checks['flash_parse_failed'] = line
if 'skipping TLS' in line.lower() or 'continuing without TLS' in line.lower():
pass # noted but not a hard failure for this test
# Report results
def report(label, ok, detail=''):
status = 'PASS' if ok else 'FAIL'
extra = f' ({detail})' if detail else ''
print(f' [{status}] {label}{extra}')
report('Firmware extension loaded',
checks['fwext_loaded'],
'NOT loaded' if checks['fwext_not_loaded'] else '')
report('Flash read executed',
checks['flash_read'],
f"{checks['flash_bytes']} bytes" if checks['flash_bytes'] else '')
report('PSK derived from DMI',
checks['psk_derived'],
checks['psk_product'] or '')
report('Certificate extracted from flash',
checks['flash_cert'])
report('Private key decrypted from flash',
checks['flash_privkey'])
report('ECDH public key extracted from flash',
checks['flash_ecdh'])
report('All TLS keys loaded',
checks['keys_loaded'])
report('ServerHello received (cipher 0xC005)',
checks['server_hello'])
report('TLS handshake completed',
checks['handshake_done'])
report('Secure TLS session established',
checks['secure_session'])
if checks['handshake_failed']:
print(f' [INFO] Handshake failure: {checks["handshake_failed"]}')
if checks['flash_parse_failed']:
print(f' [INFO] Flash parse failure: {checks["flash_parse_failed"]}')
# Step 4: Close device
print()
print('Closing device...')
d.close_sync()
print('Device closed successfully')
del d
del c
# Summary
print()
all_ok = (checks['fwext_loaded'] and checks['flash_read'] and
checks['psk_derived'] and checks['keys_loaded'] and
checks['handshake_done'] and checks['secure_session'])
fwext_ok_keys_fail = (checks['fwext_loaded'] and checks['flash_read'] and
not checks['keys_loaded'])
no_fwext = checks['no_fwext_skip'] or checks['fwext_not_loaded']
if all_ok:
print('=== RESULT: ALL TLS CHECKS PASSED ===')
print('TLS session established with real hardware.')
sys.exit(0)
elif no_fwext:
print('=== RESULT: FWEXT NOT LOADED ===')
print('The firmware extension is not loaded on the sensor.')
print('This is required for flash access and TLS handshake.')
print()
print('To resolve, pair the device first with python-validity:')
print(' sudo validity-sensors-firmware # download/upload firmware')
print(' sudo python3 -c "from validitysensor.init import open; open()"')
print()
print('The fwext_loaded check verified the driver correctly detects this.')
sys.exit(0) # Not a driver bug — this is expected without fwext
elif fwext_ok_keys_fail:
print('=== RESULT: PARTIAL — Flash readable but keys incomplete ===')
print('Flash read succeeded but TLS key material is missing or corrupt.')
print('Re-pairing with python-validity may fix this.')
sys.exit(1)
else:
print('=== RESULT: SOME TLS CHECKS FAILED ===')
sys.exit(1)