From a486b58c5a0482d6c38e07ca73e30da6b017182d Mon Sep 17 00:00:00 2001 From: Leonardo Francisco Date: Tue, 7 Apr 2026 17:12:50 -0400 Subject: [PATCH] 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" --- libfprint/drivers/validity/validity.c | 247 ++++++- libfprint/drivers/validity/validity.h | 58 +- libfprint/drivers/validity/validity_capture.c | 203 +++++- libfprint/drivers/validity/validity_capture.h | 7 +- libfprint/drivers/validity/validity_db.c | 2 +- libfprint/drivers/validity/validity_enroll.c | 661 ++++++++++++++++-- libfprint/drivers/validity/validity_fwext.c | 21 +- libfprint/drivers/validity/validity_pair.c | 147 +++- libfprint/drivers/validity/validity_pair.h | 7 + libfprint/drivers/validity/validity_sensor.c | 151 ++++ libfprint/drivers/validity/validity_tls.c | 236 ++++++- libfprint/drivers/validity/validity_verify.c | 47 +- libfprint/drivers/validity/vcsfw_protocol.c | 21 +- 13 files changed, 1612 insertions(+), 196 deletions(-) diff --git a/libfprint/drivers/validity/validity.c b/libfprint/drivers/validity/validity.c index 61d328c5..592b768a 100644 --- a/libfprint/drivers/validity/validity.c +++ b/libfprint/drivers/validity/validity.c @@ -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); diff --git a/libfprint/drivers/validity/validity.h b/libfprint/drivers/validity/validity.h index a8a763d8..eecca284 100644 --- a/libfprint/drivers/validity/validity.h +++ b/libfprint/drivers/validity/validity.h @@ -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; diff --git a/libfprint/drivers/validity/validity_capture.c b/libfprint/drivers/validity/validity_capture.c index fb6df854..3b0fcc2e 100644 --- a/libfprint/drivers/validity/validity_capture.c +++ b/libfprint/drivers/validity/validity_capture.c @@ -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) diff --git a/libfprint/drivers/validity/validity_capture.h b/libfprint/drivers/validity/validity_capture.h index 02cbff35..dc19e506 100644 --- a/libfprint/drivers/validity/validity_capture.h +++ b/libfprint/drivers/validity/validity_capture.h @@ -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; /* ================================================================ diff --git a/libfprint/drivers/validity/validity_db.c b/libfprint/drivers/validity/validity_db.c index 1004c519..29ca7892 100644 --- a/libfprint/drivers/validity/validity_db.c +++ b/libfprint/drivers/validity/validity_db.c @@ -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('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(' 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('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('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; diff --git a/libfprint/drivers/validity/validity_fwext.c b/libfprint/drivers/validity/validity_fwext.c index 9a499829..6e89b522 100644 --- a/libfprint/drivers/validity/validity_fwext.c +++ b/libfprint/drivers/validity/validity_fwext.c @@ -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; diff --git a/libfprint/drivers/validity/validity_pair.c b/libfprint/drivers/validity/validity_pair.c index af0144b5..8735ce39 100644 --- a/libfprint/drivers/validity/validity_pair.c +++ b/libfprint/drivers/validity/validity_pair.c @@ -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; diff --git a/libfprint/drivers/validity/validity_pair.h b/libfprint/drivers/validity/validity_pair.h index 4ed812ba..c9878b39 100644 --- a/libfprint/drivers/validity/validity_pair.h +++ b/libfprint/drivers/validity/validity_pair.h @@ -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, diff --git a/libfprint/drivers/validity/validity_sensor.c b/libfprint/drivers/validity/validity_sensor.c index 377243b2..b9029b5b 100644 --- a/libfprint/drivers/validity/validity_sensor.c +++ b/libfprint/drivers/validity/validity_sensor.c @@ -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" }, diff --git a/libfprint/drivers/validity/validity_tls.c b/libfprint/drivers/validity/validity_tls.c index 28e5e386..587a5e30 100644 --- a/libfprint/drivers/validity/validity_tls.c +++ b/libfprint/drivers/validity/validity_tls.c @@ -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); } diff --git a/libfprint/drivers/validity/validity_verify.c b/libfprint/drivers/validity/validity_verify.c index a2a835a7..ee2c1aa5 100644 --- a/libfprint/drivers/validity/validity_verify.c +++ b/libfprint/drivers/validity/validity_verify.c @@ -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 */ diff --git a/libfprint/drivers/validity/vcsfw_protocol.c b/libfprint/drivers/validity/vcsfw_protocol.c index 8fc49947..13b4697a 100644 --- a/libfprint/drivers/validity/vcsfw_protocol.c +++ b/libfprint/drivers/validity/vcsfw_protocol.c @@ -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;