validity: enrollment, verification & DB operations\n\nImplement the complete enrollment and verification flow for\nSynaptics VCSFW (Validity) fingerprint sensors:\n\n- Enrollment state machine: LED control, capture loop with\n scan_complete polling, enrollment_update_start/end cycle,\n template building across ~8-9 stages until TID is received\n- DB write phase: StgWindsor storage auto-creation (0x04b3),\n user record creation, finger record creation with proper\n write_enable/call_cleanups wrapping\n- Pre-enrollment cleanup: delete existing user records from\n sensor DB before re-enrolling (prevents 0x0526 errors)\n- Stale session cleanup: send enrollment_update_end before\n starting new enrollment to close interrupted sessions\n- Verification: match_finger command with proper response\n parsing, delete and clear_storage operations\n- Print data: FPI_PRINT_RAW with fpi-data GVariant containing\n user identity string\n- Capture fixes: TST instruction search bug (save patched_tst\n before key_line replacement), ENROLL vs IDENTIFY mode\n differences in capture command structure\n- TLS improvements: proper session state tracking, reconnect\n handling, extended response buffer management\n- Pairing: device certificate chain validation, Windows Hello\n compatible key exchange\n\nTested on 06cb:009a (Synaptics Metallica MIS Touch):\n- Fresh enrollment: completes in 7-9 stages\n- Re-enrollment: pre-cleanup deletes stale records, then enrolls\n- Verification: verify-match confirmed (3x consecutive)\n\nReference: python-validity by uunicorn"

This commit is contained in:
Leonardo Francisco 2026-04-07 17:12:50 -04:00 committed by lewohart
parent 94bbb5fa2c
commit a486b58c5a
13 changed files with 1612 additions and 196 deletions

View file

@ -166,7 +166,9 @@ err_close:
* 3) GET_FW_INFO (0x43 0x02) check if fwext loaded
* 4) Send init_hardcoded blob (per-device, via HAL)
* 5) If no fwext: send init_hardcoded_clean_slate blob
* 6) Upload firmware extension (if not loaded)
* 6) Pairing check
* 7) TLS handshake (works without fwext uses partition 1 keys)
* 8) Upload firmware extension via TLS (if not loaded)
*/
typedef enum {
@ -180,16 +182,21 @@ typedef enum {
OPEN_RECV_INIT_HARDCODED,
OPEN_SEND_INIT_CLEAN_SLATE,
OPEN_RECV_INIT_CLEAN_SLATE,
OPEN_UPLOAD_FWEXT,
OPEN_PAIR,
OPEN_TLS_READ_FLASH,
OPEN_TLS_DERIVE_PSK,
OPEN_TLS_HANDSHAKE,
OPEN_UPLOAD_FWEXT,
OPEN_SENSOR_IDENTIFY,
OPEN_SENSOR_IDENTIFY_RECV,
OPEN_SENSOR_FACTORY_BITS,
OPEN_SENSOR_FACTORY_BITS_RECV,
OPEN_CAPTURE_SETUP,
OPEN_CALIBRATE_BUILD,
OPEN_CALIBRATE_SEND,
OPEN_CALIBRATE_SEND_RECV,
OPEN_CALIBRATE_READ_DATA,
OPEN_CALIBRATE_LOOP,
OPEN_DONE,
OPEN_NUM_STATES,
} ValidityOpenSsmState;
@ -296,21 +303,29 @@ pair_ssm_done (FpiSsm *ssm,
if (error)
{
/* Check if the pairing caused a reboot — same pattern as fwext upload */
if (g_error_matches (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_REMOVED))
/* After reboot, USB transfers fail — this is expected */
if (self->pair_state.reboot_pending)
{
fp_info ("Device rebooting after pairing (USB error expected)");
g_clear_error (&error);
}
else if (g_error_matches (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_REMOVED))
{
fp_info ("Device rebooting after pairing");
fpi_ssm_mark_failed (self->open_ssm, error);
return;
}
fp_warn ("Pairing failed: %s — continuing (device may not work)",
error->message);
g_clear_error (&error);
else
{
fp_warn ("Pairing failed: %s — continuing (device may not work)",
error->message);
g_clear_error (&error);
}
}
/* Check if pairing caused a reboot (PAIR_REBOOT_RECV was reached) */
if (self->pair_state.priv_blob != NULL)
/* Check if pairing caused a reboot */
if (self->pair_state.priv_blob != NULL ||
self->pair_state.reboot_pending)
{
/* Pairing was performed and device is rebooting.
* Signal to fprintd to retry the open. */
@ -345,6 +360,29 @@ tls_handshake_ssm_done (FpiSsm *ssm,
fpi_ssm_next_state (self->open_ssm);
}
/* Callback for calibration EP 0x82 bulk read — saves data for processing */
static void
calib_bulk_read_cb (FpiUsbTransfer *transfer,
FpDevice *dev,
gpointer user_data,
GError *error)
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev);
if (error)
{
fpi_ssm_mark_failed (transfer->ssm, error);
return;
}
/* Save the raw calibration data for processing in the next state */
g_clear_pointer (&self->bulk_data, g_free);
self->bulk_data = g_memdup2 (transfer->buffer, transfer->actual_length);
self->bulk_data_len = transfer->actual_length;
fpi_ssm_next_state (transfer->ssm);
}
static void
open_run_state (FpiSsm *ssm,
FpDevice *dev)
@ -534,7 +572,17 @@ open_run_state (FpiSsm *ssm,
return;
}
fp_info ("Firmware extension not loaded — starting upload");
/* Fwext upload requires a TLS session (flash writes need TLS).
* If TLS handshake failed/skipped, we can't upload. */
if (!self->tls.secure_rx)
{
fp_warn ("No TLS session — cannot upload firmware extension "
"(device may need pairing first)");
fpi_ssm_jump_to_state (ssm, OPEN_DONE);
return;
}
fp_info ("Firmware extension not loaded — starting upload via TLS");
self->open_ssm = ssm;
FpiSsm *fwext_ssm = validity_fwext_upload_ssm_new (dev);
@ -554,14 +602,6 @@ open_run_state (FpiSsm *ssm,
return;
}
/* Without fwext, no flash commands work */
if (!self->fwext_loaded)
{
fp_info ("No firmware extension — skipping pairing check");
fpi_ssm_next_state (ssm);
return;
}
fp_info ("Starting pairing check…");
validity_pair_state_init (&self->pair_state);
self->open_ssm = ssm;
@ -582,17 +622,8 @@ open_run_state (FpiSsm *ssm,
return;
}
/* Without fwext, flash partition isn't accessible */
if (!self->fwext_loaded)
{
fp_info ("No firmware extension — skipping TLS "
"(device needs pairing or fwext upload)");
fpi_ssm_jump_to_state (ssm, OPEN_DONE);
return;
}
/* Read flash partition 1 to get TLS keys.
* Uses standalone SSM (not subsm) so failure is non-fatal. */
* TLS works independently of fwext (partition 2). */
self->open_ssm = ssm;
FpiSsm *flash_ssm = fpi_ssm_new (dev,
validity_tls_flash_read_run_state,
@ -699,6 +730,14 @@ open_run_state (FpiSsm *ssm,
return;
}
{
GString *hex = g_string_new ("identify_sensor raw: ");
for (gsize i = 0; i < self->cmd_response_len; i++)
g_string_append_printf (hex, "%02x ", self->cmd_response_data[i]);
fp_dbg ("%s", hex->str);
g_string_free (hex, TRUE);
}
if (!validity_sensor_parse_identify (self->cmd_response_data,
self->cmd_response_len,
&self->sensor.ident))
@ -835,6 +874,156 @@ open_run_state (FpiSsm *ssm,
}
break;
case OPEN_CALIBRATE_BUILD:
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev);
/* Run calibration captures to establish sensor finger-detect baseline.
* PY: sensor.calibrate() 3 iterations of CALIBRATE capture.
* Without this, chunk 0x26 (Finger Detect) always triggers. */
if (!self->sensor.type_info ||
self->capture.bytes_per_line == 0)
{
fp_info ("No capture state — skipping calibration");
fpi_ssm_jump_to_state (ssm, OPEN_DONE);
return;
}
self->calib_iteration = 0;
g_clear_pointer (&self->capture.calib_data, g_free);
self->capture.calib_data_len = 0;
fp_info ("Starting sensor calibration (%u iterations)",
self->capture.calibration_iterations);
fpi_ssm_next_state (ssm);
}
break;
case OPEN_CALIBRATE_SEND:
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev);
gsize cmd_len;
guint8 *cmd;
cmd = validity_capture_build_cmd_02 (&self->capture,
self->sensor.type_info,
VALIDITY_CAPTURE_CALIBRATE,
&cmd_len);
if (!cmd)
{
fp_warn ("Failed to build calibration capture command");
fpi_ssm_jump_to_state (ssm, OPEN_DONE);
return;
}
fp_dbg ("Calibration iteration %u/%u",
self->calib_iteration + 1,
self->capture.calibration_iterations);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case OPEN_CALIBRATE_SEND_RECV:
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev);
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("Calibration capture failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_jump_to_state (ssm, OPEN_DONE);
return;
}
/* Read calibration data from EP 0x82.
* PY: usb.read_82() reads all bulk data from the sensor. */
{
gsize expected_size = (gsize)(self->capture.calibration_frames *
self->capture.lines_per_frame + 1) *
self->capture.bytes_per_line;
FpiUsbTransfer *xfer = fpi_usb_transfer_new (dev);
fp_dbg ("Reading calibration data: %zu bytes from EP 0x82",
expected_size);
xfer->ssm = ssm;
fpi_usb_transfer_fill_bulk (xfer, VALIDITY_EP_DATA_IN,
expected_size);
fpi_usb_transfer_submit (xfer, 5000, NULL,
calib_bulk_read_cb, NULL);
}
}
break;
case OPEN_CALIBRATE_READ_DATA:
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev);
if (self->bulk_data && self->bulk_data_len > 0)
{
/* Average the raw calibration frames */
gsize averaged_len = 0;
guint8 *averaged = validity_capture_average_frames (
self->bulk_data,
self->bulk_data_len,
self->capture.lines_per_frame,
self->capture.bytes_per_line,
self->sensor.type_info->lines_per_calibration_data,
self->capture.calibration_frames,
&averaged_len);
if (averaged && averaged_len > 0)
{
/* Process calibration: scale and accumulate into calib_data */
validity_capture_process_calibration (
&self->capture.calib_data,
&self->capture.calib_data_len,
averaged,
averaged_len,
self->capture.bytes_per_line);
fp_dbg ("Calibration iteration %u complete: "
"averaged %zu bytes, calib_data %zu bytes",
self->calib_iteration + 1,
averaged_len,
self->capture.calib_data_len);
g_free (averaged);
}
else
{
fp_dbg ("Calibration iteration %u: averaging failed",
self->calib_iteration + 1);
g_free (averaged);
}
g_clear_pointer (&self->bulk_data, g_free);
self->bulk_data_len = 0;
}
else
{
fp_dbg ("Calibration iteration %u: no bulk data",
self->calib_iteration + 1);
}
fpi_ssm_next_state (ssm);
}
break;
case OPEN_CALIBRATE_LOOP:
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev);
self->calib_iteration++;
if (self->calib_iteration < self->capture.calibration_iterations)
fpi_ssm_jump_to_state (ssm, OPEN_CALIBRATE_SEND);
else
{
fp_info ("Sensor calibration complete");
fpi_ssm_next_state (ssm);
}
}
break;
case OPEN_DONE:
/* All init commands sent. Mark open complete. */
fpi_ssm_mark_completed (ssm);

View file

@ -105,37 +105,75 @@ typedef enum {
CALIB_NUM_STATES,
} ValidityCalibState;
/* Enrollment SSM states */
/* Enrollment SSM states — matches python-validity sensor.py Sensor.enroll() */
typedef enum {
ENROLL_START = 0,
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_DB_WRITE_ENABLE,
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,
ENROLL_APPEND_IMAGE, /* 1st enrollment_update (trigger) */
ENROLL_APPEND_IMAGE_RECV,
ENROLL_CLEANUPS,
ENROLL_CLEANUPS, /* PY: call_cleanups() after 1st enrollment_update */
ENROLL_CLEANUPS_RECV,
ENROLL_UPDATE_END,
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_LED_OFF,
ENROLL_WAIT_FINGER_INT,
ENROLL_LED_OFF, /* PY: glow_end_scan() — LAST step per PY */
ENROLL_LED_OFF_RECV,
ENROLL_DONE,
ENROLL_NUM_STATES,
@ -150,6 +188,10 @@ typedef enum {
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,
@ -232,6 +274,8 @@ struct _FpiDeviceValidity
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;

View file

@ -665,6 +665,12 @@ build_line_update_type1 (const ValidityCaptureState *capture,
GArray *lines_arr;
gsize cnt = 2; /* line counter starts at 2 per python-validity */
/* Save the patched TST (before key_line replacement) for instruction
* searches later. PY searches the original patched tst variable, not
* the chunk data that has the key_line prepended. */
g_autofree guint8 *patched_tst = NULL;
gsize patched_tst_len = 0;
/* Copy input chunks, patching timeslot table in-place */
chunks_arr = g_array_new (FALSE, TRUE, sizeof (ValidityCaptureChunk));
@ -686,6 +692,11 @@ build_line_update_type1 (const ValidityCaptureState *capture,
capture->factory_calibration_values_len,
capture->key_calibration_line);
/* Save the patched TST before key_line replacement.
* Instruction searches must use this, not the key_line-modified data. */
patched_tst = g_memdup2 (c.data, c.size);
patched_tst_len = c.size;
/* Prepend key line to the timeslot table.
* In type 1: c[1] = get_key_line() + tst[line_width:] */
{
@ -782,6 +793,40 @@ build_line_update_type1 (const ValidityCaptureState *capture,
};
g_array_append_val (chunks_arr, ir);
}
else if (mode == VALIDITY_CAPTURE_ENROLL_IDENTIFY)
{
/* Hybrid: IDENTIFY chunk (0x4e) for reliable completion + ENROLL
* image reconstruction for proper enrollment data processing.
* Works around sensors where chunk 0x26 triggers false finger
* detection from ambient capacitance. */
static const guint8 wtf_data[] = {
0xfb, 0xb2, 0x0f, 0x00, 0x00, 0x00, 0x0f, 0x00,
0x30, 0x00, 0x00, 0x00, 0x87, 0x00, 0x02, 0x00,
0x67, 0x00, 0x0a, 0x00, 0x01, 0x80, 0x00, 0x00,
0x0a, 0x02, 0x00, 0x00, 0x0b, 0x19, 0x00, 0x00,
0x88, 0x13, 0xb8, 0x0b, 0x01, 0x09, 0x10, 0x00,
};
ValidityCaptureChunk fd = {
.type = CAPT_CHUNK_WTF,
.size = sizeof (wtf_data),
.data = g_memdup2 (wtf_data, sizeof (wtf_data)),
};
g_array_append_val (chunks_arr, fd);
/* Image Reconstruction — ENROLL mode (byte 4 = 0x23) */
static const guint8 recon_enroll[] = {
0x02, 0x00, 0x18, 0x00, 0x23, 0x00, 0x00, 0x00,
0x70, 0x00, 0x70, 0x00, 0x4d, 0x01, 0x00, 0x00,
0xa0, 0x00, 0x8c, 0x00, 0x3c, 0x32, 0x32, 0x1e,
0x3c, 0x0a, 0x02, 0x02,
};
ValidityCaptureChunk ir = {
.type = CAPT_CHUNK_IMAGE_RECON,
.size = sizeof (recon_enroll),
.data = g_memdup2 (recon_enroll, sizeof (recon_enroll)),
};
g_array_append_val (chunks_arr, ir);
}
/* CALIBRATE mode: no Finger Detect or Image Reconstruction */
/* --- Interleave --- */
@ -800,25 +845,13 @@ build_line_update_type1 (const ValidityCaptureState *capture,
* Build line entries from calibration data for the timeslot table. */
lines_arr = g_array_new (FALSE, TRUE, sizeof (LineEntry));
/* We need the patched timeslot table for instruction searches */
{
const guint8 *tst_data = NULL;
gsize tst_len = 0;
/* Find the Timeslot Table 2D chunk in our patched chunks */
for (gsize i = 0; i < chunks_arr->len; i++)
{
ValidityCaptureChunk *ch = &g_array_index (chunks_arr, ValidityCaptureChunk, i);
if (ch->type == CAPT_CHUNK_TIMESLOT_2D)
{
tst_data = ch->data;
tst_len = ch->size;
break;
}
}
if (tst_data && tst_len > 0)
{
/* We need the patched timeslot table (before key_line replacement)
* for instruction searches see PY's line_update_type_1 which uses
* the 'tst' variable, not 'c[1]' (which has key_line prepended). */
if (patched_tst && patched_tst_len > 0)
{
const guint8 *tst_data = patched_tst;
gsize tst_len = patched_tst_len;
/* Line 0: calibration blob at Enable Rx position */
{
gssize pc = validity_capture_find_nth_insn (tst_data, tst_len,
@ -895,8 +928,7 @@ build_line_update_type1 (const ValidityCaptureState *capture,
g_array_append_val (lines_arr, le);
}
}
}
}
}
/* Align all line data to 4-byte boundary */
for (gsize i = 0; i < lines_arr->len; i++)
@ -1043,6 +1075,11 @@ validity_capture_build_cmd_02 (const ValidityCaptureState *capture,
if (!patched)
return NULL;
/* Debug: log chunk types and sizes */
for (gsize i = 0; i < n_patched; i++)
fp_dbg ("cmd_02 chunk[%zu]: type=0x%02x size=%zu",
i, patched[i].type, patched[i].size);
/* Merge chunks back to binary */
merged = validity_capture_merge_chunks (patched, n_patched, &merged_len);
validity_capture_chunks_free (patched, n_patched);
@ -1065,6 +1102,17 @@ validity_capture_build_cmd_02 (const ValidityCaptureState *capture,
memcpy (cmd + 5, merged, merged_len);
g_free (merged);
/* Debug: dump first 200 bytes of capture command for comparison with PY */
{
GString *hex = g_string_new ("");
gsize dump_len = MIN (*out_len, 200);
for (gsize i = 0; i < dump_len; i++)
g_string_append_printf (hex, "%02x", cmd[i]);
fp_dbg ("cmd_02 mode=%d len=%zu first %zu bytes: %s",
mode, *out_len, dump_len, hex->str);
g_string_free (hex, TRUE);
}
return cmd;
}
@ -1476,11 +1524,11 @@ validity_subtype_to_finger (guint16 subtype)
static const guint8 glow_start_data[] = {
0x39, 0x20, 0xbf, 0x02, 0x00, 0xff, 0xff, 0x00,
0x00, 0x01, 0x99, 0x00, 0x20, 0x00, 0x00, 0x00,
0x00, 0x00, 0x99, 0x99, 0x00, 0x00, 0x00, 0x00,
0x00, 0x99, 0x99, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff,
0x00, 0x00, 0x00, 0x99, 0x00, 0x20, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00,
0x00, 0x00, 0x99, 0x00, 0x20, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@ -1489,17 +1537,17 @@ static const guint8 glow_start_data[] = {
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,
};
static const guint8 glow_end_data[] = {
0x39, 0xf4, 0x01, 0x00, 0x00, 0xf4, 0x01, 0x00,
0x00, 0x01, 0xff, 0x00, 0x20, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf4, 0x01,
0x00, 0x00, 0x00, 0xff, 0x00, 0x20, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xf4, 0x01, 0x00,
0x00, 0x00, 0xff, 0x00, 0x20, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@ -1508,7 +1556,7 @@ static const guint8 glow_end_data[] = {
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,
};
const guint8 *
@ -1582,6 +1630,96 @@ static const guint8 capture_prog_type1_b5[] = {
0x00, 0x00, 0x00, 0x00,
};
/* Device-specific capture program for sensor type 0x0199 (57K0 family).
* From python-validity SensorCaptureProg entry: major=6, dev_type=0x199,
* a0=0x18, a1=0x19, 2 blobs totalling 648 bytes. */
static const guint8 capture_prog_type1_0199[] = {
/* Blob 0: 228 bytes */
0x23, 0x00, 0x00, 0x00, 0x20, 0x00, 0x08, 0x00,
0x00, 0x20, 0x00, 0x80, 0x00, 0x00, 0x01, 0x00,
0x32, 0x00, 0x74, 0x00, 0x00, 0x00, 0x00, 0x80,
0x20, 0x20, 0x04, 0x00, 0x24, 0x20, 0x00, 0x00,
0x50, 0x20, 0x77, 0x36, 0x28, 0x20, 0x01, 0x00,
0x30, 0x20, 0x01, 0x00, 0x3c, 0x20, 0x80, 0x00,
0x08, 0x21, 0x38, 0x00, 0x0c, 0x21, 0x00, 0x00,
0x48, 0x21, 0x07, 0x00, 0x4c, 0x21, 0x00, 0x00,
0x58, 0x20, 0x00, 0x00, 0x5c, 0x20, 0x00, 0x00,
0x60, 0x20, 0x00, 0x00, 0x68, 0x20, 0x05, 0x00,
0x6c, 0x20, 0x01, 0x49, 0x70, 0x20, 0x01, 0x41,
0x74, 0x20, 0x01, 0x88, 0x78, 0x20, 0x01, 0x80,
0x84, 0x20, 0x20, 0x00, 0x94, 0x20, 0x01, 0x80,
0x9c, 0x20, 0x09, 0x02, 0xa0, 0x20, 0x0b, 0x19,
0xb4, 0x20, 0x03, 0x00, 0xb8, 0x20, 0x3b, 0x04,
0xbc, 0x20, 0x14, 0x00, 0xc0, 0x20, 0x02, 0x00,
0xc4, 0x20, 0x01, 0x00, 0xc8, 0x20, 0x02, 0x00,
0x33, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x80,
0xcc, 0x20, 0x00, 0x00, 0xf5, 0x03, 0xd0, 0x20,
0x00, 0x00, 0xa1, 0x01, 0x32, 0x00, 0x44, 0x00,
0x00, 0x00, 0x00, 0x80, 0xdc, 0x20, 0xe8, 0x03,
0xe0, 0x20, 0x64, 0x01, 0xe4, 0x20, 0xd0, 0x02,
0xe8, 0x20, 0x00, 0x01, 0xf0, 0x20, 0x05, 0x00,
0xf8, 0x20, 0x05, 0x00, 0xfc, 0x20, 0x00, 0x00,
0xb8, 0x20, 0x3a, 0x00, 0x00, 0x08, 0x04, 0x00,
0x14, 0x08, 0x00, 0x00, 0x08, 0x08, 0x00, 0x00,
0x08, 0x08, 0x00, 0x00, 0x14, 0x08, 0x30, 0x00,
0x08, 0x08, 0x00, 0x00, 0x14, 0x08, 0x31, 0x00,
0x1c, 0x08, 0x1a, 0x00,
/* Blob 1: 420 bytes */
0x32, 0x00, 0x0c, 0x00,
0x00, 0x00, 0x00, 0x80, 0x50, 0x11, 0x01, 0x00,
0x4c, 0x11, 0x1e, 0x00, 0x34, 0x00, 0x78, 0x01,
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, 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, 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,
0x10, 0x22, 0x17, 0x10, 0x22, 0x17, 0x10, 0x22,
0x16, 0x10, 0x22, 0x16, 0x10, 0x22, 0x16, 0x01,
0x06, 0x50, 0x10, 0x25, 0x01, 0x01, 0x00, 0x00,
0x07, 0xc8, 0x07, 0x8c, 0x06, 0xff, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x4f, 0x80, 0x00, 0x6d,
0x03, 0x00, 0x28, 0x03, 0x07, 0x03, 0x09, 0x90,
0x09, 0x8d, 0xb0, 0x0b, 0x90, 0x88, 0x09, 0x91,
0x85, 0x8e, 0x08, 0xc1, 0x81, 0x0b, 0x91, 0x90,
0x91, 0x0a, 0xc1, 0xb8, 0x92, 0x8a, 0x09, 0x93,
0x87, 0x8a, 0x89, 0x0b, 0x93, 0x88, 0x89, 0x89,
0x08, 0xc8, 0x81, 0x91, 0x89, 0x0a, 0xc8, 0x88,
0x92, 0x89, 0x09, 0x9a, 0x81, 0x8a, 0x89, 0x0b,
0x9a, 0x88, 0x89, 0x89, 0x08, 0xd0, 0x81, 0x91,
0x89, 0x0a, 0xd0, 0x88, 0x92, 0x89, 0x08, 0x02,
0x81, 0x8a, 0x09, 0x5a, 0x81, 0x0a, 0x02, 0x88,
0x89, 0x0b, 0x5a, 0x88, 0x08, 0xd9, 0x81, 0x89,
0x89, 0x0a, 0xd9, 0x90, 0x89, 0x89, 0x09, 0x5e,
0x82, 0x89, 0x89, 0x0b, 0x5e, 0x88, 0x89, 0x89,
0x08, 0xe1, 0x81, 0x89, 0x89, 0x0a, 0xe1, 0x90,
0x89, 0x89, 0x09, 0x64, 0x82, 0x89, 0x89, 0x0b,
0x64, 0x88, 0x89, 0x09, 0x6e, 0x81, 0x08, 0xe9,
0x81, 0x89, 0x0b, 0x6e, 0x88, 0x0a, 0xe9, 0x90,
0x91, 0xb9, 0x09, 0x6f, 0x82, 0x8a, 0x8f, 0x0b,
0x6f, 0x88, 0x91, 0x89, 0x08, 0xf0, 0x81, 0x8a,
0x89, 0x0a, 0xf0, 0x90, 0x89, 0x89, 0x09, 0x76,
0x82, 0x89, 0x91, 0x0b, 0x76, 0xb8, 0x91, 0x8a,
0x08, 0xf8, 0x87, 0x92, 0x91, 0x0a, 0xf8, 0x88,
0x8a, 0x92, 0x09, 0x7c, 0x81, 0x89, 0x8a, 0x0b,
0x7c, 0x09, 0x01, 0x80, 0x89, 0x89, 0x0b, 0x01,
0x88, 0x89, 0x91, 0x09, 0x7f, 0x81, 0x89, 0x92,
0x0b, 0x7f, 0x09, 0x08, 0x80, 0x89, 0x92, 0x0b,
0x08, 0x88, 0x89, 0x92, 0x0c, 0x07, 0x03, 0x03,
0x07, 0x20, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00,
0x2f, 0x00, 0x04, 0x00, 0x70, 0x00, 0x00, 0x00,
0x29, 0x00, 0x04, 0x00, 0x70, 0x00, 0x00, 0x00,
0x35, 0x00, 0x04, 0x00, 0x80, 0x00, 0x00, 0x00,
};
/* Device types that use line_update_type_1 */
static const guint16 line_update_type1_devices[] = {
0x00B5, 0x0885, 0x00B3, 0x143B, 0x1055,
@ -1601,6 +1739,13 @@ validity_capture_prog_lookup (guint8 rom_major,
* type-1 devices with 0x78 bytes/line geometry. */
if (rom_major == 6)
{
/* Device-specific programs take priority */
if (dev_type == 0x0199)
{
*out_len = sizeof (capture_prog_type1_0199);
return capture_prog_type1_0199;
}
for (gsize i = 0; i < G_N_ELEMENTS (line_update_type1_devices); i++)
{
if (line_update_type1_devices[i] == dev_type)

View file

@ -34,9 +34,10 @@
* Values match python-validity CaptureMode enum.
* ================================================================ */
typedef enum {
VALIDITY_CAPTURE_CALIBRATE = 1,
VALIDITY_CAPTURE_IDENTIFY = 2,
VALIDITY_CAPTURE_ENROLL = 3,
VALIDITY_CAPTURE_CALIBRATE = 1,
VALIDITY_CAPTURE_IDENTIFY = 2,
VALIDITY_CAPTURE_ENROLL = 3,
VALIDITY_CAPTURE_ENROLL_IDENTIFY = 4, /* IDENTIFY chunk (0x4e) + ENROLL recon (0x23) */
} ValidityCaptureMode;
/* ================================================================

View file

@ -264,7 +264,7 @@ validity_db_build_cmd_create_enrollment (gboolean start,
}
/* cmd 0x68: Enrollment update start
* Format: 0x68 | key(4LE) | 0(4LE) */
* PY format: pack('<BLL', 0x68, key, 0) 9 bytes: 0x68, key(4LE), 0(4LE) */
guint8 *
validity_db_build_cmd_enrollment_update_start (guint32 key,
gsize *out_len)

View file

@ -66,6 +66,30 @@ interrupt_cb (FpiUsbTransfer *transfer,
g_error_free (error);
return;
}
/* Scan-complete timeout — finger was likely stale (no fresh placement).
* Stop capture, signal user to lift, then retry. */
if (g_error_matches (error, G_USB_DEVICE_ERROR, G_USB_DEVICE_ERROR_TIMED_OUT) &&
target_state == 3)
{
g_error_free (error);
self->scan_incomplete_count++;
fp_info ("Scan incomplete (attempt %u) — asking user to retry",
self->scan_incomplete_count);
if (self->scan_incomplete_count > 3)
{
fp_warn ("Too many scan retries, giving up");
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_GENERAL));
return;
}
fpi_device_enroll_progress (
FP_DEVICE (self), self->enroll_stage, NULL,
fpi_device_retry_new (FP_DEVICE_RETRY_REMOVE_FINGER));
/* Skip get_prg_status (only valid after complete scan) — go
* straight to capture_stop LED off delay LED on retry */
fpi_ssm_jump_to_state (ssm, ENROLL_CAPTURE_STOP);
return;
}
fpi_ssm_mark_failed (ssm, error);
return;
}
@ -79,12 +103,25 @@ interrupt_cb (FpiUsbTransfer *transfer,
int_type = transfer->buffer[0];
fp_dbg ("Interrupt: type=0x%02x (len=%" G_GSSIZE_FORMAT ")",
int_type, transfer->actual_length);
if (transfer->actual_length >= 5)
fp_dbg ("Interrupt: type=0x%02x bytes=[%02x %02x %02x %02x %02x] (len=%" G_GSSIZE_FORMAT ")",
int_type, transfer->buffer[0], transfer->buffer[1],
transfer->buffer[2], transfer->buffer[3], transfer->buffer[4],
transfer->actual_length);
else
fp_dbg ("Interrupt: type=0x%02x (len=%" G_GSSIZE_FORMAT ")",
int_type, transfer->actual_length);
/* Check if this is the interrupt we're waiting for */
if (int_type == (guint8) target_state)
{
/* Finger-down detected */
if (int_type == 2)
{
fp_info ("Finger detected on sensor");
fpi_device_report_finger_status_changes (
FP_DEVICE (self), FP_FINGER_STATUS_PRESENT, FP_FINGER_STATUS_NEEDED);
}
/* Check scan-complete bit if waiting for type 3 */
if (int_type == 3 && transfer->actual_length >= 3)
{
@ -93,6 +130,8 @@ interrupt_cb (FpiUsbTransfer *transfer,
/* Not scan complete yet, keep waiting */
goto read_again;
}
/* Scan fully complete — reset retry counter */
self->scan_incomplete_count = 0;
}
fpi_ssm_next_state (ssm);
return;
@ -110,7 +149,9 @@ read_again:
FpiUsbTransfer *new_transfer = fpi_usb_transfer_new (device);
fpi_usb_transfer_fill_interrupt (new_transfer, VALIDITY_EP_INT_IN,
VALIDITY_USB_INT_DATA_SIZE);
fpi_usb_transfer_submit (new_transfer, VALIDITY_USB_TIMEOUT,
/* 30s timeout for scan_complete; unlimited for finger-down */
fpi_usb_transfer_submit (new_transfer,
(target_state == 3) ? 30000 : 0,
self->interrupt_cancellable,
interrupt_cb, ssm);
}
@ -130,11 +171,39 @@ start_interrupt_wait (FpiDeviceValidity *self,
transfer = fpi_usb_transfer_new (FP_DEVICE (self));
fpi_usb_transfer_fill_interrupt (transfer, VALIDITY_EP_INT_IN,
VALIDITY_USB_INT_DATA_SIZE);
fpi_usb_transfer_submit (transfer, VALIDITY_USB_TIMEOUT,
fpi_usb_transfer_submit (transfer, 0,
self->interrupt_cancellable,
interrupt_cb, ssm);
}
/* Simple interrupt callback — accepts any interrupt and advances SSM.
* Used between the two enrollment_update calls where PY does usb.wait_int(). */
static void
update_interrupt_cb (FpiUsbTransfer *transfer,
FpDevice *device,
gpointer user_data,
GError *error)
{
FpiSsm *ssm = user_data;
if (error)
{
if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_REMOVED));
else
fpi_ssm_mark_failed (ssm, error);
g_clear_error (&error);
return;
}
if (transfer->actual_length >= 1)
fp_dbg ("Update interrupt: type=0x%02x (len=%" G_GSSIZE_FORMAT ")",
transfer->buffer[0], transfer->actual_length);
fpi_ssm_next_state (ssm);
}
/* ================================================================
* Enrollment response parsing
*
@ -159,15 +228,30 @@ parse_enrollment_update_response (const guint8 *data,
EnrollmentUpdateResult *result)
{
gsize pos = 0;
guint16 declared_len;
memset (result, 0, sizeof (*result));
/* First 2 bytes are a length field (PY: l, = unpack('<H', res[:2])) */
if (data_len < 2)
return FALSE;
declared_len = FP_READ_UINT16_LE (data);
pos = 2;
if (declared_len != data_len - 2)
fp_warn ("enrollment_update: declared len %u != actual %zu",
declared_len, data_len - 2);
while (pos + 4 <= data_len)
{
guint16 tag = FP_READ_UINT16_LE (&data[pos]);
guint16 len = FP_READ_UINT16_LE (&data[pos + 2]);
gsize block_size = ENROLLMENT_MAGIC_LEN + len;
fp_dbg ("enrollment_update: tag=%u len=%u block_size=%zu pos=%zu",
tag, len, block_size, pos);
if (pos + block_size > data_len)
break;
@ -214,16 +298,105 @@ enroll_run_state (FpiSsm *ssm,
switch (fpi_ssm_get_cur_state (ssm))
{
case ENROLL_CLEANUP_STALE:
{
/* Close any stale enrollment session before starting fresh.
* Firmware may have leftover state from a previous session
* (e.g. if enrollment was interrupted). Send cmd 0x69 flag=0
* (enrollment_update_end) errors are expected and ignored. */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_create_enrollment (FALSE, &cmd_len);
self->enroll_key = 0;
self->enroll_stage = 0;
self->scan_incomplete_count = 0;
g_clear_pointer (&self->enroll_template, g_free);
self->enroll_template_len = 0;
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case ENROLL_CLEANUP_STALE_RECV:
/* Ignore status — no active session is fine (0x0405, etc.) */
fpi_ssm_next_state (ssm);
break;
case ENROLL_PRE_GET_STORAGE:
{
/* Check for existing user records that would cause 0x0526 */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_get_user_storage (
VALIDITY_STORAGE_NAME, &cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case ENROLL_PRE_GET_STORAGE_RECV:
{
validity_user_storage_clear (&self->list_storage);
if (self->cmd_response_status != VCSFW_STATUS_OK ||
!self->cmd_response_data ||
!validity_db_parse_user_storage (self->cmd_response_data,
self->cmd_response_len,
&self->list_storage))
{
/* No storage or parse error — skip cleanup, go to enrollment */
fpi_ssm_jump_to_state (ssm, ENROLL_START);
return;
}
if (self->list_storage.user_count == 0)
{
fp_dbg ("No existing users — skipping pre-enrollment cleanup");
fpi_ssm_jump_to_state (ssm, ENROLL_START);
return;
}
fp_info ("Pre-enrollment cleanup: deleting %u existing user(s)",
self->list_storage.user_count);
self->list_user_idx = 0;
fpi_ssm_next_state (ssm);
}
break;
case ENROLL_PRE_DEL_USER:
{
if (self->list_user_idx >= self->list_storage.user_count)
{
fp_info ("Pre-enrollment cleanup done");
validity_user_storage_clear (&self->list_storage);
fpi_ssm_jump_to_state (ssm, ENROLL_START);
return;
}
guint16 user_dbid = self->list_storage.user_dbids[self->list_user_idx];
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_del_record (user_dbid, &cmd_len);
fp_info ("Deleting user record dbid=%u", user_dbid);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case ENROLL_PRE_DEL_USER_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
fp_warn ("Pre-enrollment del_record(dbid=%u) failed: status=0x%04x",
self->list_storage.user_dbids[self->list_user_idx],
self->cmd_response_status);
self->list_user_idx++;
fpi_ssm_jump_to_state (ssm, ENROLL_PRE_DEL_USER);
}
break;
case ENROLL_START:
{
/* cmd 0x69 flag=1: create enrollment session */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_create_enrollment (TRUE, &cmd_len);
self->enroll_key = 0;
self->enroll_stage = 0;
g_clear_pointer (&self->enroll_template, g_free);
self->enroll_template_len = 0;
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
@ -252,7 +425,18 @@ enroll_run_state (FpiSsm *ssm,
break;
case ENROLL_LED_ON_RECV:
/* Glow start doesn't need status check (best effort) */
/* LED is on — signal that we need a finger.
* PY sends capture IMMEDIATELY after glow_start_scan(), no delay.
* The ENROLL finger detect (chunk 0x26) needs to see the transition
* from no-finger to finger-down to establish a proper baseline.
* A delay would mean the finger is already on the sensor. */
fpi_device_report_finger_status_changes (
dev, FP_FINGER_STATUS_NEEDED, FP_FINGER_STATUS_NONE);
fpi_ssm_next_state (ssm);
break;
case ENROLL_WAIT_FINGER_DELAY:
/* Pass-through (no delay needed — capture waits for finger via interrupts) */
fpi_ssm_next_state (ssm);
break;
@ -303,8 +487,55 @@ enroll_run_state (FpiSsm *ssm,
break;
case ENROLL_WAIT_SCAN_COMPLETE:
/* Wait for interrupt type 3 with scan_complete bit */
start_interrupt_wait (self, ssm, 3);
{
/* Wait for interrupt type 3 with scan_complete bit.
* Use 30-second timeout: enroll mode scans need proper finger contact. */
FpiUsbTransfer *transfer;
fpi_ssm_set_data (ssm, GINT_TO_POINTER (3), NULL);
transfer = fpi_usb_transfer_new (FP_DEVICE (self));
fpi_usb_transfer_fill_interrupt (transfer, VALIDITY_EP_INT_IN,
VALIDITY_USB_INT_DATA_SIZE);
fpi_usb_transfer_submit (transfer, 30000,
self->interrupt_cancellable,
interrupt_cb, ssm);
}
break;
case ENROLL_GET_PRG_STATUS:
{
/* cmd 0x51: get_prg_status2 (after scan complete, before capture stop) */
const guint8 cmd[] = { 0x51, 0x00, 0x20, 0x00, 0x00 };
vcsfw_tls_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
case ENROLL_GET_PRG_STATUS_RECV:
/* Status doesn't matter, just advance */
fpi_ssm_next_state (ssm);
break;
case ENROLL_CAPTURE_STOP:
{
/* cmd 0x04: capture stop/cleanup */
const guint8 cmd[] = { 0x04 };
vcsfw_tls_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
case ENROLL_CAPTURE_STOP_RECV:
/* PY: no glow_end after capture — LED stays on. */
if (self->scan_incomplete_count > 0)
{
/* Incomplete scan: retry after a brief delay.
* glow_start at the top of the loop will reinitialize.
* PY: in the except block, just retries the whole loop. */
fpi_ssm_jump_to_state_delayed (ssm, ENROLL_LED_ON, 3000);
}
else
{
/* Good scan — proceed to enrollment_update_start */
fpi_ssm_next_state (ssm);
}
break;
case ENROLL_UPDATE_START:
@ -337,9 +568,21 @@ enroll_run_state (FpiSsm *ssm,
}
break;
case ENROLL_WAIT_UPDATE_START_INT:
{
/* PY: usb.wait_int() inside enrollment_update_start() */
FpiUsbTransfer *transfer = fpi_usb_transfer_new (dev);
fpi_usb_transfer_fill_interrupt (transfer, VALIDITY_EP_INT_IN,
VALIDITY_USB_INT_DATA_SIZE);
fpi_usb_transfer_submit (transfer, 0,
self->interrupt_cancellable,
update_interrupt_cb, ssm);
}
break;
case ENROLL_DB_WRITE_ENABLE:
{
/* Send db_write_enable blob before enrollment_update */
/* PY: write_enable() before 1st enrollment_update */
gsize blob_len;
const guint8 *blob = validity_db_get_write_enable_blob (self->dev_type, &blob_len);
vcsfw_tls_cmd_send (self, ssm, blob, blob_len, NULL);
@ -349,13 +592,8 @@ enroll_run_state (FpiSsm *ssm,
case ENROLL_DB_WRITE_ENABLE_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("db_write_enable failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
fp_warn ("db_write_enable (1st) failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_next_state (ssm);
}
break;
@ -373,54 +611,26 @@ enroll_run_state (FpiSsm *ssm,
case ENROLL_APPEND_IMAGE_RECV:
{
/* First enrollment_update call — just triggers firmware processing.
* Response is status=OK with len=0; no data to parse here.
* The actual result comes from the second call after the interrupt. */
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("enrollment_update failed: status=0x%04x",
fp_warn ("enrollment_update (trigger) non-OK: status=0x%04x — skip to update_end",
self->cmd_response_status);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
/* Don't fail — firmware may be rejecting this iteration.
* Skip remaining enrollment_update states and go to UPDATE_END,
* which will proceed to LOOP_CHECK and retry or exit. */
fpi_ssm_jump_to_state (ssm, ENROLL_UPDATE_END);
return;
}
/* Parse the enrollment update response */
if (self->cmd_response_data && self->cmd_response_len > 0)
{
EnrollmentUpdateResult result;
if (parse_enrollment_update_response (self->cmd_response_data,
self->cmd_response_len,
&result))
{
/* Update template for next iteration */
g_clear_pointer (&self->enroll_template, g_free);
if (result.template_data)
{
self->enroll_template = g_steal_pointer (&result.template_data);
self->enroll_template_len = result.template_len;
}
/* If tid is present, enrollment is complete */
if (result.tid)
{
/* Store tid for finger creation */
/* tid stays in enroll_template context — we'll
* build finger data in the commit phase */
g_clear_pointer (&self->bulk_data, g_free);
self->bulk_data = g_steal_pointer (&result.tid);
self->bulk_data_len = result.tid_len;
}
enrollment_update_result_clear (&result);
}
}
fpi_ssm_next_state (ssm);
}
break;
case ENROLL_CLEANUPS:
{
/* cmd 0x1a: call_cleanups after db_write_enable + enrollment_update */
/* PY: call_cleanups() in finally block of enrollment_update (1st) */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_call_cleanups (&cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
@ -433,17 +643,137 @@ enroll_run_state (FpiSsm *ssm,
/* Status 0x0491 = nothing to commit, which is OK */
if (self->cmd_response_status != VCSFW_STATUS_OK &&
self->cmd_response_status != 0x0491)
fp_warn ("call_cleanups (1st) failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_next_state (ssm);
}
break;
case ENROLL_WAIT_UPDATE_INT:
{
/* PY: usb.wait_int() — wait for firmware to finish processing
* the enrollment image before reading the result. */
FpiUsbTransfer *transfer = fpi_usb_transfer_new (dev);
fpi_usb_transfer_fill_interrupt (transfer, VALIDITY_EP_INT_IN,
VALIDITY_USB_INT_DATA_SIZE);
fpi_usb_transfer_submit (transfer, 0,
self->interrupt_cancellable,
update_interrupt_cb, ssm);
}
break;
case ENROLL_DB_WRITE_ENABLE_READ:
{
/* PY: write_enable() before 2nd enrollment_update */
gsize blob_len;
const guint8 *blob = validity_db_get_write_enable_blob (self->dev_type, &blob_len);
vcsfw_tls_cmd_send (self, ssm, blob, blob_len, NULL);
}
break;
case ENROLL_DB_WRITE_ENABLE_READ_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
fp_warn ("db_write_enable (2nd) failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_next_state (ssm);
}
break;
case ENROLL_APPEND_IMAGE_READ:
{
/* Second cmd 0x6B: enrollment_update — reads the actual result
* with template/header/tid data. Same payload as the first call. */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_enrollment_update (
self->enroll_template, self->enroll_template_len, &cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case ENROLL_APPEND_IMAGE_READ_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("call_cleanups failed: status=0x%04x",
fp_warn ("enrollment_update (read) failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
/* Parse the enrollment update response for template/header/tid */
if (self->cmd_response_data && self->cmd_response_len > 0)
{
EnrollmentUpdateResult result;
fp_info ("enrollment_update read response: len=%zu",
self->cmd_response_len);
if (parse_enrollment_update_response (self->cmd_response_data,
self->cmd_response_len,
&result))
{
/* Update template for next iteration */
g_clear_pointer (&self->enroll_template, g_free);
if (result.template_data)
{
self->enroll_template = g_steal_pointer (&result.template_data);
self->enroll_template_len = result.template_len;
fp_info (" template: %zu bytes", self->enroll_template_len);
}
if (result.header)
fp_info (" header: %zu bytes", result.header_len);
/* If tid is present, enrollment is complete */
if (result.tid)
{
fp_info (" tid: %zu bytes — enrollment complete!",
result.tid_len);
g_clear_pointer (&self->bulk_data, g_free);
self->bulk_data = g_steal_pointer (&result.tid);
self->bulk_data_len = result.tid_len;
}
enrollment_update_result_clear (&result);
}
}
else
{
fp_info ("enrollment_update read response: EMPTY (len=0)");
}
fpi_ssm_next_state (ssm);
}
break;
case ENROLL_CLEANUPS_READ:
{
/* PY: call_cleanups() in finally block of enrollment_update (2nd) */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_call_cleanups (&cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case ENROLL_CLEANUPS_READ_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK &&
self->cmd_response_status != 0x0491)
fp_warn ("call_cleanups (2nd) failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_next_state (ssm);
}
break;
case ENROLL_UPDATE_END:
{
/* cmd 0x69 flag=0: enrollment_update_end */
/* PY: enrollment_update_end() → pack('<BL', 0x69, 0)
* Same as create_enrollment(FALSE) signals end of this iteration's
* update cycle. Called in PY's finally block for each iteration. */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_create_enrollment (FALSE, &cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
@ -459,24 +789,28 @@ enroll_run_state (FpiSsm *ssm,
{
self->enroll_stage++;
/* Report progress */
fpi_device_enroll_progress (dev, self->enroll_stage, NULL, NULL);
/* Report progress (capped at nr_enroll_stages for the UI) */
if (self->enroll_stage <= VALIDITY_ENROLL_STAGES)
fpi_device_enroll_progress (dev, self->enroll_stage, NULL, NULL);
fp_info ("Enrollment stage %u/%u", self->enroll_stage,
VALIDITY_ENROLL_STAGES);
/* If we have a TID, enrollment is complete — go to DB commit */
/* If we have a TID, enrollment is complete.
* PY calls enrollment_update_end twice: once in the finally
* block (ENROLL_UPDATE_END) and once more after the loop. */
if (self->bulk_data && self->bulk_data_len > 0)
{
fpi_ssm_jump_to_state (ssm, ENROLL_DB_WRITE_ENABLE2);
fpi_ssm_jump_to_state (ssm, ENROLL_UPDATE_END2);
return;
}
/* If we reached max stages without TID, that's an error */
if (self->enroll_stage >= VALIDITY_ENROLL_STAGES)
/* PY loops indefinitely until TID appears. Use a generous
* upper bound to avoid an infinite loop on broken firmware. */
if (self->enroll_stage >= 30)
{
fp_warn ("Enrollment did not complete within %u stages",
VALIDITY_ENROLL_STAGES);
self->enroll_stage);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_GENERAL));
return;
@ -487,6 +821,133 @@ enroll_run_state (FpiSsm *ssm,
}
break;
case ENROLL_UPDATE_END2:
{
/* PY: second enrollment_update_end() after the loop.
* Same command: pack('<BL', 0x69, 0) */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_create_enrollment (FALSE, &cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case ENROLL_UPDATE_END2_RECV:
{
/* Ignore status — PY doesn't check it for the second call either */
fpi_ssm_next_state (ssm);
}
break;
case ENROLL_GET_STORAGE:
{
/* Discover the StgWindsor storage dbid — PY uses
* db.get_user_storage(name='StgWindsor').dbid for all record ops. */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_get_user_storage (
VALIDITY_STORAGE_NAME, &cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case ENROLL_GET_STORAGE_RECV:
{
/* PY: parse_user_storage returns None on 0x04b3 (not found).
* init_db then creates the storage. We handle both cases. */
if (self->cmd_response_status == 0x04b3)
{
fp_info ("StgWindsor storage not found — creating it");
fpi_ssm_next_state (ssm); /* → ENROLL_INIT_STORAGE_WE */
return;
}
ValidityUserStorage stg = { 0 };
if (self->cmd_response_status != VCSFW_STATUS_OK ||
!self->cmd_response_data ||
!validity_db_parse_user_storage (self->cmd_response_data,
self->cmd_response_len, &stg))
{
fp_warn ("get_user_storage failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
self->enroll_storage_dbid = stg.dbid;
fp_info ("Storage dbid: %u", stg.dbid);
validity_user_storage_clear (&stg);
/* Skip storage creation states — jump to DB_WRITE_ENABLE2 */
fpi_ssm_jump_to_state (ssm, ENROLL_DB_WRITE_ENABLE2);
}
break;
case ENROLL_INIT_STORAGE_WE:
{
/* PY: db.new_user_storate() → new_record(1, 4, 3, 'StgWindsor\0')
* First: db_write_enable */
gsize blob_len;
const guint8 *blob = validity_db_get_write_enable_blob (self->dev_type, &blob_len);
vcsfw_tls_cmd_send (self, ssm, blob, blob_len, NULL);
}
break;
case ENROLL_INIT_STORAGE_WE_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
fp_warn ("db_write_enable (init_storage) failed: 0x%04x",
self->cmd_response_status);
fpi_ssm_next_state (ssm);
}
break;
case ENROLL_INIT_STORAGE_CREATE:
{
/* PY: db.new_record(1, 4, 3, b'StgWindsor\0')
* parent=1 (root), type=4 (storage), storage=3, data=name */
const gchar *name = VALIDITY_STORAGE_NAME;
gsize name_len = strlen (name) + 1; /* include NUL */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_new_record (
1, 4, 3, (const guint8 *) name, name_len, &cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case ENROLL_INIT_STORAGE_CREATE_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("create storage failed: status=0x%04x",
self->cmd_response_status);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
fp_info ("StgWindsor storage created successfully");
fpi_ssm_next_state (ssm);
}
break;
case ENROLL_INIT_STORAGE_CLEAN:
{
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_call_cleanups (&cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case ENROLL_INIT_STORAGE_CLEAN_RECV:
{
/* Now retry get_user_storage to get the dbid */
fpi_ssm_jump_to_state (ssm, ENROLL_GET_STORAGE);
}
break;
case ENROLL_DB_WRITE_ENABLE2:
{
/* Enable DB writes for storing the finger record */
@ -549,9 +1010,9 @@ enroll_run_state (FpiSsm *ssm,
/* cmd 0x47: new_record(parent=storage_dbid, type=5=user, storage=storage_dbid, data=identity) */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_new_record (
3, /* root storage dbid (standard for StgWindsor) */
self->enroll_storage_dbid,
VALIDITY_DB_RECORD_TYPE_USER,
3, /* storage */
self->enroll_storage_dbid,
identity, identity_len,
&cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
@ -592,6 +1053,43 @@ enroll_run_state (FpiSsm *ssm,
}
break;
case ENROLL_CREATE_USER_CLEANUPS:
{
/* PY: new_record always calls call_cleanups in finally block */
gsize cmd_len;
guint8 *cmd = validity_db_build_cmd_call_cleanups (&cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
g_free (cmd);
}
break;
case ENROLL_CREATE_USER_CLEANUPS_RECV:
fpi_ssm_next_state (ssm);
break;
case ENROLL_DB_WRITE_ENABLE3:
{
/* PY: new_record calls db_write_enable before each cmd 0x47 */
gsize blob_len;
const guint8 *blob = validity_db_get_write_enable_blob (self->dev_type, &blob_len);
vcsfw_tls_cmd_send (self, ssm, blob, blob_len, NULL);
}
break;
case ENROLL_DB_WRITE_ENABLE3_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("db_write_enable3 failed: 0x%04x",
self->cmd_response_status);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
return;
}
fpi_ssm_next_state (ssm);
}
break;
case ENROLL_CREATE_FINGER:
{
FpPrint *print = NULL;
@ -617,7 +1115,7 @@ enroll_run_state (FpiSsm *ssm,
guint8 *cmd = validity_db_build_cmd_new_record (
user_dbid,
0x0b, /* finger type: becomes 0x06 after db_write_enable */
3, /* storage */
self->enroll_storage_dbid,
finger_data, finger_data_len,
&cmd_len);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
@ -661,6 +1159,18 @@ enroll_run_state (FpiSsm *ssm,
fpi_ssm_next_state (ssm);
break;
case ENROLL_WAIT_FINGER_INT:
{
/* PY: usb.wait_int() after new_finger/cleanups — accept any interrupt */
FpiUsbTransfer *transfer = fpi_usb_transfer_new (dev);
fpi_usb_transfer_fill_interrupt (transfer, VALIDITY_EP_INT_IN,
VALIDITY_USB_INT_DATA_SIZE);
fpi_usb_transfer_submit (transfer, 5000,
self->interrupt_cancellable,
update_interrupt_cb, ssm);
}
break;
case ENROLL_LED_OFF:
{
gsize cmd_len;
@ -706,16 +1216,27 @@ enroll_ssm_done (FpiSsm *ssm,
fpi_print_set_type (print, FPI_PRINT_RAW);
fpi_print_set_device_stored (print, TRUE);
/* Store the user ID as driver data for later verify/identify */
/* Store the user ID as driver data for later verify/identify.
* The RAW data GVariant is required for serialization. */
GVariant *user_id_var = g_object_get_data (G_OBJECT (print),
"validity-user-id");
if (user_id_var)
{
const gchar *uid = g_variant_get_string (user_id_var, NULL);
GVariant *data = g_variant_new_string (uid);
g_object_set (print, "fpi-data", data, NULL);
GDate *date = g_date_new ();
g_date_set_time_t (date, time (NULL));
fp_print_set_enroll_date (print, date);
g_date_free (date);
}
else
{
/* Fallback: store an empty marker */
GVariant *data = g_variant_new_string ("");
g_object_set (print, "fpi-data", data, NULL);
}
g_clear_pointer (&self->enroll_template, g_free);
self->enroll_template_len = 0;

View file

@ -349,10 +349,13 @@ validity_fwext_get_db_write_enable (guint16 vid,
*
* 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_cmd_send(self, ssm, ..., NULL) with a
* 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 */
@ -390,7 +393,7 @@ validity_fwext_upload_run_state (FpiSsm *ssm,
validity_fwext_build_write_hw_reg32 (FWEXT_HW_REG_WRITE_ADDR,
FWEXT_HW_REG_WRITE_VALUE,
cmd, &cmd_len);
vcsfw_cmd_send (self, ssm, cmd, cmd_len, NULL);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
}
break;
@ -413,7 +416,7 @@ validity_fwext_upload_run_state (FpiSsm *ssm,
validity_fwext_build_read_hw_reg32 (FWEXT_HW_REG_READ_ADDR,
cmd, &cmd_len);
vcsfw_cmd_send (self, ssm, cmd, cmd_len, NULL);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
}
break;
@ -501,7 +504,7 @@ validity_fwext_upload_run_state (FpiSsm *ssm,
return;
}
vcsfw_cmd_send (self, ssm, dbe, dbe_len, NULL);
vcsfw_tls_cmd_send (self, ssm, dbe, dbe_len, NULL);
}
break;
@ -539,7 +542,7 @@ validity_fwext_upload_run_state (FpiSsm *ssm,
ud->write_offset += chunk_size;
vcsfw_cmd_send (self, ssm, cmd, cmd_len, NULL);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
}
break;
@ -560,7 +563,7 @@ validity_fwext_upload_run_state (FpiSsm *ssm,
{
guint8 cmd[] = { VCSFW_CMD_CLEANUP };
vcsfw_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
vcsfw_tls_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
@ -602,7 +605,7 @@ validity_fwext_upload_run_state (FpiSsm *ssm,
fp_info ("FWEXT: Writing firmware signature (%d bytes)",
FWEXT_SIGNATURE_SIZE);
vcsfw_cmd_send (self, ssm, cmd, cmd_len, NULL);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
}
break;
@ -623,7 +626,7 @@ validity_fwext_upload_run_state (FpiSsm *ssm,
{
guint8 cmd[] = { VCSFW_CMD_GET_FW_INFO, FWEXT_PARTITION };
vcsfw_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
vcsfw_tls_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
@ -658,7 +661,7 @@ validity_fwext_upload_run_state (FpiSsm *ssm,
fp_info ("FWEXT: Rebooting sensor to activate new firmware");
vcsfw_cmd_send (self, ssm, cmd, cmd_len, NULL);
vcsfw_tls_cmd_send (self, ssm, cmd, cmd_len, NULL);
}
break;

View file

@ -663,14 +663,102 @@ validity_pair_run_state (FpiSsm *ssm,
if (ps->num_partitions > 0)
{
fp_info ("Flash has %u partitions — pairing not needed",
fp_info ("Flash has %u partitions — verifying TLS keys",
ps->num_partitions);
fpi_ssm_jump_to_state (ssm, PAIR_DONE);
/* Read flash partition 1 to check if TLS keys exist.
* If they do, pairing is complete. If not, we re-pair. */
fpi_ssm_next_state (ssm);
return;
}
fp_info ("Flash has 0 partitions — device needs pairing");
/* Look up device descriptor */
ps->dev_desc = validity_hal_device_lookup (self->dev_type);
if (!ps->dev_desc)
{
fp_warn ("No HAL descriptor for dev_type=%u", self->dev_type);
fpi_ssm_mark_failed (ssm,
fpi_device_error_new (FP_DEVICE_ERROR_NOT_SUPPORTED));
return;
}
/* No partitions — skip TLS verify, go straight to pairing */
fpi_ssm_jump_to_state (ssm, PAIR_SEND_RESET_BLOB);
}
break;
case PAIR_VERIFY_TLS_SEND:
{
/* Read flash partition 1 (TLS cert store) to verify keys exist */
guint8 cmd[13];
cmd[0] = VCSFW_CMD_READ_FLASH;
cmd[1] = 0x01; /* partition */
cmd[2] = 0x01; /* access flag */
FP_WRITE_UINT16_LE (&cmd[3], 0x0000);
FP_WRITE_UINT32_LE (&cmd[5], 0x0000);
FP_WRITE_UINT32_LE (&cmd[9], 0x1000);
vcsfw_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
case PAIR_VERIFY_TLS_RECV:
{
/* Check if TLS flash has valid key data */
gboolean have_keys = FALSE;
if (self->cmd_response_status == VCSFW_STATUS_OK &&
self->cmd_response_data && self->cmd_response_len > 6)
{
guint32 flash_sz = FP_READ_UINT32_LE (self->cmd_response_data);
const guint8 *flash_data = self->cmd_response_data + 6;
gsize flash_avail = self->cmd_response_len - 6;
if (flash_sz > flash_avail)
flash_sz = flash_avail;
/* Quick check: scan for block IDs 3 (cert), 4 (privkey), 6 (ecdh) */
const guint8 *pos = flash_data;
gsize remaining = flash_sz;
gboolean found_priv = FALSE, found_ecdh = FALSE, found_cert = FALSE;
while (remaining >= 36) /* header(4) + hash(32) */
{
guint16 block_id = FP_READ_UINT16_LE (pos);
guint16 block_size = FP_READ_UINT16_LE (pos + 2);
if (block_id == 0xFFFF)
break;
pos += 36; /* skip header + hash */
remaining -= 36;
if (block_size > remaining)
break;
if (block_id == 4)
found_priv = TRUE;
if (block_id == 6)
found_ecdh = TRUE;
if (block_id == 3)
found_cert = TRUE;
pos += block_size;
remaining -= block_size;
}
have_keys = found_priv && found_ecdh && found_cert;
}
if (have_keys)
{
fp_info ("TLS keys verified on flash — pairing not needed");
fpi_ssm_jump_to_state (ssm, PAIR_DONE);
return;
}
fp_info ("TLS keys missing from flash — starting pairing");
/* Look up device descriptor */
ps->dev_desc = validity_hal_device_lookup (self->dev_type);
if (!ps->dev_desc)
@ -788,6 +876,16 @@ validity_pair_run_state (FpiSsm *ssm,
case PAIR_PARTITION_FLASH_RECV:
{
if (self->cmd_response_status == 0x0404)
{
/* 0x0404 = partitions already exist (half-initialized device).
* Factory reset will wipe flash, then reboot. Next device open
* will start with a clean slate and full pairing will succeed. */
fp_info ("Flash already partitioned (0x0404) — factory reset needed");
fpi_ssm_next_state (ssm);
return;
}
if (self->cmd_response_status != VCSFW_STATUS_OK)
{
fp_warn ("partition_flash failed: status=0x%04x",
@ -810,7 +908,32 @@ validity_pair_run_state (FpiSsm *ssm,
}
}
fpi_ssm_next_state (ssm);
/* Skip factory reset states — go straight to CMD50 */
fpi_ssm_jump_to_state (ssm, PAIR_CMD50_SEND);
}
break;
case PAIR_FACTORY_RESET_SEND:
{
/* CMD 0x10 + 0x61 zero bytes: wipes flash partition table.
* python-validity: usb.cmd(b'\x10' + b'\0' * 0x61) */
guint8 cmd[98];
memset (cmd, 0, sizeof (cmd));
cmd[0] = 0x10;
vcsfw_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
case PAIR_FACTORY_RESET_RECV:
{
if (self->cmd_response_status != VCSFW_STATUS_OK)
fp_warn ("Factory reset cmd 0x10 status=0x%04x",
self->cmd_response_status);
else
fp_info ("Factory reset complete — rebooting sensor");
/* Reboot; next device open will pair from clean state */
fpi_ssm_jump_to_state (ssm, PAIR_REBOOT_SEND);
}
break;
@ -963,6 +1086,12 @@ validity_pair_run_state (FpiSsm *ssm,
self->tls.tls_cert_len = ps->server_cert_len;
}
/* Set priv_key — the TLS handshake needs the actual EC private key
* (EVP_PKEY*) to sign cert_verify. We have it as ps->client_key. */
if (self->tls.priv_key)
EVP_PKEY_free (self->tls.priv_key);
self->tls.priv_key = EVP_PKEY_dup (ps->client_key);
OPENSSL_cleanse (priv_le, sizeof (priv_le));
OPENSSL_cleanse (priv_be, sizeof (priv_be));
@ -997,8 +1126,10 @@ validity_pair_run_state (FpiSsm *ssm,
case PAIR_TLS_HANDSHAKE:
{
/* Establish TLS session — python-validity: tls.open() */
self->open_ssm = ssm; /* for handshake callback */
/* Establish TLS session — python-validity: tls.open()
* Uses subsm: tls_ssm completion/failure propagates to pair SSM.
* NOTE: do NOT overwrite self->open_ssm here it must remain
* pointing to the open SSM for pair_ssm_done to work. */
FpiSsm *tls_ssm = fpi_ssm_new (dev,
validity_tls_handshake_run_state,
TLS_HS_NUM_STATES);
@ -1156,9 +1287,11 @@ validity_pair_run_state (FpiSsm *ssm,
case PAIR_REBOOT_SEND:
{
/* Reboot: 0x05 0x02 0x00 (python-validity: tls.cmd(unhex('050200'))) */
/* Reboot: 0x05 0x02 0x00 (python-validity: tls.cmd(unhex('050200')))
* Use raw USB TLS may not be established (factory reset path). */
guint8 cmd[] = { VCSFW_CMD_REBOOT, 0x02, 0x00 };
vcsfw_tls_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
ps->reboot_pending = TRUE;
vcsfw_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;

View file

@ -66,6 +66,9 @@ typedef struct
/* 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 */
@ -239,11 +242,15 @@ 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,

View file

@ -162,6 +162,157 @@ static const ValidityDeviceInfo device_info_table[] = {
{ 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" },

View file

@ -62,9 +62,9 @@ static const guint8 fw_pubkey_x[32] = {
static const guint8 fw_pubkey_y[32] = {
0x94, 0xca, 0xa6, 0x21, 0x47, 0xa8, 0x61, 0xf7,
0x8d, 0x94, 0x93, 0x23, 0x8b, 0x58, 0x3c, 0x24,
0x86, 0xa8, 0x07, 0x4d, 0xf4, 0xd5, 0x8b, 0xef,
0x6e, 0x0d, 0xc5, 0xbe, 0xb6, 0xf8, 0x38, 0xa8
0x8d, 0x94, 0x93, 0x23, 0x8b, 0xc5, 0x43, 0x62,
0x88, 0x7a, 0xd0, 0xf4, 0xd5, 0x8b, 0xef, 0x6e,
0x0d, 0xc5, 0xbe, 0xb6, 0xf8, 0x38, 0x55, 0xa8
};
/* ================================================================
@ -701,16 +701,37 @@ handle_priv_block (ValidityTlsState *tls,
for (gsize i = 0; i < TLS_EC_COORD_SIZE; i++)
d_be[i] = d_le[TLS_EC_COORD_SIZE - 1 - i];
{
GString *hex = g_string_new ("TLS priv d(BE): ");
for (gsize i = 0; i < TLS_EC_COORD_SIZE; i++)
g_string_append_printf (hex, "%02x", d_be[i]);
fp_dbg ("%s", hex->str);
g_string_free (hex, TRUE);
}
BIGNUM *d_bn = BN_bin2bn (d_be, TLS_EC_COORD_SIZE, NULL);
/* Derive public key Q = d * G on P-256 */
EC_GROUP *group = EC_GROUP_new_by_curve_name (NID_X9_62_prime256v1);
EC_POINT *pub_pt = EC_POINT_new (group);
EC_POINT_mul (group, pub_pt, d_bn, NULL, NULL, NULL);
guint8 pub_uncompressed[1 + 2 * TLS_EC_COORD_SIZE]; /* 0x04 || x || y */
size_t pt_len = EC_POINT_point2oct (group, pub_pt,
POINT_CONVERSION_UNCOMPRESSED,
pub_uncompressed,
sizeof (pub_uncompressed), NULL);
EC_POINT_free (pub_pt);
EC_GROUP_free (group);
EVP_PKEY *pkey = NULL;
OSSL_PARAM_BLD *bld = OSSL_PARAM_BLD_new ();
OSSL_PARAM_BLD_push_utf8_string (bld, OSSL_PKEY_PARAM_GROUP_NAME,
"prime256v1", 0);
OSSL_PARAM_BLD_push_BN (bld, OSSL_PKEY_PARAM_PRIV_KEY, d_bn);
OSSL_PARAM_BLD_push_octet_string (bld, OSSL_PKEY_PARAM_PUB_KEY,
pub_uncompressed, pt_len);
/* We need to derive the public key from d. Use EVP_PKEY_fromdata with
* just the private key OpenSSL 3.x can derive the public key. */
OSSL_PARAM *params = OSSL_PARAM_BLD_to_param (bld);
EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_from_name (NULL, "EC", NULL);
EVP_PKEY_fromdata_init (pctx);
@ -805,7 +826,7 @@ handle_ecdh_block (ValidityTlsState *tls,
return FALSE;
}
/* Note: fw_pubkey_x/y are already big-endian in our constants */
/* Note: fw_pubkey_x/y are stored as little-endian, like ECDH blob coords */
/* Verify: fwpub.verify(signature, key_blob, ECDSA(SHA256)) */
EVP_MD_CTX *md_ctx = EVP_MD_CTX_new ();
@ -964,6 +985,17 @@ hs_append_msg (GByteArray *buf, GChecksum *hash,
/* Update handshake hash */
g_checksum_update (hash, hdr, 4);
g_checksum_update (hash, body, body_len);
{
static const char *names[] = {
[0x01] = "ClientHello", [0x02] = "ServerHello",
[0x0b] = "Certificate", [0x0d] = "CertRequest",
[0x0e] = "ServerHelloDone", [0x10] = "ClientKEX",
[0x0f] = "CertVerify", [0x14] = "Finished"
};
const char *n = (type < 0x15 && names[type]) ? names[type] : "unknown";
fp_dbg ("hs_hash UPDATE %s (type=0x%02x, %zu bytes fed)", n, type, 4 + body_len);
}
}
/* Build ClientHello */
@ -1002,9 +1034,9 @@ validity_tls_build_client_hello (ValidityTlsState *tls, gsize *out_len)
};
g_byte_array_append (hello, suites, sizeof (suites));
/* compression (none) */
guint8 comp[] = { 0x01, 0x00 };
g_byte_array_append (hello, comp, 2);
/* compression (none — python-validity sends length=0, no methods) */
guint8 comp[] = { 0x00 };
g_byte_array_append (hello, comp, 1);
/* extensions */
guint8 ext_truncated_hmac[] = {
@ -1100,6 +1132,19 @@ validity_tls_parse_server_hello (ValidityTlsState *tls,
/* Update handshake hash */
g_checksum_update (tls->handshake_hash, hs_pos, 4 + hs_len);
{
static const char *names[] = {
[0x01] = "ClientHello", [0x02] = "ServerHello",
[0x0b] = "Certificate", [0x0d] = "CertRequest",
[0x0e] = "ServerHelloDone", [0x10] = "ClientKEX",
[0x0f] = "CertVerify", [0x14] = "Finished"
};
const char *n = (hs_type < 0x15 && names[hs_type]) ? names[hs_type] : "unknown";
fp_dbg ("hs_hash UPDATE(srv) %s (type=0x%02x, %u bytes fed, first4: %02x%02x%02x%02x)",
n, hs_type, (unsigned) (4 + hs_len),
hs_pos[0], hs_pos[1], hs_pos[2], hs_pos[3]);
}
switch (hs_type)
{
case TLS_HS_SERVER_HELLO:
@ -1216,8 +1261,35 @@ validity_tls_build_client_finish (ValidityTlsState *tls, gsize *out_len)
EVP_PKEY_derive (derive_ctx, NULL, &pms_len);
EVP_PKEY_derive (derive_ctx, pre_master_secret, &pms_len);
EVP_PKEY_CTX_free (derive_ctx);
{
GString *hex = g_string_new ("TLS pms: ");
for (gsize i = 0; i < pms_len; i++)
g_string_append_printf (hex, "%02x", pre_master_secret[i]);
g_string_append_printf (hex, " (len=%zu)", pms_len);
fp_dbg ("%s", hex->str);
g_string_free (hex, TRUE);
}
/* ---- Derive master_secret and key_block ---- */
{
GString *hex = g_string_new ("TLS server_random: ");
for (gsize i = 0; i < TLS_RANDOM_SIZE; i++)
g_string_append_printf (hex, "%02x", tls->server_random[i]);
fp_dbg ("%s", hex->str);
g_string_free (hex, TRUE);
}
{
GChecksum *hc = g_checksum_copy (tls->handshake_hash);
guint8 hd[32]; gsize hl = 32;
g_checksum_get_digest (hc, hd, &hl);
g_checksum_free (hc);
GString *hex = g_string_new ("TLS hash after srv: ");
for (gsize i = 0; i < 32; i++)
g_string_append_printf (hex, "%02x", hd[i]);
fp_dbg ("%s", hex->str);
g_string_free (hex, TRUE);
}
guint8 seed[2 * TLS_RANDOM_SIZE];
memcpy (seed, tls->client_random, TLS_RANDOM_SIZE);
memcpy (seed + TLS_RANDOM_SIZE, tls->server_random, TLS_RANDOM_SIZE);
@ -1243,6 +1315,17 @@ validity_tls_build_client_finish (ValidityTlsState *tls, gsize *out_len)
memcpy (tls->encryption_key, key_block + 0x40, TLS_AES_KEY_SIZE);
memcpy (tls->decryption_key, key_block + 0x60, TLS_AES_KEY_SIZE);
{
GString *hex = g_string_new ("TLS sign_key: ");
for (gsize i = 0; i < 8; i++)
g_string_append_printf (hex, "%02x", tls->sign_key[i]);
g_string_append_printf (hex, "... enc_key: ");
for (gsize i = 0; i < 8; i++)
g_string_append_printf (hex, "%02x", tls->encryption_key[i]);
fp_dbg ("%s", hex->str);
g_string_free (hex, TRUE);
}
OPENSSL_cleanse (pre_master_secret, sizeof (pre_master_secret));
OPENSSL_cleanse (key_block, sizeof (key_block));
@ -1251,6 +1334,16 @@ validity_tls_build_client_finish (ValidityTlsState *tls, gsize *out_len)
/* 1. Certificate (type 0x0B) */
{
fp_dbg ("TLS cert_len=%zu, cert first 20 bytes:", tls->tls_cert_len);
{
GString *hex = g_string_new (" cert: ");
gsize dump_len = MIN (tls->tls_cert_len, 40);
for (gsize i = 0; i < dump_len; i++)
g_string_append_printf (hex, "%02x", tls->tls_cert[i]);
fp_dbg ("%s", hex->str);
g_string_free (hex, TRUE);
}
GByteArray *cert_body = g_byte_array_new ();
guint8 cert_prefix[] = { 0xac, 0x16 };
g_byte_array_append (cert_body, cert_prefix, 2);
@ -1274,6 +1367,17 @@ validity_tls_build_client_finish (ValidityTlsState *tls, gsize *out_len)
hs_append_msg (hs_msgs, tls->handshake_hash,
TLS_HS_CERTIFICATE, wrapped2->data, wrapped2->len);
g_byte_array_free (wrapped2, TRUE);
{
GChecksum *hc = g_checksum_copy (tls->handshake_hash);
guint8 hd[32]; gsize hl = 32;
g_checksum_get_digest (hc, hd, &hl);
g_checksum_free (hc);
GString *hex = g_string_new ("TLS hash after cert: ");
for (gsize i = 0; i < 32; i++)
g_string_append_printf (hex, "%02x", hd[i]);
fp_dbg ("%s", hex->str);
g_string_free (hex, TRUE);
}
}
/* 2. ClientKeyExchange (type 0x10) */
@ -1281,19 +1385,34 @@ validity_tls_build_client_finish (ValidityTlsState *tls, gsize *out_len)
guint8 pubpoint[65]; /* 0x04 + 32 + 32 */
get_ec_pubpoint_bytes (tls->session_key, pubpoint, sizeof (pubpoint));
/* python-validity sends: 0x04 || x_le || y_le
* OpenSSL gives us: 0x04 || x_be || y_be
* We need to reverse each coordinate to little-endian */
guint8 kex_body[65];
kex_body[0] = 0x04;
for (gsize i = 0; i < 32; i++)
{
kex_body[1 + i] = pubpoint[32 - i]; /* x: reverse BE to LE */
kex_body[33 + i] = pubpoint[64 - i]; /* y: reverse BE to LE */
}
/* python-validity sends: 0x04 || to_bytes(x)[::-1] || to_bytes(y)[::-1]
* to_bytes() returns LE, [::-1] converts back to BE.
* So python-validity sends BE coordinates.
* OpenSSL gives us: 0x04 || x_be || y_be already correct! */
guint8 *kex_body = pubpoint; /* Use as-is, it's already BE */
gsize kex_body_len = 65;
{
GString *hex = g_string_new ("TLS kex_body: ");
for (gsize i = 0; i < 65; i++)
g_string_append_printf (hex, "%02x", kex_body[i]);
fp_dbg ("%s", hex->str);
g_string_free (hex, TRUE);
}
hs_append_msg (hs_msgs, tls->handshake_hash,
TLS_HS_CLIENT_KEY_EXCHANGE, kex_body, sizeof (kex_body));
TLS_HS_CLIENT_KEY_EXCHANGE, kex_body, kex_body_len);
{
GChecksum *hc = g_checksum_copy (tls->handshake_hash);
guint8 hd[32]; gsize hl = 32;
g_checksum_get_digest (hc, hd, &hl);
g_checksum_free (hc);
GString *hex = g_string_new ("TLS hash after kex: ");
for (gsize i = 0; i < 32; i++)
g_string_append_printf (hex, "%02x", hd[i]);
fp_dbg ("%s", hex->str);
g_string_free (hex, TRUE);
}
}
/* 3. CertificateVerify (type 0x0F) */
@ -1305,12 +1424,15 @@ validity_tls_build_client_finish (ValidityTlsState *tls, gsize *out_len)
g_checksum_get_digest (hash_copy, hs_hash, &hash_len);
g_checksum_free (hash_copy);
/* ECDSA sign with preshared hash (Prehashed) */
EVP_MD_CTX *md_ctx = EVP_MD_CTX_new ();
EVP_PKEY_CTX *sign_pctx = NULL;
EVP_DigestSignInit (md_ctx, &sign_pctx, NULL, NULL, tls->priv_key);
{
GString *hex = g_string_new ("TLS hs_hash for CertVerify: ");
for (gsize i = 0; i < 32; i++)
g_string_append_printf (hex, "%02x", hs_hash[i]);
fp_dbg ("%s", hex->str);
g_string_free (hex, TRUE);
}
/* We're signing a pre-hashed value, so use raw ECDSA */
/* ECDSA sign pre-hashed value */
size_t sig_len = 0;
EVP_PKEY_CTX *raw_ctx = EVP_PKEY_CTX_new (tls->priv_key, NULL);
EVP_PKEY_sign_init (raw_ctx);
@ -1319,7 +1441,15 @@ validity_tls_build_client_finish (ValidityTlsState *tls, gsize *out_len)
EVP_PKEY_sign (raw_ctx, signature, &sig_len, hs_hash, 32);
EVP_PKEY_CTX_free (raw_ctx);
EVP_MD_CTX_free (md_ctx);
/* Self-verify the CertVerify signature */
{
EVP_PKEY_CTX *vfy_ctx = EVP_PKEY_CTX_new (tls->priv_key, NULL);
EVP_PKEY_verify_init (vfy_ctx);
int vrc = EVP_PKEY_verify (vfy_ctx, signature, sig_len, hs_hash, 32);
fp_dbg ("TLS CertVerify self-verify: %s (rc=%d, sig_len=%zu)",
vrc == 1 ? "OK" : "FAILED", vrc, sig_len);
EVP_PKEY_CTX_free (vfy_ctx);
}
hs_append_msg (hs_msgs, tls->handshake_hash,
TLS_HS_CERT_VERIFY, signature, sig_len);
@ -1362,6 +1492,17 @@ validity_tls_build_client_finish (ValidityTlsState *tls, gsize *out_len)
vd_seed, vd_seed_len,
verify_data, TLS_VERIFY_DATA_SIZE);
{
GString *hex = g_string_new ("TLS Finished hs_hash: ");
for (gsize i = 0; i < 32; i++)
g_string_append_printf (hex, "%02x", hs_hash[i]);
g_string_append_printf (hex, " verify_data: ");
for (gsize i = 0; i < TLS_VERIFY_DATA_SIZE; i++)
g_string_append_printf (hex, "%02x", verify_data[i]);
fp_dbg ("%s", hex->str);
g_string_free (hex, TRUE);
}
/* Build Finished handshake message: type(1) || 3-byte-len || verify_data */
guint8 fin_msg[4 + TLS_VERIFY_DATA_SIZE];
fin_msg[0] = TLS_HS_FINISHED;
@ -1370,8 +1511,10 @@ validity_tls_build_client_finish (ValidityTlsState *tls, gsize *out_len)
fin_msg[3] = TLS_VERIFY_DATA_SIZE;
memcpy (fin_msg + 4, verify_data, TLS_VERIFY_DATA_SIZE);
/* Update handshake hash with the Finished message we're sending */
g_checksum_update (tls->handshake_hash, fin_msg, sizeof (fin_msg));
/* NOTE: Do NOT update handshake hash with client Finished.
* python-validity's make_finish() doesn't call update_neg(), and the
* device's server Finished verify_data is computed WITHOUT including
* the client Finished in the hash (non-standard TLS behavior). */
/* Encrypt Finished as handshake record */
gsize signed_len = sizeof (fin_msg) + TLS_HMAC_SIZE;
@ -1397,6 +1540,16 @@ validity_tls_build_client_finish (ValidityTlsState *tls, gsize *out_len)
g_free (encrypted);
*out_len = output->len;
/* Debug: hex dump the full client finish */
{
GString *hex = g_string_new ("TLS_CF:");
for (gsize i = 0; i < output->len; i++)
g_string_append_printf (hex, "%02x", output->data[i]);
fp_dbg ("%s", hex->str);
g_string_free (hex, TRUE);
}
return g_byte_array_free (output, FALSE);
}
@ -1515,6 +1668,20 @@ validity_tls_parse_server_finish (ValidityTlsState *tls,
sf_seed, sf_seed_len,
expected_vd, TLS_VERIFY_DATA_SIZE);
{
GString *hex = g_string_new ("TLS ServerFinished hs_hash: ");
for (gsize i = 0; i < 32; i++)
g_string_append_printf (hex, "%02x", hs_hash[i]);
g_string_append_printf (hex, " expected_vd: ");
for (gsize i = 0; i < TLS_VERIFY_DATA_SIZE; i++)
g_string_append_printf (hex, "%02x", expected_vd[i]);
g_string_append_printf (hex, " received_vd: ");
for (gsize i = 0; i < TLS_VERIFY_DATA_SIZE; i++)
g_string_append_printf (hex, "%02x", decrypted[4 + i]);
fp_dbg ("%s", hex->str);
g_string_free (hex, TRUE);
}
if (memcmp (decrypted + 4, expected_vd, TLS_VERIFY_DATA_SIZE) != 0)
{
g_free (decrypted);
@ -1626,6 +1793,19 @@ tls_raw_recv_cb (FpiUsbTransfer *transfer,
self->cmd_response_data = NULL;
fp_dbg ("TLS recv: %zu bytes", self->cmd_response_len);
/* Hex dump first bytes for debugging */
if (self->cmd_response_data && self->cmd_response_len > 0)
{
gsize dump_len = self->cmd_response_len;
g_autofree gchar *hex = g_malloc (dump_len * 3 + 1);
for (gsize i = 0; i < dump_len; i++)
g_snprintf (hex + i * 3, 4, "%02x ",
self->cmd_response_data[i]);
hex[dump_len * 3] = '\0';
fp_dbg ("TLS recv hex: %s", hex);
}
fpi_ssm_next_state (transfer->ssm);
}

View file

@ -85,13 +85,19 @@ verify_interrupt_cb (FpiUsbTransfer *transfer,
int_type = transfer->buffer[0];
fp_dbg ("Verify interrupt: type=0x%02x (len=%" G_GSSIZE_FORMAT ")",
int_type, transfer->actual_length);
if (transfer->actual_length >= 5)
fp_dbg ("Verify interrupt: type=0x%02x bytes=[%02x %02x %02x %02x %02x] (len=%" G_GSSIZE_FORMAT ")",
int_type, transfer->buffer[0], transfer->buffer[1],
transfer->buffer[2], transfer->buffer[3], transfer->buffer[4],
transfer->actual_length);
else
fp_dbg ("Verify interrupt: type=0x%02x (len=%" G_GSSIZE_FORMAT ")",
int_type, transfer->actual_length);
/* During match wait, type 3 = result available */
/* During match wait, type 3 = match found, type 5 = no match */
if (fpi_ssm_get_cur_state (ssm) == VERIFY_WAIT_MATCH_INT)
{
if (int_type == 3)
if (int_type == 3 || int_type == 5)
{
fpi_ssm_next_state (ssm);
return;
@ -135,7 +141,7 @@ read_again:
FpiUsbTransfer *new_transfer = fpi_usb_transfer_new (device);
fpi_usb_transfer_fill_interrupt (new_transfer, VALIDITY_EP_INT_IN,
VALIDITY_USB_INT_DATA_SIZE);
fpi_usb_transfer_submit (new_transfer, VALIDITY_USB_TIMEOUT,
fpi_usb_transfer_submit (new_transfer, 0,
self->interrupt_cancellable,
verify_interrupt_cb, ssm);
}
@ -150,7 +156,10 @@ verify_start_interrupt_wait (FpiDeviceValidity *self,
transfer = fpi_usb_transfer_new (FP_DEVICE (self));
fpi_usb_transfer_fill_interrupt (transfer, VALIDITY_EP_INT_IN,
VALIDITY_USB_INT_DATA_SIZE);
fpi_usb_transfer_submit (transfer, VALIDITY_USB_TIMEOUT,
/* Use no timeout (0) for finger-wait and scan-complete states,
* since these wait for physical user interaction.
* The interrupt_cancellable handles cancellation. */
fpi_usb_transfer_submit (transfer, 0,
self->interrupt_cancellable,
verify_interrupt_cb, ssm);
}
@ -370,6 +379,32 @@ verify_run_state (FpiSsm *ssm,
verify_start_interrupt_wait (self, ssm);
break;
case VERIFY_GET_PRG_STATUS:
{
/* cmd 0x51: get_prg_status2 (after scan complete, before capture stop) */
const guint8 cmd[] = { 0x51, 0x00, 0x20, 0x00, 0x00 };
vcsfw_tls_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
case VERIFY_GET_PRG_STATUS_RECV:
/* Status doesn't matter, just advance */
fpi_ssm_next_state (ssm);
break;
case VERIFY_CAPTURE_STOP:
{
/* cmd 0x04: capture stop/cleanup */
const guint8 cmd[] = { 0x04 };
vcsfw_tls_cmd_send (self, ssm, cmd, sizeof (cmd), NULL);
}
break;
case VERIFY_CAPTURE_STOP_RECV:
/* Cleanup status doesn't matter */
fpi_ssm_next_state (ssm);
break;
case VERIFY_MATCH_START:
{
/* cmd 0x5E: match_finger */

View file

@ -226,6 +226,16 @@ tls_cmd_receive_cb (FpiUsbTransfer *transfer,
}
/* 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,
@ -304,8 +314,9 @@ vcsfw_tls_cmd_run_state (FpiSsm *ssm,
cmd_data->cmd_len,
&wrapped_len);
/* Build USB payload: 0x44000000 prefix + TLS record */
gsize usb_len = 4 + 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,
@ -314,11 +325,7 @@ vcsfw_tls_cmd_run_state (FpiSsm *ssm,
transfer = fpi_usb_transfer_new (dev);
transfer->short_is_error = TRUE;
fpi_usb_transfer_fill_bulk (transfer, VALIDITY_EP_CMD_OUT, usb_len);
transfer->buffer[0] = 0x44;
transfer->buffer[1] = 0x00;
transfer->buffer[2] = 0x00;
transfer->buffer[3] = 0x00;
memcpy (transfer->buffer + 4, wrapped, wrapped_len);
memcpy (transfer->buffer, wrapped, wrapped_len);
g_free (wrapped);
transfer->ssm = ssm;