From 7460e0dec45549fa778d51c9f515294b616bc471 Mon Sep 17 00:00:00 2001 From: Joshua Grisham Date: Thu, 28 Aug 2025 21:01:54 +0200 Subject: [PATCH] sdcp-v2: Move SDCP support from using NSS to OpenSSL 3.0 and implement for egismoc --- doc/libfprint-2-sections.txt | 17 +- libfprint/drivers/egismoc/egismoc.c | 1058 +++++++----- libfprint/drivers/egismoc/egismoc.h | 59 +- libfprint/drivers/virtual-sdcp.c | 676 -------- libfprint/fp-sdcp-device-private.h | 41 +- libfprint/fp-sdcp-device.c | 79 +- libfprint/fpi-log.c | 56 + libfprint/fpi-log.h | 29 + libfprint/fpi-sdcp-device.c | 1467 ++++++----------- libfprint/fpi-sdcp-device.h | 129 +- libfprint/fpi-sdcp.c | 1053 ++++++++++++ libfprint/fpi-sdcp.h | 132 ++ libfprint/meson.build | 27 +- libfprint/sdcp/generate-gresource.py | 25 + libfprint/sdcp/meson.build | 32 + ...evices Root Certificate Authority 2017.pem | 19 + .../Windows Hello 2096ADCC CA 2021.pem | 23 + .../Windows Hello Secure Devices PCA 2018.pem | 26 + meson.build | 5 +- tests/egismoc/custom.pcapng | Bin 73752 -> 88960 bytes tests/egismoc/device | 117 +- tests/meson.build | 36 +- tests/sdcp-key-db/cert9.db | Bin 28672 -> 0 bytes tests/sdcp-key-db/key4.db | Bin 36864 -> 0 bytes tests/test-fp-sdcp-device.c | 143 ++ tests/test-fpi-sdcp.c | 162 ++ tests/test-sdcp-device-fake.c | 196 +++ tests/test-sdcp-device-fake.h | 33 + tests/test-sdcp-utils.c | 60 + tests/test-sdcp-utils.h | 102 ++ tests/virtual-sdcp.py | 144 -- 31 files changed, 3593 insertions(+), 2353 deletions(-) delete mode 100644 libfprint/drivers/virtual-sdcp.c create mode 100644 libfprint/fpi-log.c create mode 100644 libfprint/fpi-sdcp.c create mode 100644 libfprint/fpi-sdcp.h create mode 100644 libfprint/sdcp/generate-gresource.py create mode 100644 libfprint/sdcp/meson.build create mode 100644 libfprint/sdcp/truststore/Microsoft ECC Devices Root Certificate Authority 2017.pem create mode 100644 libfprint/sdcp/truststore/Windows Hello 2096ADCC CA 2021.pem create mode 100644 libfprint/sdcp/truststore/Windows Hello Secure Devices PCA 2018.pem delete mode 100644 tests/sdcp-key-db/cert9.db delete mode 100644 tests/sdcp-key-db/key4.db create mode 100644 tests/test-fp-sdcp-device.c create mode 100644 tests/test-fpi-sdcp.c create mode 100644 tests/test-sdcp-device-fake.c create mode 100644 tests/test-sdcp-device-fake.h create mode 100644 tests/test-sdcp-utils.c create mode 100644 tests/test-sdcp-utils.h delete mode 100644 tests/virtual-sdcp.py diff --git a/doc/libfprint-2-sections.txt b/doc/libfprint-2-sections.txt index b582f2c2..756ca794 100644 --- a/doc/libfprint-2-sections.txt +++ b/doc/libfprint-2-sections.txt @@ -226,19 +226,24 @@ fpi_image_device_set_bz3_threshold Internal FpSdcpDevice FpiSdcpClaim FpSdcpDeviceClass -fpi_sdcp_claim_copy -fpi_sdcp_claim_free fpi_sdcp_claim_get_type fpi_sdcp_claim_new -fpi_sdcp_device_connect_complete +fpi_sdcp_claim_copy +fpi_sdcp_claim_free +fpi_sdcp_device_open_complete fpi_sdcp_device_get_connect_data +fpi_sdcp_device_connect_complete fpi_sdcp_device_get_reconnect_data fpi_sdcp_device_reconnect_complete +fpi_sdcp_device_list_complete +fpi_sdcp_device_enroll_commit fpi_sdcp_device_enroll_commit_complete -fpi_sdcp_device_enroll_ready -fpi_sdcp_device_enroll_set_nonce +fpi_sdcp_device_get_identify_data +fpi_sdcp_device_set_identify_data fpi_sdcp_device_identify_retry fpi_sdcp_device_identify_complete +fpi_sdcp_device_get_print_id +fpi_sdcp_device_set_print_id
@@ -249,6 +254,8 @@ fp_warn fp_err BUG_ON BUG +fp_dbg_hex_dump_bytes +fp_dbg_hex_dump_gbytes
diff --git a/libfprint/drivers/egismoc/egismoc.c b/libfprint/drivers/egismoc/egismoc.c index 8f35a67f..7bf71a54 100644 --- a/libfprint/drivers/egismoc/egismoc.c +++ b/libfprint/drivers/egismoc/egismoc.c @@ -1,15 +1,10 @@ /* * Driver for Egis Technology (LighTuning) Match-On-Chip sensors - * Originally authored 2023 by Joshua Grisham + * Copyright (C) 2023-2025 Joshua Grisham * * Portions of code and logic inspired from the elanmoc libfprint driver * which is copyright (C) 2021 Elan Microelectronics Inc (see elanmoc.c) * - * Based on original reverse-engineering work by Joshua Grisham. The protocol has - * been reverse-engineered from captures of the official Windows driver, and by - * testing commands on the sensor with a multiplatform Python prototype driver: - * https://github.com/joshuagrisham/galaxy-book2-pro-linux/tree/main/fingerprint/ - * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either @@ -26,6 +21,7 @@ */ #define FP_COMPONENT "egismoc" +#include "fpi-log.h" #include #include @@ -42,19 +38,24 @@ struct _FpiDeviceEgisMoc FpiSsm *task_ssm; FpiSsm *cmd_ssm; FpiUsbTransfer *cmd_transfer; - GCancellable *interrupt_cancellable; GPtrArray *enrolled_ids; + GBytes *enrollment_nonce; gint max_enroll_stages; + FpiSsm *wait_finger_ssm; + gint64 wait_finger_start; + GCancellable *interrupt_cancellable; + gboolean dev_init_done; }; -G_DEFINE_TYPE (FpiDeviceEgisMoc, fpi_device_egismoc, FP_TYPE_DEVICE); +G_DEFINE_TYPE (FpiDeviceEgisMoc, fpi_device_egismoc, FP_TYPE_SDCP_DEVICE); static const FpIdEntry egismoc_id_table[] = { { .vid = 0x1c7a, .pid = 0x0582, .driver_data = EGISMOC_DRIVER_CHECK_PREFIX_TYPE1 }, - { .vid = 0x1c7a, .pid = 0x0583, .driver_data = EGISMOC_DRIVER_CHECK_PREFIX_TYPE1 }, + { .vid = 0x1c7a, .pid = 0x0583, .driver_data = EGISMOC_DRIVER_CHECK_PREFIX_TYPE1 | EGISMOC_DRIVER_MAX_ENROLL_STAGES_15 }, { .vid = 0x1c7a, .pid = 0x0586, .driver_data = EGISMOC_DRIVER_CHECK_PREFIX_TYPE1 | EGISMOC_DRIVER_MAX_ENROLL_STAGES_20 }, { .vid = 0x1c7a, .pid = 0x0587, .driver_data = EGISMOC_DRIVER_CHECK_PREFIX_TYPE1 | EGISMOC_DRIVER_MAX_ENROLL_STAGES_20 }, { .vid = 0x1c7a, .pid = 0x05a1, .driver_data = EGISMOC_DRIVER_CHECK_PREFIX_TYPE2 }, + { .vid = 0x1c7a, .pid = 0x05a5, .driver_data = EGISMOC_DRIVER_CHECK_PREFIX_TYPE2 | EGISMOC_DRIVER_MAX_ENROLL_STAGES_15 }, { .vid = 0, .pid = 0, .driver_data = 0 } }; @@ -74,45 +75,12 @@ typedef struct egismoc_enroll_print int stage; } EnrollPrint; -static void -egismoc_finger_on_sensor_cb (FpiUsbTransfer *transfer, - FpDevice *device, - gpointer userdata, - GError *error) +typedef struct egismoc_identify_print { - fp_dbg ("Finger on sensor callback"); - fpi_device_report_finger_status (device, FP_FINGER_STATUS_PRESENT); - - g_return_if_fail (transfer->ssm); - if (error) - fpi_ssm_mark_failed (transfer->ssm, error); - else - fpi_ssm_next_state (transfer->ssm); -} - -static void -egismoc_wait_finger_on_sensor (FpiSsm *ssm, - FpDevice *device) -{ - fp_dbg ("Wait for finger on sensor"); - FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); - - g_autoptr(FpiUsbTransfer) transfer = fpi_usb_transfer_new (device); - - fpi_usb_transfer_fill_interrupt (transfer, EGISMOC_EP_CMD_INTERRUPT_IN, - EGISMOC_USB_INTERRUPT_IN_RECV_LENGTH); - transfer->ssm = ssm; - /* Interrupt on this device always returns 1 byte short; this is expected */ - transfer->short_is_error = FALSE; - - fpi_device_report_finger_status (device, FP_FINGER_STATUS_NEEDED); - - fpi_usb_transfer_submit (g_steal_pointer (&transfer), - EGISMOC_USB_INTERRUPT_TIMEOUT, - self->interrupt_cancellable, - egismoc_finger_on_sensor_cb, - NULL); -} + GBytes *id; + GBytes *mac; + GError *error; +} IdentifyPrint; static gboolean egismoc_validate_response_prefix (const guchar *buffer_in, @@ -380,52 +348,108 @@ egismoc_exec_cmd (FpDevice *device, } static void -egismoc_set_print_data (FpPrint *print, - const gchar *device_print_id, - const gchar *user_id) -{ - GVariant *print_id_var = NULL; - GVariant *fpi_data = NULL; - g_autofree gchar *fill_user_id = NULL; - - if (user_id) - fill_user_id = g_strdup (user_id); - else - fill_user_id = g_strndup (device_print_id, EGISMOC_FINGERPRINT_DATA_SIZE); - - fpi_print_fill_from_user_id (print, fill_user_id); - - fpi_print_set_type (print, FPI_PRINT_RAW); - fpi_print_set_device_stored (print, TRUE); - - g_object_set (print, "description", fill_user_id, NULL); - - print_id_var = g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE, - device_print_id, - EGISMOC_FINGERPRINT_DATA_SIZE, - sizeof (guchar)); - fpi_data = g_variant_new ("(@ay)", print_id_var); - g_object_set (print, "fpi-data", fpi_data, NULL); -} - -static GPtrArray * -egismoc_get_enrolled_prints (FpDevice *device) +egismoc_wait_finger_ssm_done (FpiSsm *ssm, + FpDevice *device, + GError *error) { + fp_dbg ("Wait for finger SSM done"); FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); - g_autoptr(GPtrArray) result = g_ptr_array_new_with_free_func (g_object_unref); + /* wait_finger_ssm is going to be freed by completion of SSM */ + g_assert (!self->wait_finger_ssm || self->wait_finger_ssm == ssm); - if (!self->enrolled_ids) - return g_steal_pointer (&result); + self->wait_finger_ssm = NULL; + self->wait_finger_start = 0; - for (guint i = 0; i < self->enrolled_ids->len; i++) + if (error) + fpi_device_action_error (device, error); +} + +static void +egismoc_finger_on_sensor_cb (FpiUsbTransfer *transfer, + FpDevice *device, + gpointer userdata, + GError *error) +{ + fp_dbg ("Finger on sensor callback"); + + g_return_if_fail (transfer->ssm); + + if (error) { + fpi_ssm_mark_failed (transfer->ssm, error); + return; + } + + /* finger is "present" when buffer begins with "SIGE" and ends in valid suffix */ + if (memcmp (transfer->buffer, egismoc_read_prefix, 4) == 0 && + egismoc_validate_response_suffix (transfer->buffer, + transfer->actual_length, + rsp_sensor_has_finger_suffix, + rsp_sensor_has_finger_suffix_len)) { - FpPrint *print = fp_print_new (device); - egismoc_set_print_data (print, g_ptr_array_index (self->enrolled_ids, i), NULL); - g_ptr_array_add (result, g_object_ref_sink (print)); + fpi_device_report_finger_status (device, FP_FINGER_STATUS_PRESENT); + fpi_ssm_next_state (transfer->ssm); } + else + { + fpi_ssm_jump_to_state (transfer->ssm, WAIT_FINGER_NOT_ON_SENSOR); + } +} - return g_steal_pointer (&result); +static void +egismoc_wait_finger_run_state (FpiSsm *ssm, + FpDevice *device) +{ + FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); + g_autoptr(FpiUsbTransfer) transfer = NULL; + + switch (fpi_ssm_get_cur_state (ssm)) + { + case WAIT_FINGER_NOT_ON_SENSOR: + if (self->wait_finger_start + EGISMOC_FINGER_ON_SENSOR_TIMEOUT_USEC > g_get_monotonic_time ()) + { + transfer = fpi_usb_transfer_new (device); + fpi_usb_transfer_fill_interrupt (transfer, EGISMOC_EP_CMD_INTERRUPT_IN, + EGISMOC_USB_INTERRUPT_IN_RECV_LENGTH); + + transfer->ssm = ssm; + /* Interrupt on this device always returns 1 byte short; this is expected */ + transfer->short_is_error = FALSE; + + fpi_usb_transfer_submit (g_steal_pointer (&transfer), + EGISMOC_USB_INTERRUPT_TIMEOUT, + self->interrupt_cancellable, + egismoc_finger_on_sensor_cb, + NULL); + } + else + { + fpi_ssm_mark_failed (ssm, fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Timed out trying to detect " + "finger on sensor")); + } + break; + + case WAIT_FINGER_ON_SENSOR: + fpi_ssm_mark_completed (ssm); + fpi_ssm_next_state (self->task_ssm); + break; + } +} + +static void +egismoc_wait_finger_on_sensor (FpDevice *device) +{ + fp_dbg ("Wait for finger on sensor"); + FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); + + self->wait_finger_start = g_get_monotonic_time (); + + fpi_device_report_finger_status (device, FP_FINGER_STATUS_NEEDED); + + g_assert (self->wait_finger_ssm == NULL); + self->wait_finger_ssm = fpi_ssm_new (device, egismoc_wait_finger_run_state, WAIT_FINGER_STATES); + fpi_ssm_start (self->wait_finger_ssm, egismoc_wait_finger_ssm_done); } static void @@ -436,6 +460,10 @@ egismoc_list_fill_enrolled_ids_cb (FpDevice *device, { fp_dbg ("List callback"); FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); + const guint8 *data; + guchar *enrollment_id = NULL; + FpiByteReader reader; + gboolean read = TRUE; if (error) { @@ -446,32 +474,29 @@ egismoc_list_fill_enrolled_ids_cb (FpDevice *device, g_clear_pointer (&self->enrolled_ids, g_ptr_array_unref); self->enrolled_ids = g_ptr_array_new_with_free_func (g_free); - FpiByteReader reader; - gboolean read = TRUE; - fpi_byte_reader_init (&reader, buffer_in, length_in); read &= fpi_byte_reader_set_pos (&reader, EGISMOC_LIST_RESPONSE_PREFIX_SIZE); /* - * Each fingerprint ID will be returned in this response as a 32 byte array + * Each enrollment_id will be returned in this response as a 32 byte array * The other stuff in the payload is 16 bytes long, so if there is at least 1 * print then the length should be at least 16+32=48 bytes long */ while (read) { - const guint8 *data; - g_autofree gchar *print_id = NULL; - - read &= fpi_byte_reader_get_data (&reader, EGISMOC_FINGERPRINT_DATA_SIZE, - &data); + read &= fpi_byte_reader_get_data (&reader, SDCP_ENROLLMENT_ID_SIZE, &data); if (!read) break; - print_id = g_strndup ((gchar *) data, EGISMOC_FINGERPRINT_DATA_SIZE); - fp_dbg ("Device fingerprint %0d: %.*s", self->enrolled_ids->len + 1, - EGISMOC_FINGERPRINT_DATA_SIZE, print_id); - g_ptr_array_add (self->enrolled_ids, g_steal_pointer (&print_id)); + enrollment_id = g_malloc0 (SDCP_ENROLLMENT_ID_SIZE); + memcpy (enrollment_id, data, SDCP_ENROLLMENT_ID_SIZE); + + fp_dbg ("Device ID %0d:", self->enrolled_ids->len + 1); + fp_dbg_hex_dump_bytes (enrollment_id, SDCP_ENROLLMENT_ID_SIZE); + + g_ptr_array_add (self->enrolled_ids, g_steal_pointer (&enrollment_id)); + g_free (enrollment_id); } fp_info ("Number of currently enrolled fingerprints on the device is %d", @@ -485,7 +510,9 @@ static void egismoc_list_run_state (FpiSsm *ssm, FpDevice *device) { - g_autoptr(GPtrArray) enrolled_prints = NULL; + FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); + FpSdcpDevice *sdcp_device = FP_SDCP_DEVICE (device); + g_autoptr(GPtrArray) ids = NULL; switch (fpi_ssm_get_cur_state (ssm)) { @@ -495,21 +522,27 @@ egismoc_list_run_state (FpiSsm *ssm, break; case LIST_RETURN_ENROLLED_PRINTS: - enrolled_prints = egismoc_get_enrolled_prints (device); - fpi_device_list_complete (device, g_steal_pointer (&enrolled_prints), NULL); + ids = g_ptr_array_new_with_free_func ((GDestroyNotify) g_bytes_unref); + for (gint i = 0; i < self->enrolled_ids->len; i++) + { + GBytes *id = g_bytes_new (g_ptr_array_index (self->enrolled_ids, i), + SDCP_ENROLLMENT_ID_SIZE); + g_ptr_array_add (ids, g_steal_pointer (&id)); + } + fpi_sdcp_device_list_complete (sdcp_device, g_steal_pointer (&ids), NULL); fpi_ssm_next_state (ssm); break; } } static void -egismoc_list (FpDevice *device) +egismoc_list (FpSdcpDevice *sdcp_device) { fp_dbg ("List"); - FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); + FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (sdcp_device); g_assert (self->task_ssm == NULL); - self->task_ssm = fpi_ssm_new (device, + self->task_ssm = fpi_ssm_new (FP_DEVICE (sdcp_device), egismoc_list_run_state, LIST_STATES); fpi_ssm_start (self->task_ssm, egismoc_task_ssm_done); @@ -523,12 +556,7 @@ egismoc_get_delete_cmd (FpDevice *device, fp_dbg ("Get delete command"); FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); g_auto(FpiByteWriter) writer = {0}; - g_autoptr(GVariant) print_data = NULL; - g_autoptr(GVariant) print_data_id_var = NULL; - const guchar *print_data_id = NULL; - gsize print_data_id_len = 0; - g_autofree gchar *print_description = NULL; - g_autofree guchar *enrolled_print_id = NULL; + g_autoptr(GBytes) enrollment_id = NULL; g_autofree guchar *result = NULL; gboolean written = TRUE; @@ -552,11 +580,9 @@ egismoc_get_delete_cmd (FpDevice *device, else if (self->enrolled_ids) num_to_delete = self->enrolled_ids->len; - const gsize body_length = sizeof (guchar) * EGISMOC_FINGERPRINT_DATA_SIZE * - num_to_delete; + const gsize body_length = sizeof (guchar) * SDCP_ENROLLMENT_ID_SIZE * num_to_delete; /* total_length is the 6 various bytes plus prefix and body payload */ - const gsize total_length = (sizeof (guchar) * 6) + cmd_delete_prefix_len + - body_length; + const gsize total_length = (sizeof (guchar) * 6) + cmd_delete_prefix_len + body_length; /* pre-fill entire payload with 00s */ fpi_byte_writer_init_with_size (&writer, total_length, TRUE); @@ -598,32 +624,26 @@ egismoc_get_delete_cmd (FpDevice *device, written &= fpi_byte_writer_put_uint8 (&writer, num_to_delete * 0x20); } - /* append desired 32-byte fingerprint IDs */ - /* if passed a delete_print then fetch its data from the FpPrint */ + /* append desired enrollment_id(s) */ + + /* if passed a delete_print then fetch its ID from the FpPrint */ if (delete_print) { - g_object_get (delete_print, "description", &print_description, NULL); - g_object_get (delete_print, "fpi-data", &print_data, NULL); - - if (!g_variant_check_format_string (print_data, "(@ay)", FALSE)) + fpi_sdcp_device_get_print_id (delete_print, &enrollment_id); + if (!enrollment_id) { fpi_ssm_mark_failed (self->task_ssm, - fpi_device_error_new (FP_DEVICE_ERROR_DATA_INVALID)); + fpi_device_error_new_msg (FP_DEVICE_ERROR_DATA_INVALID, + "Print data missing ID")); return NULL; } - g_variant_get (print_data, "(@ay)", &print_data_id_var); - print_data_id = g_variant_get_fixed_array (print_data_id_var, - &print_data_id_len, sizeof (guchar)); + fp_dbg ("Delete enrollment ID:"); + fp_dbg_hex_dump_gbytes (enrollment_id); - if (!g_str_has_prefix (print_description, "FP")) - fp_dbg ("Fingerprint '%s' was not created by libfprint; deleting anyway.", - print_description); - - fp_info ("Delete fingerprint %s (%s)", print_description, print_data_id); - - written &= fpi_byte_writer_put_data (&writer, print_data_id, - EGISMOC_FINGERPRINT_DATA_SIZE); + written &= fpi_byte_writer_put_data (&writer, + g_bytes_get_data (enrollment_id, NULL), + g_bytes_get_size (enrollment_id)); } /* Otherwise assume this is a "clear" - just loop through and append all enrolled IDs */ else if (self->enrolled_ids) @@ -632,7 +652,7 @@ egismoc_get_delete_cmd (FpDevice *device, { written &= fpi_byte_writer_put_data (&writer, g_ptr_array_index (self->enrolled_ids, i), - EGISMOC_FINGERPRINT_DATA_SIZE); + SDCP_ENROLLMENT_ID_SIZE); } } @@ -679,7 +699,7 @@ egismoc_delete_cb (FpDevice *device, { fpi_ssm_mark_failed (self->task_ssm, fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, - "Unsupported delete action.")); + "Unsupported delete action")); } } else @@ -694,8 +714,10 @@ static void egismoc_delete_run_state (FpiSsm *ssm, FpDevice *device) { + FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); g_autofree guchar *payload = NULL; gsize payload_length = 0; + GError *error = NULL; switch (fpi_ssm_get_cur_state (ssm)) { @@ -707,10 +729,24 @@ egismoc_delete_run_state (FpiSsm *ssm, case DELETE_DELETE: if (fpi_device_get_current_action (device) == FPI_DEVICE_ACTION_DELETE) - payload = egismoc_get_delete_cmd (device, fpi_ssm_get_data (ssm), - &payload_length); + { + payload = egismoc_get_delete_cmd (device, fpi_ssm_get_data (ssm), + &payload_length); + } else - payload = egismoc_get_delete_cmd (device, NULL, &payload_length); + { + if (self->enrolled_ids->len == 0) + { + error = fpi_device_error_new_msg (FP_DEVICE_ERROR_DATA_NOT_FOUND, + "Clear attempted when there are no prints " + "currently stored on the device"); + fpi_device_delete_complete (device, error); + fpi_ssm_mark_failed (self->task_ssm, error); + return; + } + + payload = egismoc_get_delete_cmd (device, NULL, &payload_length); + } egismoc_exec_cmd (device, g_steal_pointer (&payload), payload_length, g_free, egismoc_delete_cb); @@ -749,6 +785,95 @@ egismoc_delete (FpDevice *device) fpi_ssm_start (self->task_ssm, egismoc_task_ssm_done); } +static void +egismoc_enroll_commit_complete_cb (FpDevice *device, + guchar *buffer_in, + gsize length_in, + GError *error) +{ + fp_dbg ("Enroll commit complete callback"); + FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); + FpSdcpDevice *sdcp_device = FP_SDCP_DEVICE (device); + + if (error) + { + fpi_ssm_mark_failed (self->task_ssm, error); + fpi_sdcp_device_enroll_commit_complete (sdcp_device, error); + return; + } + + fpi_sdcp_device_enroll_commit_complete (sdcp_device, NULL); + fpi_ssm_next_state (self->task_ssm); +} + +static void +egismoc_enroll_commit_cb (FpDevice *device, + guchar *buffer_in, + gsize length_in, + GError *error) +{ + fp_dbg ("Enroll commit callback"); + FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); + FpSdcpDevice *sdcp_device = FP_SDCP_DEVICE (device); + + g_clear_pointer (&self->enrollment_nonce, g_bytes_unref); + + if (error) + { + fpi_ssm_mark_failed (self->task_ssm, error); + fpi_sdcp_device_enroll_commit_complete (sdcp_device, error); + return; + } + + if (!egismoc_validate_response_suffix (buffer_in, + length_in, + rsp_commit_success_suffix, + rsp_commit_success_suffix_len)) + { + g_propagate_error (&error, fpi_device_error_new_msg (FP_DEVICE_ERROR_DATA_INVALID, + "Enrollment was rejected by the device")); + fpi_ssm_mark_failed (self->task_ssm, error); + fpi_sdcp_device_enroll_commit_complete (sdcp_device, error); + return; + } + + egismoc_exec_cmd (device, cmd_sensor_reset, cmd_sensor_reset_len, + NULL, egismoc_enroll_commit_complete_cb); +} + +static void +egismoc_enroll_commit (FpSdcpDevice *sdcp_device, + GBytes *id) +{ + fp_dbg ("Enroll commit"); + FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (sdcp_device); + g_auto(FpiByteWriter) writer = {0}; + g_autoptr(GError) error = NULL; + const guint8 *new_id; + gsize new_id_len = 0; + gsize payload_len = 0; + + fpi_byte_writer_init (&writer); + if (!fpi_byte_writer_put_data (&writer, cmd_new_print_prefix, + cmd_new_print_prefix_len)) + goto out_fail; + + new_id = g_bytes_get_data (id, &new_id_len); + + if (!fpi_byte_writer_put_data (&writer, new_id, new_id_len)) + goto out_fail; + + payload_len = fpi_byte_writer_get_size (&writer); + egismoc_exec_cmd (FP_DEVICE (self), fpi_byte_writer_reset_and_get_data (&writer), + payload_len, g_free, egismoc_enroll_commit_cb); + return; + +out_fail: + g_propagate_error (&error, fpi_device_error_new (FP_DEVICE_ERROR_PROTO)); + fpi_ssm_mark_failed (self->task_ssm, error); + fpi_sdcp_device_enroll_commit_complete (sdcp_device, error); +} + static void egismoc_enroll_status_report (FpDevice *device, EnrollPrint *enroll_print, @@ -808,11 +933,7 @@ egismoc_read_capture_cb (FpDevice *device, } /* Check that the read payload indicates "success" */ - if (egismoc_validate_response_prefix (buffer_in, - length_in, - rsp_read_success_prefix, - rsp_read_success_prefix_len) && - egismoc_validate_response_suffix (buffer_in, + if (egismoc_validate_response_suffix (buffer_in, length_in, rsp_read_success_suffix, rsp_read_success_suffix_len)) @@ -825,11 +946,7 @@ egismoc_read_capture_cb (FpDevice *device, /* If not success then the sensor can either report "off center" or "sensor is dirty" */ /* "Off center" */ - if (egismoc_validate_response_prefix (buffer_in, - length_in, - rsp_read_offcenter_prefix, - rsp_read_offcenter_prefix_len) && - egismoc_validate_response_suffix (buffer_in, + if (egismoc_validate_response_suffix (buffer_in, length_in, rsp_read_offcenter_suffix, rsp_read_offcenter_suffix_len)) @@ -858,6 +975,42 @@ egismoc_read_capture_cb (FpDevice *device, fpi_ssm_jump_to_state (self->task_ssm, ENROLL_CAPTURE_SENSOR_RESET); } +static void +egismoc_enroll_starting_cb (FpDevice *device, + guchar *buffer_in, + gsize length_in, + GError *error) +{ + fp_dbg ("Enroll starting callback"); + FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); + g_autofree gchar *enrollment_nonce_hex = NULL; + + if (error) + { + fpi_ssm_mark_failed (self->task_ssm, error); + return; + } + + if (!egismoc_validate_response_suffix (buffer_in, + length_in, + rsp_enroll_starting_suffix, + rsp_enroll_starting_suffix_len)) + { + fpi_ssm_mark_failed (self->task_ssm, + fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, + "Invalid response when starting enrollment")); + return; + } + + /* clear and fetch SDCP device enrollment nonce from response */ + g_clear_pointer (&self->enrollment_nonce, g_bytes_unref); + self->enrollment_nonce = g_bytes_new (buffer_in + + EGISMOC_ENROLL_STARTING_RESPONSE_PREFIX_SIZE, + SDCP_NONCE_SIZE); + + fpi_ssm_next_state (self->task_ssm); +} + static void egismoc_enroll_check_cb (FpDevice *device, guchar *buffer_in, @@ -909,15 +1062,14 @@ egismoc_get_check_cmd (FpDevice *device, * 3) Hard-coded prefix (cmd_check_prefix) * 4) 2-byte size indiciator, 20*Number of enrolled identifiers without plus 9 * ((enrolled_ids->len + 1) * 0x20) - * 5) Hard-coded 32 * 0x00 bytes + * 5) SDCP Identify nonce (always hard-coded 32 * 0x00 bytes on these devices) * 6) All of the currently registered prints in their 32-byte device identifiers * (enrolled_list) * 7) Hard-coded suffix (cmd_check_suffix) */ g_assert (self->enrolled_ids); - const gsize body_length = sizeof (guchar) * self->enrolled_ids->len * - EGISMOC_FINGERPRINT_DATA_SIZE; + const gsize body_length = sizeof (guchar) * self->enrolled_ids->len * SDCP_ENROLLMENT_ID_SIZE; /* prefix length can depend on the type */ const gsize check_prefix_length = (fpi_device_get_driver_data (device) & @@ -929,7 +1081,7 @@ egismoc_get_check_cmd (FpDevice *device, * the body payload */ const gsize total_length = (sizeof (guchar) * 6) + check_prefix_length - + EGISMOC_CMD_CHECK_SEPARATOR_LENGTH + + SDCP_NONCE_SIZE + body_length + cmd_check_suffix_len; @@ -983,15 +1135,15 @@ egismoc_get_check_cmd (FpDevice *device, (self->enrolled_ids->len + 1) * 0x20); } - /* add 00s "separator" to offset position */ - written &= fpi_byte_writer_change_pos (&writer, - EGISMOC_CMD_CHECK_SEPARATOR_LENGTH); + /* skip ahead to leave Identify nonce as 00s (always 00s for egismoc devices) */ + written &= fpi_byte_writer_change_pos (&writer, SDCP_NONCE_SIZE); + /* add each of the enrolled IDs */ for (guint i = 0; i < self->enrolled_ids->len && written; i++) { written &= fpi_byte_writer_put_data (&writer, g_ptr_array_index (self->enrolled_ids, i), - EGISMOC_FINGERPRINT_DATA_SIZE); + SDCP_ENROLLMENT_ID_SIZE); } /* command suffix */ @@ -1009,13 +1161,11 @@ static void egismoc_enroll_run_state (FpiSsm *ssm, FpDevice *device) { - g_auto(FpiByteWriter) writer = {0}; FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); + FpSdcpDevice *sdcp_device = FP_SDCP_DEVICE (device); EnrollPrint *enroll_print = fpi_ssm_get_data (ssm); g_autofree guchar *payload = NULL; gsize payload_length = 0; - g_autofree gchar *device_print_id = NULL; - g_autofree gchar *user_id = NULL; switch (fpi_ssm_get_cur_state (ssm)) { @@ -1046,7 +1196,7 @@ egismoc_enroll_run_state (FpiSsm *ssm, break; case ENROLL_WAIT_FINGER: - egismoc_wait_finger_on_sensor (ssm, device); + egismoc_wait_finger_on_sensor (device); break; case ENROLL_SENSOR_CHECK: @@ -1062,7 +1212,7 @@ egismoc_enroll_run_state (FpiSsm *ssm, case ENROLL_START: egismoc_exec_cmd (device, cmd_enroll_starting, cmd_enroll_starting_len, - NULL, egismoc_task_ssm_next_state_cb); + NULL, egismoc_enroll_starting_cb); break; case ENROLL_CAPTURE_SENSOR_RESET: @@ -1077,7 +1227,12 @@ egismoc_enroll_run_state (FpiSsm *ssm, break; case ENROLL_CAPTURE_WAIT_FINGER: - egismoc_wait_finger_on_sensor (ssm, device); + egismoc_wait_finger_on_sensor (device); + break; + + case ENROLL_CAPTURE_POST_WAIT_FINGER: + egismoc_exec_cmd (device, cmd_capture_post_wait_finger, cmd_capture_post_wait_finger_len, + NULL, egismoc_task_ssm_next_state_cb); break; case ENROLL_CAPTURE_READ_RESPONSE: @@ -1091,49 +1246,18 @@ egismoc_enroll_run_state (FpiSsm *ssm, break; case ENROLL_COMMIT: - user_id = fpi_print_generate_user_id (enroll_print->print); - fp_dbg ("New fingerprint ID: %s", user_id); - - device_print_id = g_strndup (user_id, EGISMOC_FINGERPRINT_DATA_SIZE); - egismoc_set_print_data (enroll_print->print, device_print_id, user_id); - - fpi_byte_writer_init (&writer); - if (!fpi_byte_writer_put_data (&writer, cmd_new_print_prefix, - cmd_new_print_prefix_len)) - { - fpi_ssm_mark_failed (ssm, fpi_device_error_new (FP_DEVICE_ERROR_PROTO)); - break; - } - if (!fpi_byte_writer_put_data (&writer, (guint8 *) device_print_id, - EGISMOC_FINGERPRINT_DATA_SIZE)) - { - fpi_ssm_mark_failed (ssm, fpi_device_error_new (FP_DEVICE_ERROR_PROTO)); - break; - } - - payload_length = fpi_byte_writer_get_size (&writer); - egismoc_exec_cmd (device, fpi_byte_writer_reset_and_get_data (&writer), - payload_length, - g_free, egismoc_task_ssm_next_state_cb); - break; - - case ENROLL_COMMIT_SENSOR_RESET: - egismoc_exec_cmd (device, cmd_sensor_reset, cmd_sensor_reset_len, - NULL, egismoc_task_ssm_next_state_cb); - break; - - case ENROLL_COMPLETE: - egismoc_enroll_status_report (device, enroll_print, ENROLL_STATUS_COMPLETE, NULL); - fpi_ssm_next_state (ssm); + g_assert (self->enrollment_nonce); + fpi_sdcp_device_enroll_commit (sdcp_device, self->enrollment_nonce, NULL); break; } } static void -egismoc_enroll (FpDevice *device) +egismoc_enroll (FpSdcpDevice *sdcp_device) { fp_dbg ("Enroll"); - FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); + FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (sdcp_device); + FpDevice *device = FP_DEVICE (sdcp_device); EnrollPrint *enroll_print = g_new0 (EnrollPrint, 1); fpi_device_get_enroll_data (device, &enroll_print->print); @@ -1145,6 +1269,34 @@ egismoc_enroll (FpDevice *device) fpi_ssm_start (self->task_ssm, egismoc_task_ssm_done); } +static void +egismoc_identify_complete_cb (FpDevice *device, + guchar *buffer_in, + gsize length_in, + GError *error) +{ + fp_dbg ("Identify complete callback"); + FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); + FpSdcpDevice *sdcp_device = FP_SDCP_DEVICE (device); + IdentifyPrint *identify_print = fpi_ssm_get_data (self->task_ssm); + + if (error) + { + fpi_device_action_error (device, error); + goto out; + } + + fpi_sdcp_device_identify_complete (sdcp_device, identify_print->id, identify_print->mac, + identify_print->error); + + fpi_ssm_next_state (self->task_ssm); + +out: + g_clear_pointer (&identify_print->id, g_bytes_unref); + g_clear_pointer (&identify_print->mac, g_bytes_unref); + g_clear_pointer (&identify_print, g_free); +} + static void egismoc_identify_check_cb (FpDevice *device, guchar *buffer_in, @@ -1153,16 +1305,17 @@ egismoc_identify_check_cb (FpDevice *device, { fp_dbg ("Identify check callback"); FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); - gchar device_print_id[EGISMOC_FINGERPRINT_DATA_SIZE]; - FpPrint *print = NULL; - FpPrint *verify_print = NULL; - GPtrArray *prints; - gboolean found = FALSE; - guint index; + FpSdcpDevice *sdcp_device = FP_SDCP_DEVICE (device); + IdentifyPrint *identify_print = fpi_ssm_get_data (self->task_ssm); + g_autofree guchar *nonce_buf = NULL; + g_autoptr(GBytes) nonce = NULL; + + g_return_if_fail (identify_print->id == NULL); + g_return_if_fail (identify_print->mac == NULL); if (error) { - fpi_ssm_mark_failed (self->task_ssm, error); + fpi_device_action_error (device, error); return; } @@ -1173,77 +1326,40 @@ egismoc_identify_check_cb (FpDevice *device, rsp_identify_match_suffix_len)) { /* - On success, there is a 32 byte array of "something"(?) in chars 14-45 - and then the 32 byte array ID of the matched print comes as chars 46-77 + * egismoc devices always use 00s for the identify nonce, so we should set + * it here instead of using the default randomly generated nonce */ - memcpy (device_print_id, - buffer_in + EGISMOC_IDENTIFY_RESPONSE_PRINT_ID_OFFSET, - EGISMOC_FINGERPRINT_DATA_SIZE); + nonce_buf = g_malloc0 (SDCP_NONCE_SIZE); + nonce = g_bytes_new (nonce_buf, SDCP_NONCE_SIZE); + fpi_sdcp_device_set_identify_data (sdcp_device, g_steal_pointer (&nonce)); - /* Create a new print from this device_print_id and then see if it matches - * the one indicated + /* + Normally for SDCP the "Authorized Identity" response should be (id,m) + but on egismoc devices there is a prefix, followed by (m,id) (yes, it + is backwards), followed by a suffix. */ - print = fp_print_new (device); - egismoc_set_print_data (print, device_print_id, NULL); - - if (!print) - { - fpi_ssm_mark_failed (self->task_ssm, - fpi_device_error_new_msg (FP_DEVICE_ERROR_DATA_INVALID, - "Failed to build a print from " - "device response.")); - return; - } - - fp_info ("Identify successful for: %s", fp_print_get_description (print)); - - if (fpi_device_get_current_action (device) == FPI_DEVICE_ACTION_IDENTIFY) - { - fpi_device_get_identify_data (device, &prints); - found = g_ptr_array_find_with_equal_func (prints, - print, - (GEqualFunc) fp_print_equal, - &index); - - if (found) - fpi_device_identify_report (device, g_ptr_array_index (prints, index), print, NULL); - else - fpi_device_identify_report (device, NULL, print, NULL); - } - else - { - fpi_device_get_verify_data (device, &verify_print); - fp_info ("Verifying against: %s", fp_print_get_description (verify_print)); - - if (fp_print_equal (verify_print, print)) - fpi_device_verify_report (device, FPI_MATCH_SUCCESS, print, NULL); - else - fpi_device_verify_report (device, FPI_MATCH_FAIL, print, NULL); - } + identify_print->mac = g_bytes_new (buffer_in + + EGISMOC_IDENTIFY_RESPONSE_PREFIX_SIZE, + SDCP_MAC_SIZE); + identify_print->id = g_bytes_new (buffer_in + + EGISMOC_IDENTIFY_RESPONSE_PREFIX_SIZE + + SDCP_MAC_SIZE, + SDCP_ENROLLMENT_ID_SIZE); } - /* If device was successfully read but it was a "not matched" */ - else if (egismoc_validate_response_suffix (buffer_in, - length_in, - rsp_identify_notmatch_suffix, - rsp_identify_notmatch_suffix_len)) + /* If device was not successfully read (not a valid "not matched") */ + else if (!egismoc_validate_response_suffix (buffer_in, + length_in, + rsp_identify_notmatch_suffix, + rsp_identify_notmatch_suffix_len)) { - fp_info ("Print was not identified by the device"); - - if (fpi_device_get_current_action (device) == FPI_DEVICE_ACTION_VERIFY) - fpi_device_verify_report (device, FPI_MATCH_FAIL, NULL, NULL); - else - fpi_device_identify_report (device, NULL, NULL, NULL); - } - else - { - fpi_ssm_mark_failed (self->task_ssm, - fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, - "Unrecognized response from device.")); - return; + g_propagate_error (&identify_print->error, + fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, + "Unrecognized response from device")); } - fpi_ssm_next_state (self->task_ssm); -} + egismoc_exec_cmd (device, cmd_sensor_reset, cmd_sensor_reset_len, + NULL, egismoc_identify_complete_cb); + } static void egismoc_identify_run_state (FpiSsm *ssm, @@ -1282,7 +1398,7 @@ egismoc_identify_run_state (FpiSsm *ssm, break; case IDENTIFY_WAIT_FINGER: - egismoc_wait_finger_on_sensor (ssm, device); + egismoc_wait_finger_on_sensor (device); break; case IDENTIFY_SENSOR_CHECK: @@ -1295,43 +1411,209 @@ egismoc_identify_run_state (FpiSsm *ssm, egismoc_exec_cmd (device, g_steal_pointer (&payload), payload_length, g_free, egismoc_identify_check_cb); break; - - case IDENTIFY_COMPLETE_SENSOR_RESET: - egismoc_exec_cmd (device, cmd_sensor_reset, cmd_sensor_reset_len, - NULL, egismoc_task_ssm_next_state_cb); - break; - - /* - * In Windows, the driver seems at this point to then immediately take - * another read from the sensor; this is suspected to be an on-chip - * "verify". However, because the user's finger is still on the sensor from - * the identify, then it seems to always return positive. We will consider - * this extra step unnecessary and just skip it in this driver. This driver - * will instead handle matching of the FpPrint from the gallery in the - * "verify" case of the callback egismoc_identify_check_cb. - */ - case IDENTIFY_COMPLETE: - if (fpi_device_get_current_action (device) == FPI_DEVICE_ACTION_IDENTIFY) - fpi_device_identify_complete (device, NULL); - else - fpi_device_verify_complete (device, NULL); - - fpi_ssm_mark_completed (ssm); - break; } } static void -egismoc_identify_verify (FpDevice *device) +egismoc_identify (FpSdcpDevice *sdcp_device) { - fp_dbg ("Identify or Verify"); - FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); + fp_dbg ("Identify"); + FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (sdcp_device); + IdentifyPrint *identify_print = g_new0 (IdentifyPrint, 1); g_assert (self->task_ssm == NULL); - self->task_ssm = fpi_ssm_new (device, egismoc_identify_run_state, IDENTIFY_STATES); + self->task_ssm = fpi_ssm_new (FP_DEVICE (sdcp_device), + egismoc_identify_run_state, + IDENTIFY_STATES); + fpi_ssm_set_data (self->task_ssm, g_steal_pointer (&identify_print), NULL); fpi_ssm_start (self->task_ssm, egismoc_task_ssm_done); } +/* + * Validates and uses the SDCP "ConnectResponse" payload to establish a secure + * device connection which can then be used to generate enrollment IDs and + * verify identities as per SDCP. + */ +static void +egismoc_connect_cb (FpDevice *device, + guchar *buffer_in, + gsize length_in, + GError *error) +{ + fp_dbg ("SDCP ConnectResponse callback"); + FpSdcpDevice *sdcp_device = FP_SDCP_DEVICE (device); + g_autoptr(GBytes) device_random = NULL; + gsize model_certificate_len = 0; + g_autoptr(FpiSdcpClaim) claim = NULL; + g_autoptr(GBytes) mac = NULL; + int pos = EGISMOC_CONNECT_RESPONSE_PREFIX_SIZE; + + if (error) + { + fpi_sdcp_device_connect_complete (sdcp_device, NULL, NULL, NULL, error); + return; + } + + /* Check that the read payload indicates "success" */ + if (!egismoc_validate_response_suffix (buffer_in, + length_in, + rsp_sdcp_connect_success_suffix, + rsp_sdcp_connect_success_suffix_len)) + { + fpi_sdcp_device_connect_complete (sdcp_device, NULL, NULL, NULL, + fpi_device_error_new_msg (FP_DEVICE_ERROR_DATA_INVALID, + "Device responded with failure " + "instead of SDCP ConnectResponse")); + return; + } + + /* buf len should be at least larger than all required parts (plus a cert) */ + if (length_in <= SDCP_RANDOM_SIZE + + SDCP_PUBLIC_KEY_SIZE + + SDCP_PUBLIC_KEY_SIZE + + SDCP_RANDOM_SIZE + + SDCP_SIGNATURE_SIZE + + SDCP_SIGNATURE_SIZE + + SDCP_MAC_SIZE) + { + fpi_sdcp_device_connect_complete (sdcp_device, NULL, NULL, NULL, + fpi_device_error_new_msg (FP_DEVICE_ERROR_DATA_INVALID, + "Device SDCP ConnectResponse " + "was not long enough")); + return; + } + + + /* + * Parse ConnectResponse parts; unfortunately these devices return a somewhat + * non-standard ConnectResponse as there are two bytes indicating cert_m's + * length which must be handled. + */ + claim = g_new0 (FpiSdcpClaim, 1); + + /* r_d */ + device_random = g_bytes_new (buffer_in + pos, SDCP_RANDOM_SIZE); + pos += SDCP_RANDOM_SIZE; + + /* next two bytes are an unsigned short giving the cert_m length */ + model_certificate_len = buffer_in[pos] << 8 | buffer_in[pos + 1]; + pos += 2; + + /* cert_m bytes based on length fetched above */ + claim->model_certificate = g_bytes_new (buffer_in + pos, model_certificate_len); + pos += model_certificate_len; + + /* pk_d */ + claim->device_public_key = g_bytes_new (buffer_in + pos, SDCP_PUBLIC_KEY_SIZE); + pos += SDCP_PUBLIC_KEY_SIZE; + + /* pk_f */ + claim->firmware_public_key = g_bytes_new (buffer_in + pos, SDCP_PUBLIC_KEY_SIZE); + pos += SDCP_PUBLIC_KEY_SIZE; + + /* h_f */ + claim->firmware_hash = g_bytes_new (buffer_in + pos, SDCP_MAC_SIZE); + pos += SDCP_MAC_SIZE; + + /* s_m */ + claim->model_signature = g_bytes_new (buffer_in + pos, SDCP_SIGNATURE_SIZE); + pos += SDCP_SIGNATURE_SIZE; + + /* s_d */ + claim->device_signature = g_bytes_new (buffer_in + pos, SDCP_SIGNATURE_SIZE); + pos += SDCP_SIGNATURE_SIZE; + + /* m */ + mac = g_bytes_new (buffer_in + pos, SDCP_MAC_SIZE); + pos += SDCP_MAC_SIZE; + + /* Derive SDCP keys and establish secured connection */ + fpi_sdcp_device_connect_complete (sdcp_device, device_random, claim, mac, error); +} + +static void +egismoc_connect (FpSdcpDevice *sdcp_device) +{ + fp_dbg ("Connect"); + FpDevice *device = FP_DEVICE (sdcp_device); + g_auto(FpiByteWriter) writer = {0}; + gboolean written = TRUE; + g_autoptr(GError) error = NULL; + + g_autoptr(GBytes) host_random = NULL; + const guchar *host_random_ptr; + gsize host_random_len = 0; + + g_autoptr(GBytes) host_public_key = NULL; + const guchar *host_public_key_ptr; + gsize host_public_key_len = 0; + + fpi_sdcp_device_get_connect_data (sdcp_device, &host_random, &host_public_key); + if (!host_random || !host_public_key) + { + fpi_sdcp_device_connect_complete (sdcp_device, NULL, NULL, NULL, + fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, + "Failed to get SDCP Connect data")); + return; + } + + host_random_ptr = g_bytes_get_data (host_random, &host_random_len); + host_public_key_ptr = g_bytes_get_data (host_public_key, &host_public_key_len); + + const int length = cmd_sdcp_connect_prefix_len + + host_random_len + + host_public_key_len + + cmd_sdcp_connect_suffix_len; + + fpi_byte_writer_init_with_size (&writer, length, TRUE); + + written &= fpi_byte_writer_put_data (&writer, cmd_sdcp_connect_prefix, + cmd_sdcp_connect_prefix_len); + + written &= fpi_byte_writer_put_data (&writer, host_random_ptr, + host_random_len); + + written &= fpi_byte_writer_put_data (&writer, host_public_key_ptr, + host_public_key_len); + + written &= fpi_byte_writer_put_data (&writer, cmd_sdcp_connect_suffix, + cmd_sdcp_connect_suffix_len); + + if (!written) + { + fpi_sdcp_device_connect_complete (sdcp_device, NULL, NULL, NULL, + fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, + "Failed to write SDCP Connect payload")); + return; + } + + /* Execute the egismoc SDCP "Connect" command */ + egismoc_exec_cmd (device, + fpi_byte_writer_reset_and_get_data (&writer), length, g_free, + egismoc_connect_cb); +} + +static void +egismoc_dev_init_done (FpiSsm *ssm, + FpDevice *device, + GError *error) +{ + FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); + + if (error) + { + g_usb_device_release_interface ( + fpi_device_get_usb_device (device), 0, 0, NULL); + egismoc_task_ssm_done (ssm, device, error); + return; + } + + egismoc_task_ssm_done (ssm, device, NULL); + fpi_sdcp_device_open_complete (FP_SDCP_DEVICE (device), NULL); + + self->dev_init_done = TRUE; +} + static void egismoc_fw_version_cb (FpDevice *device, guchar *buffer_in, @@ -1360,7 +1642,7 @@ egismoc_fw_version_cb (FpDevice *device, fpi_ssm_mark_failed (self->task_ssm, fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, "Device firmware response " - "was not valid.")); + "was not valid")); return; } @@ -1380,23 +1662,6 @@ egismoc_fw_version_cb (FpDevice *device, fpi_ssm_next_state (self->task_ssm); } -static void -egismoc_dev_init_done (FpiSsm *ssm, - FpDevice *device, - GError *error) -{ - if (error) - { - g_usb_device_release_interface ( - fpi_device_get_usb_device (device), 0, 0, NULL); - egismoc_task_ssm_done (ssm, device, error); - return; - } - - egismoc_task_ssm_done (ssm, device, NULL); - fpi_device_open_complete (device, NULL); -} - static void egismoc_dev_init_handler (FpiSsm *ssm, FpDevice *device) @@ -1463,6 +1728,82 @@ egismoc_dev_init_handler (FpiSsm *ssm, NULL); } +static void +egismoc_open (FpSdcpDevice *sdcp_device) +{ + fp_dbg ("Opening device"); + FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (sdcp_device); + FpDevice *device = FP_DEVICE (sdcp_device); + GError *error = NULL; + + self->interrupt_cancellable = g_cancellable_new (); + + if (!g_usb_device_reset (fpi_device_get_usb_device (device), &error)) + { + fpi_sdcp_device_open_complete (sdcp_device, error); + return; + } + + if (!g_usb_device_claim_interface (fpi_device_get_usb_device (device), + 0, 0, &error)) + { + fpi_sdcp_device_open_complete (sdcp_device, error); + return; + } + + if (self->dev_init_done) + { + fpi_sdcp_device_open_complete (FP_SDCP_DEVICE (device), NULL); + } + else + { + g_assert (self->task_ssm == NULL); + self->task_ssm = fpi_ssm_new (device, egismoc_dev_init_handler, DEV_INIT_STATES); + fpi_ssm_start (self->task_ssm, egismoc_dev_init_done); + } +} + +static void +egismoc_cancel (FpDevice *device) +{ + fp_dbg ("Cancel"); + FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); + + g_cancellable_cancel (self->interrupt_cancellable); + g_clear_object (&self->interrupt_cancellable); + self->interrupt_cancellable = g_cancellable_new (); + + /* Terminate ongoing action if cancel is called */ + if (self->task_ssm) + fpi_ssm_mark_failed (self->task_ssm, fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Operation was cancelled")); +} + +static void +egismoc_suspend (FpDevice *device) +{ + fp_dbg ("Suspend"); + + egismoc_cancel (device); + g_cancellable_cancel (fpi_device_get_cancellable (device)); + fpi_device_suspend_complete (device, NULL); +} + +static void +egismoc_close (FpDevice *device) +{ + fp_dbg ("Closing device"); + FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); + GError *error = NULL; + + egismoc_cancel (device); + g_clear_object (&self->interrupt_cancellable); + + g_usb_device_release_interface (fpi_device_get_usb_device (device), + 0, 0, &error); + fpi_device_close_complete (device, error); +} + static void egismoc_probe (FpDevice *device) { @@ -1470,8 +1811,7 @@ egismoc_probe (FpDevice *device) GError *error = NULL; g_autofree gchar *serial = NULL; FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); - - fp_dbg ("%s enter --> ", G_STRFUNC); + guint64 driver_data; /* Claim usb interface */ usb_dev = fpi_device_get_usb_device (device); @@ -1515,8 +1855,11 @@ egismoc_probe (FpDevice *device) return; } - if (fpi_device_get_driver_data (device) & EGISMOC_DRIVER_MAX_ENROLL_STAGES_20) - self->max_enroll_stages = 20; + driver_data = fpi_device_get_driver_data (device); + if (driver_data & EGISMOC_DRIVER_MAX_ENROLL_STAGES_20) + self->max_enroll_stages = 20; + else if (driver_data & EGISMOC_DRIVER_MAX_ENROLL_STAGES_15) + self->max_enroll_stages = 15; else self->max_enroll_stages = EGISMOC_MAX_ENROLL_STAGES_DEFAULT; @@ -1528,69 +1871,6 @@ egismoc_probe (FpDevice *device) fpi_device_probe_complete (device, serial, NULL, error); } -static void -egismoc_open (FpDevice *device) -{ - fp_dbg ("Opening device"); - FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); - GError *error = NULL; - - self->interrupt_cancellable = g_cancellable_new (); - - if (!g_usb_device_reset (fpi_device_get_usb_device (device), &error)) - { - fpi_device_open_complete (device, error); - return; - } - - if (!g_usb_device_claim_interface (fpi_device_get_usb_device (device), - 0, 0, &error)) - { - fpi_device_open_complete (device, error); - return; - } - - g_assert (self->task_ssm == NULL); - self->task_ssm = fpi_ssm_new (device, egismoc_dev_init_handler, DEV_INIT_STATES); - fpi_ssm_start (self->task_ssm, egismoc_dev_init_done); -} - -static void -egismoc_cancel (FpDevice *device) -{ - fp_dbg ("Cancel"); - FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); - - g_cancellable_cancel (self->interrupt_cancellable); - g_clear_object (&self->interrupt_cancellable); - self->interrupt_cancellable = g_cancellable_new (); -} - -static void -egismoc_suspend (FpDevice *device) -{ - fp_dbg ("Suspend"); - - egismoc_cancel (device); - g_cancellable_cancel (fpi_device_get_cancellable (device)); - fpi_device_suspend_complete (device, NULL); -} - -static void -egismoc_close (FpDevice *device) -{ - fp_dbg ("Closing device"); - FpiDeviceEgisMoc *self = FPI_DEVICE_EGISMOC (device); - GError *error = NULL; - - egismoc_cancel (device); - g_clear_object (&self->interrupt_cancellable); - - g_usb_device_release_interface (fpi_device_get_usb_device (device), - 0, 0, &error); - fpi_device_close_complete (device, error); -} - static void fpi_device_egismoc_init (FpiDeviceEgisMoc *self) { @@ -1601,6 +1881,7 @@ static void fpi_device_egismoc_class_init (FpiDeviceEgisMocClass *klass) { FpDeviceClass *dev_class = FP_DEVICE_CLASS (klass); + FpSdcpDeviceClass *sdcp_dev_class = FP_SDCP_DEVICE_CLASS (klass); dev_class->id = FP_COMPONENT; dev_class->full_name = EGISMOC_DRIVER_FULLNAME; @@ -1612,18 +1893,23 @@ fpi_device_egismoc_class_init (FpiDeviceEgisMocClass *klass) /* device should be "always off" unless being used */ dev_class->temp_hot_seconds = 0; + sdcp_dev_class->open = egismoc_open; + sdcp_dev_class->connect = egismoc_connect; + sdcp_dev_class->list = egismoc_list; + sdcp_dev_class->enroll = egismoc_enroll; + sdcp_dev_class->enroll_commit = egismoc_enroll_commit; + sdcp_dev_class->identify = egismoc_identify; + dev_class->probe = egismoc_probe; - dev_class->open = egismoc_open; dev_class->cancel = egismoc_cancel; dev_class->suspend = egismoc_suspend; dev_class->close = egismoc_close; - dev_class->identify = egismoc_identify_verify; - dev_class->verify = egismoc_identify_verify; - dev_class->enroll = egismoc_enroll; dev_class->delete = egismoc_delete; dev_class->clear_storage = egismoc_clear_storage; - dev_class->list = egismoc_list; fpi_device_class_auto_initialize_features (dev_class); dev_class->features |= FP_DEVICE_FEATURE_DUPLICATES_CHECK; + + sdcp_dev_class->ignore_device_certificate = FALSE; + sdcp_dev_class->ignore_device_signatures = FALSE; } diff --git a/libfprint/drivers/egismoc/egismoc.h b/libfprint/drivers/egismoc/egismoc.h index ef31d2cd..06f74102 100644 --- a/libfprint/drivers/egismoc/egismoc.h +++ b/libfprint/drivers/egismoc/egismoc.h @@ -1,15 +1,10 @@ /* * Driver for Egis Technology (LighTuning) Match-On-Chip sensors - * Originally authored 2023 by Joshua Grisham + * Copyright (C) 2023-2025 Joshua Grisham * * Portions of code and logic inspired from the elanmoc libfprint driver * which is copyright (C) 2021 Elan Microelectronics Inc (see elanmoc.c) * - * Based on original reverse-engineering work by Joshua Grisham. The protocol has - * been reverse-engineered from captures of the official Windows driver, and by - * testing commands on the sensor with a multiplatform Python prototype driver: - * https://github.com/joshuagrisham/galaxy-book2-pro-linux/tree/main/fingerprint/ - * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either @@ -27,16 +22,18 @@ #pragma once -#include "fpi-device.h" #include "fpi-ssm.h" -G_DECLARE_FINAL_TYPE (FpiDeviceEgisMoc, fpi_device_egismoc, FPI, DEVICE_EGISMOC, FpDevice) +#include "fpi-sdcp-device.h" + +G_DECLARE_FINAL_TYPE (FpiDeviceEgisMoc, fpi_device_egismoc, FPI, DEVICE_EGISMOC, FpSdcpDevice) #define EGISMOC_DRIVER_FULLNAME "Egis Technology (LighTuning) Match-on-Chip" #define EGISMOC_DRIVER_CHECK_PREFIX_TYPE1 (1 << 0) #define EGISMOC_DRIVER_CHECK_PREFIX_TYPE2 (1 << 1) #define EGISMOC_DRIVER_MAX_ENROLL_STAGES_20 (1 << 2) +#define EGISMOC_DRIVER_MAX_ENROLL_STAGES_15 (1 << 3) #define EGISMOC_EP_CMD_OUT (0x02 | FPI_USB_ENDPOINT_OUT) #define EGISMOC_EP_CMD_IN (0x81 | FPI_USB_ENDPOINT_IN) @@ -52,7 +49,11 @@ G_DECLARE_FINAL_TYPE (FpiDeviceEgisMoc, fpi_device_egismoc, FPI, DEVICE_EGISMOC, #define EGISMOC_MAX_ENROLL_STAGES_DEFAULT 10 #define EGISMOC_MAX_ENROLL_NUM 10 -#define EGISMOC_FINGERPRINT_DATA_SIZE 32 +#define EGISMOC_FINGER_ON_SENSOR_TIMEOUT_USEC (10 * G_USEC_PER_SEC) + +#define EGISMOC_CONNECT_RESPONSE_PREFIX_SIZE 15 +#define EGISMOC_IDENTIFY_RESPONSE_PREFIX_SIZE 14 +#define EGISMOC_ENROLL_STARTING_RESPONSE_PREFIX_SIZE 14 #define EGISMOC_LIST_RESPONSE_PREFIX_SIZE 14 #define EGISMOC_LIST_RESPONSE_SUFFIX_SIZE 2 @@ -72,6 +73,9 @@ static gsize cmd_fw_version_len = sizeof (cmd_fw_version) / sizeof (cmd_fw_versi static guchar rsp_fw_version_suffix[] = {0x90, 0x00}; static gsize rsp_fw_version_suffix_len = sizeof (rsp_fw_version_suffix) / sizeof (rsp_fw_version_suffix[0]); +static guchar rsp_sensor_has_finger_suffix[] = {0x90, 0x00, 0x90, 0x00}; +static gsize rsp_sensor_has_finger_suffix_len = sizeof (rsp_sensor_has_finger_suffix) / sizeof (rsp_sensor_has_finger_suffix[0]); + static guchar cmd_list[] = {0x00, 0x00, 0x00, 0x07, 0x50, 0x19, 0x04, 0x00, 0x00, 0x01, 0x40}; static gsize cmd_list_len = sizeof (cmd_list) / sizeof (cmd_list[0]); @@ -93,18 +97,19 @@ static gsize cmd_sensor_enroll_len = sizeof (cmd_sensor_enroll) / sizeof (cmd_se static guchar cmd_enroll_starting[] = {0x00, 0x00, 0x00, 0x07, 0x50, 0x16, 0x01, 0x00, 0x00, 0x00, 0x20}; static gsize cmd_enroll_starting_len = sizeof (cmd_enroll_starting) / sizeof (cmd_enroll_starting[0]); +static guchar rsp_enroll_starting_suffix[] = {0x90, 0x00}; +static gsize rsp_enroll_starting_suffix_len = sizeof (rsp_enroll_starting_suffix) / sizeof (rsp_enroll_starting_suffix[0]); static guchar cmd_sensor_start_capture[] = {0x00, 0x00, 0x00, 0x04, 0x50, 0x16, 0x02, 0x01}; static gsize cmd_sensor_start_capture_len = sizeof (cmd_sensor_start_capture) / sizeof (cmd_sensor_start_capture[0]); +static guchar cmd_capture_post_wait_finger[] = {0x00, 0x00, 0x00, 0x07, 0x50, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x80}; +static gsize cmd_capture_post_wait_finger_len = sizeof (cmd_capture_post_wait_finger) / sizeof (cmd_capture_post_wait_finger[0]); + static guchar cmd_read_capture[] = {0x00, 0x00, 0x00, 0x07, 0x50, 0x16, 0x02, 0x02, 0x00, 0x00, 0x02}; static gsize cmd_read_capture_len = sizeof (cmd_read_capture) / sizeof (cmd_read_capture[0]); -static guchar rsp_read_success_prefix[] = {0x00, 0x00, 0x00, 0x04}; -static gsize rsp_read_success_prefix_len = sizeof (rsp_read_success_prefix) / sizeof (rsp_read_success_prefix[0]); static guchar rsp_read_success_suffix[] = {0x90, 0x00}; static gsize rsp_read_success_suffix_len = sizeof (rsp_read_success_suffix) / sizeof (rsp_read_success_suffix[0]); -static guchar rsp_read_offcenter_prefix[] = {0x00, 0x00, 0x00, 0x04}; -static gsize rsp_read_offcenter_prefix_len = sizeof (rsp_read_offcenter_prefix) / sizeof (rsp_read_offcenter_prefix[0]); static guchar rsp_read_offcenter_suffix[] = {0x64, 0x91}; static gsize rsp_read_offcenter_suffix_len = sizeof (rsp_read_offcenter_suffix) / sizeof (rsp_read_offcenter_suffix[0]); static guchar rsp_read_dirty_prefix[] = {0x00, 0x00, 0x00, 0x02, 0x64}; @@ -112,6 +117,8 @@ static gsize rsp_read_dirty_prefix_len = sizeof (rsp_read_dirty_prefix) / sizeof static guchar cmd_commit_starting[] = {0x00, 0x00, 0x00, 0x07, 0x50, 0x16, 0x05, 0x00, 0x00, 0x00, 0x20}; static gsize cmd_commit_starting_len = sizeof (cmd_commit_starting) / sizeof (cmd_commit_starting[0]); +static guchar rsp_commit_success_suffix[] = {0x90, 0x00}; +static gsize rsp_commit_success_suffix_len = sizeof (rsp_commit_success_suffix) / sizeof (rsp_commit_success_suffix[0]); /* commands which exist on the device but are currently not used */ @@ -130,8 +137,13 @@ static gsize cmd_commit_starting_len = sizeof (cmd_commit_starting) / sizeof (cm /* prefixes/suffixes and other things for dynamically created command payloads */ #define EGISMOC_CHECK_BYTES_LENGTH 2 -#define EGISMOC_IDENTIFY_RESPONSE_PRINT_ID_OFFSET 46 -#define EGISMOC_CMD_CHECK_SEPARATOR_LENGTH 32 + +static guchar cmd_sdcp_connect_prefix[] = {0x00, 0x00, 0x00, 0x6b, 0x50, 0x57, 0x01, 0x00, 0x00, 0x00, 0x62, 0x20}; +static gsize cmd_sdcp_connect_prefix_len = sizeof (cmd_sdcp_connect_prefix) / sizeof (cmd_sdcp_connect_prefix[0]); +static guchar cmd_sdcp_connect_suffix[] = {0x00, 0x00}; +static gsize cmd_sdcp_connect_suffix_len = sizeof (cmd_sdcp_connect_suffix) / sizeof (cmd_sdcp_connect_suffix[0]); +static guchar rsp_sdcp_connect_success_suffix[] = {0x90, 0x00}; +static gsize rsp_sdcp_connect_success_suffix_len = sizeof (rsp_sdcp_connect_success_suffix) / sizeof (rsp_sdcp_connect_success_suffix[0]); static guchar cmd_new_print_prefix[] = {0x00, 0x00, 0x00, 0x27, 0x50, 0x16, 0x03, 0x00, 0x00, 0x00, 0x20}; static gsize cmd_new_print_prefix_len = sizeof (cmd_new_print_prefix) / sizeof (cmd_new_print_prefix[0]); @@ -169,6 +181,18 @@ typedef enum { DEV_INIT_STATES, } DeviceInitStates; +typedef enum { + CONNECT, + CONNECT_RESPONSE, + CONNECT_STATES, +} ConnectStates; + +typedef enum { + WAIT_FINGER_NOT_ON_SENSOR, + WAIT_FINGER_ON_SENSOR, + WAIT_FINGER_STATES, +} WaitFingerStates; + typedef enum { IDENTIFY_GET_ENROLLED_IDS, IDENTIFY_CHECK_ENROLLED_NUM, @@ -177,8 +201,6 @@ typedef enum { IDENTIFY_WAIT_FINGER, IDENTIFY_SENSOR_CHECK, IDENTIFY_CHECK, - IDENTIFY_COMPLETE_SENSOR_RESET, - IDENTIFY_COMPLETE, IDENTIFY_STATES, } IdentifyStates; @@ -194,11 +216,10 @@ typedef enum { ENROLL_CAPTURE_SENSOR_RESET, ENROLL_CAPTURE_SENSOR_START_CAPTURE, ENROLL_CAPTURE_WAIT_FINGER, + ENROLL_CAPTURE_POST_WAIT_FINGER, ENROLL_CAPTURE_READ_RESPONSE, ENROLL_COMMIT_START, ENROLL_COMMIT, - ENROLL_COMMIT_SENSOR_RESET, - ENROLL_COMPLETE, ENROLL_STATES, } EnrollStates; diff --git a/libfprint/drivers/virtual-sdcp.c b/libfprint/drivers/virtual-sdcp.c deleted file mode 100644 index 95431da7..00000000 --- a/libfprint/drivers/virtual-sdcp.c +++ /dev/null @@ -1,676 +0,0 @@ -/* - * Virtual driver for SDCP device debugging - * - * Copyright (C) 2020 Benjamin Berg - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/* - * This is a virtual test driver to test the basic SDCP functionality. - * It uses the test binaries from Microsoft, which were extended to allow - * a simple chat with the device. - * The environment variable contains the to be executed binary including - * arguments. This binary should be compiled from the code in - * https://github.com/Microsoft/SecureDeviceConnectionProtocol - * or, until it is merged upstream - * https://github.com/benzea/SecureDeviceConnectionProtocol - * - * Note that using this as an external executable has the advantage that we - * do not need to link against mbedtls or any other crypto library. - */ - -#define FP_COMPONENT "virtual_sdcp" - -#include "fpi-log.h" -#include "fpi-ssm.h" - -#include "../fpi-sdcp-device.h" - -#include -#include - -struct _FpDeviceVirtualSdcp -{ - FpSdcpDevice parent; - - GSubprocess *proc; - GOutputStream *proc_stdin; - GInputStream *proc_stdout; - - /* Only valid while a read/write is pending */ - GByteArray *msg_out; - GByteArray *msg_in; - - GByteArray *connect_msg; -}; - -G_DECLARE_FINAL_TYPE (FpDeviceVirtualSdcp, fpi_device_virtual_sdcp, FPI, DEVICE_VIRTUAL_SDCP, FpSdcpDevice) -G_DEFINE_TYPE (FpDeviceVirtualSdcp, fpi_device_virtual_sdcp, FP_TYPE_SDCP_DEVICE) - -guint8 ca_1[] = { - 0x30, 0x82, 0x03, 0xFD, 0x30, 0x82, 0x03, 0x82, 0xA0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x13, 0x33, - 0x00, 0x00, 0x00, 0x07, 0xE8, 0x9D, 0x61, 0x62, 0x4D, 0x46, 0x0F, 0x95, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x07, 0x30, 0x0A, 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x02, 0x30, 0x81, - 0x84, 0x31, 0x0B, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x13, - 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x08, 0x13, 0x0A, 0x57, 0x61, 0x73, 0x68, 0x69, 0x6E, 0x67, - 0x74, 0x6F, 0x6E, 0x31, 0x10, 0x30, 0x0E, 0x06, 0x03, 0x55, 0x04, 0x07, 0x13, 0x07, 0x52, 0x65, - 0x64, 0x6D, 0x6F, 0x6E, 0x64, 0x31, 0x1E, 0x30, 0x1C, 0x06, 0x03, 0x55, 0x04, 0x0A, 0x13, 0x15, - 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, 0x74, 0x20, 0x43, 0x6F, 0x72, 0x70, 0x6F, 0x72, - 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x31, 0x2E, 0x30, 0x2C, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x25, - 0x57, 0x69, 0x6E, 0x64, 0x6F, 0x77, 0x73, 0x20, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x53, 0x65, - 0x63, 0x75, 0x72, 0x65, 0x20, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x20, 0x50, 0x43, 0x41, - 0x20, 0x32, 0x30, 0x31, 0x38, 0x30, 0x1E, 0x17, 0x0D, 0x31, 0x38, 0x30, 0x31, 0x33, 0x31, 0x31, - 0x39, 0x35, 0x34, 0x35, 0x33, 0x5A, 0x17, 0x0D, 0x32, 0x38, 0x30, 0x31, 0x33, 0x31, 0x32, 0x30, - 0x30, 0x34, 0x35, 0x33, 0x5A, 0x30, 0x7D, 0x31, 0x0B, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, - 0x13, 0x02, 0x55, 0x53, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x08, 0x13, 0x0A, 0x57, - 0x61, 0x73, 0x68, 0x69, 0x6E, 0x67, 0x74, 0x6F, 0x6E, 0x31, 0x10, 0x30, 0x0E, 0x06, 0x03, 0x55, - 0x04, 0x07, 0x13, 0x07, 0x52, 0x65, 0x64, 0x6D, 0x6F, 0x6E, 0x64, 0x31, 0x1E, 0x30, 0x1C, 0x06, - 0x03, 0x55, 0x04, 0x0A, 0x13, 0x15, 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, 0x74, 0x20, - 0x43, 0x6F, 0x72, 0x70, 0x6F, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x31, 0x27, 0x30, 0x25, 0x06, - 0x03, 0x55, 0x04, 0x03, 0x13, 0x1E, 0x57, 0x69, 0x6E, 0x64, 0x6F, 0x77, 0x73, 0x20, 0x48, 0x65, - 0x6C, 0x6C, 0x6F, 0x20, 0x31, 0x39, 0x42, 0x39, 0x32, 0x39, 0x36, 0x35, 0x20, 0x43, 0x41, 0x20, - 0x32, 0x30, 0x31, 0x38, 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, - 0x01, 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0xBE, - 0x4B, 0x90, 0x6E, 0x24, 0xFC, 0xA1, 0x53, 0xC8, 0xA7, 0x3C, 0x70, 0xE8, 0x97, 0xCD, 0x1B, 0x31, - 0xE4, 0x95, 0x91, 0x7A, 0x58, 0xA2, 0x86, 0xA8, 0x70, 0xF6, 0x09, 0x30, 0x77, 0x99, 0x3D, 0x10, - 0xDF, 0xF7, 0x95, 0x0F, 0x68, 0x83, 0xE6, 0xA4, 0x11, 0x7C, 0xDA, 0x82, 0xE7, 0x0B, 0x8B, 0xF2, - 0x9D, 0x6B, 0x5B, 0xF5, 0x3E, 0x77, 0xB4, 0xC1, 0x0E, 0x49, 0x00, 0x83, 0xBA, 0x94, 0xF8, 0xA3, - 0x82, 0x01, 0xD7, 0x30, 0x82, 0x01, 0xD3, 0x30, 0x10, 0x06, 0x09, 0x2B, 0x06, 0x01, 0x04, 0x01, - 0x82, 0x37, 0x15, 0x01, 0x04, 0x03, 0x02, 0x01, 0x00, 0x30, 0x1D, 0x06, 0x03, 0x55, 0x1D, 0x0E, - 0x04, 0x16, 0x04, 0x14, 0x13, 0x93, 0xC8, 0xCD, 0xF2, 0x23, 0x9A, 0x2D, 0xC6, 0x9B, 0x2A, 0xEB, - 0x9A, 0xAB, 0x99, 0x0B, 0x56, 0x04, 0x5E, 0x7C, 0x30, 0x65, 0x06, 0x03, 0x55, 0x1D, 0x20, 0x04, - 0x5E, 0x30, 0x5C, 0x30, 0x06, 0x06, 0x04, 0x55, 0x1D, 0x20, 0x00, 0x30, 0x52, 0x06, 0x0C, 0x2B, - 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x4C, 0x83, 0x7D, 0x01, 0x01, 0x30, 0x42, 0x30, 0x40, 0x06, - 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x01, 0x16, 0x34, 0x68, 0x74, 0x74, 0x70, 0x3A, - 0x2F, 0x2F, 0x77, 0x77, 0x77, 0x2E, 0x6D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, 0x74, 0x2E, - 0x63, 0x6F, 0x6D, 0x2F, 0x70, 0x6B, 0x69, 0x6F, 0x70, 0x73, 0x2F, 0x44, 0x6F, 0x63, 0x73, 0x2F, - 0x52, 0x65, 0x70, 0x6F, 0x73, 0x69, 0x74, 0x6F, 0x72, 0x79, 0x2E, 0x68, 0x74, 0x6D, 0x00, 0x30, - 0x19, 0x06, 0x09, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x14, 0x02, 0x04, 0x0C, 0x1E, 0x0A, - 0x00, 0x53, 0x00, 0x75, 0x00, 0x62, 0x00, 0x43, 0x00, 0x41, 0x30, 0x0B, 0x06, 0x03, 0x55, 0x1D, - 0x0F, 0x04, 0x04, 0x03, 0x02, 0x01, 0x86, 0x30, 0x0F, 0x06, 0x03, 0x55, 0x1D, 0x13, 0x01, 0x01, - 0xFF, 0x04, 0x05, 0x30, 0x03, 0x01, 0x01, 0xFF, 0x30, 0x1F, 0x06, 0x03, 0x55, 0x1D, 0x23, 0x04, - 0x18, 0x30, 0x16, 0x80, 0x14, 0xDA, 0xCA, 0x4B, 0xD0, 0x4C, 0x56, 0x03, 0x27, 0x5F, 0x97, 0xEB, - 0x75, 0xA3, 0x02, 0xC3, 0xBF, 0x45, 0x9C, 0xF8, 0xB1, 0x30, 0x68, 0x06, 0x03, 0x55, 0x1D, 0x1F, - 0x04, 0x61, 0x30, 0x5F, 0x30, 0x5D, 0xA0, 0x5B, 0xA0, 0x59, 0x86, 0x57, 0x68, 0x74, 0x74, 0x70, - 0x3A, 0x2F, 0x2F, 0x77, 0x77, 0x77, 0x2E, 0x6D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, 0x74, - 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x70, 0x6B, 0x69, 0x6F, 0x70, 0x73, 0x2F, 0x63, 0x72, 0x6C, 0x2F, - 0x57, 0x69, 0x6E, 0x64, 0x6F, 0x77, 0x73, 0x25, 0x32, 0x30, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x25, - 0x32, 0x30, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x25, 0x32, 0x30, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x25, 0x32, 0x30, 0x50, 0x43, 0x41, 0x25, 0x32, 0x30, 0x32, 0x30, 0x31, 0x38, 0x2E, - 0x63, 0x72, 0x6C, 0x30, 0x75, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x01, 0x04, - 0x69, 0x30, 0x67, 0x30, 0x65, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x02, 0x86, - 0x59, 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, 0x77, 0x77, 0x77, 0x2E, 0x6D, 0x69, 0x63, 0x72, - 0x6F, 0x73, 0x6F, 0x66, 0x74, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x70, 0x6B, 0x69, 0x6F, 0x70, 0x73, - 0x2F, 0x63, 0x65, 0x72, 0x74, 0x73, 0x2F, 0x57, 0x69, 0x6E, 0x64, 0x6F, 0x77, 0x73, 0x25, 0x32, - 0x30, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x25, 0x32, 0x30, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x25, - 0x32, 0x30, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x25, 0x32, 0x30, 0x50, 0x43, 0x41, 0x25, - 0x32, 0x30, 0x32, 0x30, 0x31, 0x38, 0x2E, 0x63, 0x72, 0x74, 0x30, 0x0A, 0x06, 0x08, 0x2A, 0x86, - 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x02, 0x03, 0x69, 0x00, 0x30, 0x66, 0x02, 0x31, 0x00, 0x87, 0xB6, - 0x82, 0xF3, 0xDA, 0xBE, 0xB1, 0x7B, 0x98, 0x7D, 0x3D, 0x0A, 0x90, 0xA8, 0xF5, 0xBF, 0x15, 0xC3, - 0xEE, 0x8A, 0x4E, 0xC0, 0x7B, 0x10, 0x1D, 0xA9, 0xE3, 0x0B, 0xEC, 0x2C, 0x53, 0x4E, 0xA7, 0xBD, - 0xF1, 0x6C, 0xAD, 0x18, 0x55, 0xBA, 0x25, 0x73, 0x55, 0xB7, 0x5B, 0x12, 0x24, 0xF4, 0x02, 0x31, - 0x00, 0xAF, 0x02, 0x9C, 0x4B, 0x92, 0xD0, 0x72, 0xA5, 0x80, 0xCA, 0x69, 0x2B, 0x38, 0x50, 0x64, - 0xD8, 0x58, 0x9E, 0xEA, 0xD6, 0x35, 0xCF, 0x68, 0x98, 0x92, 0x81, 0x09, 0x61, 0xC2, 0xBD, 0xB1, - 0x4C, 0x7F, 0xAE, 0x55, 0x7B, 0xFC, 0x22, 0xDD, 0xD6, 0xB7, 0x7C, 0xB5, 0xA8, 0x18, 0x5D, 0x33, - 0x04 -}; -guint8 ca_2[] = { - 0x30, 0x82, 0x04, 0x56, 0x30, 0x82, 0x03, 0xDC, 0xA0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x13, 0x33, - 0x00, 0x00, 0x00, 0x03, 0x6C, 0xCF, 0xED, 0xE2, 0x44, 0x70, 0x19, 0xBF, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x03, 0x30, 0x0A, 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x03, 0x30, 0x81, - 0x94, 0x31, 0x0B, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x13, - 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x08, 0x13, 0x0A, 0x57, 0x61, 0x73, 0x68, 0x69, 0x6E, 0x67, - 0x74, 0x6F, 0x6E, 0x31, 0x10, 0x30, 0x0E, 0x06, 0x03, 0x55, 0x04, 0x07, 0x13, 0x07, 0x52, 0x65, - 0x64, 0x6D, 0x6F, 0x6E, 0x64, 0x31, 0x1E, 0x30, 0x1C, 0x06, 0x03, 0x55, 0x04, 0x0A, 0x13, 0x15, - 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, 0x74, 0x20, 0x43, 0x6F, 0x72, 0x70, 0x6F, 0x72, - 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x31, 0x3E, 0x30, 0x3C, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x35, - 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, 0x74, 0x20, 0x45, 0x43, 0x43, 0x20, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x20, 0x52, 0x6F, 0x6F, 0x74, 0x20, 0x43, 0x65, 0x72, 0x74, 0x69, - 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, - 0x20, 0x32, 0x30, 0x31, 0x37, 0x30, 0x1E, 0x17, 0x0D, 0x31, 0x38, 0x30, 0x31, 0x32, 0x35, 0x31, - 0x39, 0x34, 0x39, 0x33, 0x38, 0x5A, 0x17, 0x0D, 0x33, 0x33, 0x30, 0x31, 0x32, 0x35, 0x31, 0x39, - 0x35, 0x39, 0x33, 0x38, 0x5A, 0x30, 0x81, 0x84, 0x31, 0x0B, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, - 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x08, 0x13, 0x0A, - 0x57, 0x61, 0x73, 0x68, 0x69, 0x6E, 0x67, 0x74, 0x6F, 0x6E, 0x31, 0x10, 0x30, 0x0E, 0x06, 0x03, - 0x55, 0x04, 0x07, 0x13, 0x07, 0x52, 0x65, 0x64, 0x6D, 0x6F, 0x6E, 0x64, 0x31, 0x1E, 0x30, 0x1C, - 0x06, 0x03, 0x55, 0x04, 0x0A, 0x13, 0x15, 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, 0x74, - 0x20, 0x43, 0x6F, 0x72, 0x70, 0x6F, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x31, 0x2E, 0x30, 0x2C, - 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x25, 0x57, 0x69, 0x6E, 0x64, 0x6F, 0x77, 0x73, 0x20, 0x48, - 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x20, 0x44, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x73, 0x20, 0x50, 0x43, 0x41, 0x20, 0x32, 0x30, 0x31, 0x38, 0x30, 0x76, 0x30, 0x10, - 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x22, - 0x03, 0x62, 0x00, 0x04, 0x1D, 0xDD, 0x08, 0x02, 0x03, 0x25, 0x75, 0x20, 0xE2, 0x71, 0x8B, 0xAD, - 0x28, 0x09, 0x82, 0xE9, 0x06, 0xEE, 0x83, 0xC5, 0x3A, 0x6C, 0x4B, 0x71, 0x92, 0x50, 0x4E, 0x20, - 0xE9, 0x72, 0xB4, 0xFC, 0x53, 0x2A, 0xEF, 0x5D, 0xCC, 0x9A, 0xB4, 0xCD, 0x76, 0xB8, 0x94, 0x97, - 0x44, 0xB2, 0x71, 0x0E, 0xC9, 0xB1, 0x16, 0x03, 0xA1, 0x65, 0x2B, 0xB9, 0xE8, 0x5D, 0x5F, 0xF2, - 0x30, 0x2E, 0xDD, 0xB1, 0x2B, 0x20, 0xFC, 0xBE, 0x00, 0x88, 0xEA, 0x1F, 0xA7, 0x7F, 0x99, 0x84, - 0x98, 0x7C, 0x71, 0x3E, 0x4D, 0x34, 0x83, 0x69, 0x9B, 0x08, 0xCB, 0x78, 0xB2, 0x4B, 0xBD, 0xD7, - 0x3E, 0xBE, 0x67, 0xA0, 0xA3, 0x82, 0x01, 0xFC, 0x30, 0x82, 0x01, 0xF8, 0x30, 0x10, 0x06, 0x09, - 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x15, 0x01, 0x04, 0x03, 0x02, 0x01, 0x00, 0x30, 0x1D, - 0x06, 0x03, 0x55, 0x1D, 0x0E, 0x04, 0x16, 0x04, 0x14, 0xDA, 0xCA, 0x4B, 0xD0, 0x4C, 0x56, 0x03, - 0x27, 0x5F, 0x97, 0xEB, 0x75, 0xA3, 0x02, 0xC3, 0xBF, 0x45, 0x9C, 0xF8, 0xB1, 0x30, 0x65, 0x06, - 0x03, 0x55, 0x1D, 0x20, 0x04, 0x5E, 0x30, 0x5C, 0x30, 0x06, 0x06, 0x04, 0x55, 0x1D, 0x20, 0x00, - 0x30, 0x52, 0x06, 0x0C, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x4C, 0x83, 0x7D, 0x01, 0x01, - 0x30, 0x42, 0x30, 0x40, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x01, 0x16, 0x34, - 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, 0x77, 0x77, 0x77, 0x2E, 0x6D, 0x69, 0x63, 0x72, 0x6F, - 0x73, 0x6F, 0x66, 0x74, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x70, 0x6B, 0x69, 0x6F, 0x70, 0x73, 0x2F, - 0x44, 0x6F, 0x63, 0x73, 0x2F, 0x52, 0x65, 0x70, 0x6F, 0x73, 0x69, 0x74, 0x6F, 0x72, 0x79, 0x2E, - 0x68, 0x74, 0x6D, 0x00, 0x30, 0x19, 0x06, 0x09, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x14, - 0x02, 0x04, 0x0C, 0x1E, 0x0A, 0x00, 0x53, 0x00, 0x75, 0x00, 0x62, 0x00, 0x43, 0x00, 0x41, 0x30, - 0x0B, 0x06, 0x03, 0x55, 0x1D, 0x0F, 0x04, 0x04, 0x03, 0x02, 0x01, 0x86, 0x30, 0x0F, 0x06, 0x03, - 0x55, 0x1D, 0x13, 0x01, 0x01, 0xFF, 0x04, 0x05, 0x30, 0x03, 0x01, 0x01, 0xFF, 0x30, 0x1F, 0x06, - 0x03, 0x55, 0x1D, 0x23, 0x04, 0x18, 0x30, 0x16, 0x80, 0x14, 0x14, 0xDA, 0x5B, 0xF1, 0x0E, 0x66, - 0x47, 0xD1, 0x5D, 0x13, 0x5F, 0x5B, 0x7A, 0xEB, 0xEB, 0x5F, 0x01, 0x08, 0xB5, 0x49, 0x30, 0x7A, - 0x06, 0x03, 0x55, 0x1D, 0x1F, 0x04, 0x73, 0x30, 0x71, 0x30, 0x6F, 0xA0, 0x6D, 0xA0, 0x6B, 0x86, - 0x69, 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, 0x77, 0x77, 0x77, 0x2E, 0x6D, 0x69, 0x63, 0x72, - 0x6F, 0x73, 0x6F, 0x66, 0x74, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x70, 0x6B, 0x69, 0x6F, 0x70, 0x73, - 0x2F, 0x63, 0x72, 0x6C, 0x2F, 0x4D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, 0x74, 0x25, 0x32, - 0x30, 0x45, 0x43, 0x43, 0x25, 0x32, 0x30, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x25, 0x32, - 0x30, 0x52, 0x6F, 0x6F, 0x74, 0x25, 0x32, 0x30, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x65, 0x25, 0x32, 0x30, 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x25, - 0x32, 0x30, 0x32, 0x30, 0x31, 0x37, 0x2E, 0x63, 0x72, 0x6C, 0x30, 0x81, 0x87, 0x06, 0x08, 0x2B, - 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x01, 0x04, 0x7B, 0x30, 0x79, 0x30, 0x77, 0x06, 0x08, 0x2B, - 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x02, 0x86, 0x6B, 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, - 0x77, 0x77, 0x77, 0x2E, 0x6D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, 0x74, 0x2E, 0x63, 0x6F, - 0x6D, 0x2F, 0x70, 0x6B, 0x69, 0x6F, 0x70, 0x73, 0x2F, 0x63, 0x65, 0x72, 0x74, 0x73, 0x2F, 0x4D, - 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, 0x74, 0x25, 0x32, 0x30, 0x45, 0x43, 0x43, 0x25, 0x32, - 0x30, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x25, 0x32, 0x30, 0x52, 0x6F, 0x6F, 0x74, 0x25, - 0x32, 0x30, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x25, 0x32, 0x30, - 0x41, 0x75, 0x74, 0x68, 0x6F, 0x72, 0x69, 0x74, 0x79, 0x25, 0x32, 0x30, 0x32, 0x30, 0x31, 0x37, - 0x2E, 0x63, 0x72, 0x74, 0x30, 0x0A, 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x03, - 0x03, 0x68, 0x00, 0x30, 0x65, 0x02, 0x30, 0x56, 0x2A, 0xAD, 0x72, 0x4C, 0xB9, 0x8C, 0xB3, 0x23, - 0x80, 0xF5, 0x5F, 0xF8, 0x21, 0x94, 0x66, 0x0F, 0x76, 0x77, 0xE2, 0x7B, 0x03, 0xDD, 0x30, 0x5E, - 0xCB, 0x90, 0xCA, 0x78, 0xE6, 0x0B, 0x2D, 0x12, 0xE5, 0xF7, 0x67, 0x31, 0x58, 0x71, 0xE6, 0xF3, - 0x64, 0xC1, 0x04, 0xB3, 0x8B, 0xE9, 0xE2, 0x02, 0x31, 0x00, 0xB9, 0x20, 0x61, 0xB9, 0xD0, 0x5E, - 0x3A, 0xA4, 0xA2, 0x8A, 0xFE, 0x1D, 0xFC, 0x27, 0x61, 0x0B, 0x98, 0x16, 0x8C, 0x02, 0x9C, 0x20, - 0x7F, 0xEE, 0xF3, 0xCB, 0x1F, 0x0A, 0x37, 0x62, 0xB1, 0x8E, 0xCE, 0xD9, 0x9A, 0x9E, 0xAC, 0xE6, - 0x1A, 0xD4, 0xB8, 0xF1, 0xA8, 0x2B, 0xB1, 0xB4, 0x40, 0x9B -}; - - -static void -msg_written_cb (GObject *source_object, - GAsyncResult *res, - gpointer user_data) -{ - GError *error = NULL; - GOutputStream *stream = G_OUTPUT_STREAM (source_object); - FpiSsm *ssm = user_data; - FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (fpi_ssm_get_device (ssm)); - - g_clear_pointer (&self->msg_out, g_byte_array_unref); - g_assert (self->msg_out == NULL); - - if (!g_output_stream_write_all_finish (stream, res, NULL, &error)) - { - fpi_ssm_mark_failed (ssm, error); - return; - } - - fpi_ssm_next_state (ssm); -} - -static void -msg_received_cb (GObject *source_object, - GAsyncResult *res, - gpointer user_data) -{ - GError *error = NULL; - GInputStream *stream = G_INPUT_STREAM (source_object); - FpiSsm *ssm = user_data; - FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (fpi_ssm_get_device (ssm)); - gsize read; - - g_assert (self->msg_out == NULL); - - if (!g_input_stream_read_all_finish (stream, res, &read, &error) || - read != self->msg_in->len) - { - g_clear_pointer (&self->msg_in, g_byte_array_unref); - - if (!error) - error = fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, - "Received EOF while reading from test binary."); - - fpi_ssm_mark_failed (ssm, error); - return; - } - - fpi_ssm_next_state (ssm); -} - -enum { - SEND_MESSAGE, - RECV_MESSAGE, - SEND_RECV_STATES -}; - -static void -send_recv_ssm (FpiSsm *ssm, FpDevice *dev) -{ - FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (dev); - - switch (fpi_ssm_get_cur_state (ssm)) - { - case SEND_MESSAGE: - g_output_stream_write_all_async (self->proc_stdin, - self->msg_out->data, - self->msg_out->len, - G_PRIORITY_DEFAULT, - fpi_device_get_cancellable (FP_DEVICE (dev)), - msg_written_cb, - ssm); - break; - - case RECV_MESSAGE: - g_input_stream_read_all_async (self->proc_stdout, - self->msg_in->data, - self->msg_in->len, - G_PRIORITY_DEFAULT, - fpi_device_get_cancellable (FP_DEVICE (dev)), - msg_received_cb, - ssm); - break; - } -} - -static void -connect_2_cb (FpiSsm *ssm, FpDevice *dev, GError *error) -{ - g_autoptr(GBytes) recv_data = NULL; - g_autoptr(GBytes) r_d = NULL; - g_autoptr(FpiSdcpClaim) claim = NULL; - g_autoptr(GBytes) mac = NULL; - g_autoptr(GBytes) ca_1_bytes = NULL, ca_2_bytes = NULL; - FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (dev); - guint16 cert_size; - - if (error) - { - fpi_sdcp_device_connect_complete (FP_SDCP_DEVICE (dev), NULL, NULL, NULL, error); - g_clear_pointer (&self->connect_msg, g_byte_array_unref); - return; - } - - memcpy (&cert_size, self->connect_msg->data + 32, 2); - g_byte_array_append (self->connect_msg, self->msg_in->data, self->msg_in->len); - g_clear_pointer (&self->msg_in, g_byte_array_unref); - /* Double check that the size is correct. */ - g_assert (self->connect_msg->len == 32 + (2 + cert_size + 65 + 65 + 32 + 64 + 64) + 32); - recv_data = g_byte_array_free_to_bytes (g_steal_pointer (&self->connect_msg)); - - claim = fpi_sdcp_claim_new (); - r_d = g_bytes_new_from_bytes (recv_data, 0, 32); - claim->cert_m = g_bytes_new_from_bytes (recv_data, 34, cert_size); - claim->pk_d = g_bytes_new_from_bytes (recv_data, 34 + cert_size, 65); - claim->pk_f = g_bytes_new_from_bytes (recv_data, 34 + cert_size + 65, 65); - claim->h_f = g_bytes_new_from_bytes (recv_data, 34 + cert_size + 65 + 65, 32); - claim->s_m = g_bytes_new_from_bytes (recv_data, 34 + cert_size + 65 + 65 + 32, 64); - claim->s_d = g_bytes_new_from_bytes (recv_data, 34 + cert_size + 65 + 65 + 32 + 64, 64); - mac = g_bytes_new_from_bytes (recv_data, 34 + cert_size + 65 + 65 + 32 + 64 + 64, 32); - - ca_1_bytes = g_bytes_new_static (ca_1, G_N_ELEMENTS (ca_1)); - ca_2_bytes = g_bytes_new_static (ca_2, G_N_ELEMENTS (ca_2)); - - fpi_sdcp_device_set_intermediat_cas (FP_SDCP_DEVICE (dev), - ca_1_bytes, - ca_2_bytes); - - fpi_sdcp_device_connect_complete (FP_SDCP_DEVICE (dev), r_d, claim, mac, NULL); -} - -static void -connect_1_cb (FpiSsm *ssm, FpDevice *dev, GError *error) -{ - FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (dev); - guint16 cert_size; - - if (error) - { - fpi_sdcp_device_connect_complete (FP_SDCP_DEVICE (dev), NULL, NULL, NULL, error); - return; - } - - g_clear_pointer (&self->connect_msg, g_byte_array_unref); - self->connect_msg = g_steal_pointer (&self->msg_in); - - memcpy (&cert_size, self->connect_msg->data + 32, 2); - - /* Nothing to send and the rest to receive. */ - self->msg_out = g_byte_array_new (); - self->msg_in = g_byte_array_new (); - g_byte_array_set_size (self->msg_in, 32 + (2 + cert_size + 65 + 65 + 32 + 64 + 64) + 32 - self->connect_msg->len); - - /* New SSM */ - ssm = fpi_ssm_new_full (FP_DEVICE (dev), send_recv_ssm, SEND_RECV_STATES, SEND_RECV_STATES, "connect 2"); - fpi_ssm_start (ssm, connect_2_cb); -} - -static void -connect (FpSdcpDevice *dev) -{ - g_autoptr(GBytes) r_h = NULL; - g_autoptr(GBytes) pk_h = NULL; - FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (dev); - FpiSsm *ssm; - - G_DEBUG_HERE (); - - g_assert (self->proc); - g_assert (self->connect_msg == NULL); - - fpi_sdcp_device_get_connect_data (dev, &r_h, &pk_h); - - self->msg_out = g_byte_array_new (); - g_byte_array_append (self->msg_out, (const guint8 *) "C", 1); - g_byte_array_append (self->msg_out, - g_bytes_get_data (r_h, NULL), - g_bytes_get_size (r_h)); - g_byte_array_append (self->msg_out, - g_bytes_get_data (pk_h, NULL), - g_bytes_get_size (pk_h)); - - self->msg_in = g_byte_array_new (); - g_byte_array_set_size (self->msg_in, 34); - - ssm = fpi_ssm_new_full (FP_DEVICE (dev), send_recv_ssm, SEND_RECV_STATES, SEND_RECV_STATES, "connect"); - fpi_ssm_start (ssm, connect_1_cb); -} - -static void -reconnect_cb (FpiSsm *ssm, FpDevice *dev, GError *error) -{ - g_autoptr(GBytes) mac = NULL; - FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (dev); - - if (error) - { - fpi_sdcp_device_reconnect_complete (FP_SDCP_DEVICE (dev), mac, error); - return; - } - - mac = g_byte_array_free_to_bytes (g_steal_pointer (&self->msg_in)); - - fpi_sdcp_device_reconnect_complete (FP_SDCP_DEVICE (dev), mac, NULL); -} - -static void -reconnect (FpSdcpDevice *dev) -{ - g_autoptr(GBytes) r_h = NULL; - FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (dev); - FpiSsm *ssm; - - G_DEBUG_HERE (); - - g_assert (self->proc); - - fpi_sdcp_device_get_reconnect_data (dev, &r_h); - - self->msg_out = g_byte_array_new (); - g_byte_array_append (self->msg_out, (const guint8 *) "R", 1); - g_byte_array_append (self->msg_out, - g_bytes_get_data (r_h, NULL), - g_bytes_get_size (r_h)); - - self->msg_in = g_byte_array_new (); - g_byte_array_set_size (self->msg_in, 32); - - ssm = fpi_ssm_new_full (FP_DEVICE (dev), send_recv_ssm, SEND_RECV_STATES, SEND_RECV_STATES, "connect 2"); - fpi_ssm_start (ssm, reconnect_cb); -} - -static void -enroll_begin_cb (FpiSsm *ssm, FpDevice *dev, GError *error) -{ - g_autoptr(GBytes) nonce = NULL; - FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (dev); - - if (error) - { - fpi_sdcp_device_enroll_ready (FP_SDCP_DEVICE (dev), error); - return; - } - - nonce = g_byte_array_free_to_bytes (g_steal_pointer (&self->msg_in)); - - fpi_sdcp_device_enroll_set_nonce (FP_SDCP_DEVICE (dev), nonce); - - /* Claim that we completed one enroll step. */ - fpi_device_enroll_progress (dev, 1, NULL, NULL); - - /* And signal that we are ready to commit. */ - fpi_sdcp_device_enroll_ready (FP_SDCP_DEVICE (dev), NULL); -} - -static void -enroll_begin (FpSdcpDevice *dev) -{ - g_autoptr(GBytes) r_h = NULL; - FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (dev); - FpiSsm *ssm; - - G_DEBUG_HERE (); - - g_assert (self->proc); - - fpi_sdcp_device_get_reconnect_data (dev, &r_h); - - self->msg_out = g_byte_array_new (); - g_byte_array_append (self->msg_out, (const guint8 *) "E", 1); - - /* Expect 32 byte nonce */ - self->msg_in = g_byte_array_new (); - g_byte_array_set_size (self->msg_in, 32); - - ssm = fpi_ssm_new_full (FP_DEVICE (dev), send_recv_ssm, SEND_RECV_STATES, SEND_RECV_STATES, "enroll_begin"); - fpi_ssm_start (ssm, enroll_begin_cb); -} - -static void -enroll_commit_cb (FpiSsm *ssm, FpDevice *dev, GError *error) -{ - FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (dev); - - g_clear_pointer (&self->msg_in, g_byte_array_unref); - - if (error) - { - fpi_sdcp_device_enroll_ready (FP_SDCP_DEVICE (dev), error); - return; - } - - /* Signal that we have committed. We don't expect a response - * from the virtual device (even though that is kind of broken). - */ - fpi_sdcp_device_enroll_commit_complete (FP_SDCP_DEVICE (dev), NULL); -} - -static void -enroll_commit (FpSdcpDevice *dev, GBytes *id_in) -{ - g_autoptr(GBytes) r_h = NULL; - g_autoptr(GBytes) id = id_in; - FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (dev); - FpiSsm *ssm; - - G_DEBUG_HERE (); - - g_assert (self->proc); - - fpi_sdcp_device_get_reconnect_data (dev, &r_h); - - self->msg_out = g_byte_array_new (); - self->msg_in = g_byte_array_new (); - if (id) - { - g_byte_array_append (self->msg_out, (const guint8 *) "F", 1); - g_byte_array_append (self->msg_out, - g_bytes_get_data (id, NULL), - g_bytes_get_size (id)); - - /* NOTE: No response from device, assume commit works. */ - } - else - { - /* Cancel enroll (does not receive a reply) */ - g_byte_array_append (self->msg_out, (const guint8 *) "G", 1); - - /* NOTE: No response from device, assume cancellation works. */ - } - - ssm = fpi_ssm_new_full (FP_DEVICE (dev), send_recv_ssm, SEND_RECV_STATES, SEND_RECV_STATES, "enroll_commit"); - fpi_ssm_start (ssm, enroll_commit_cb); -} - -static void -identify_cb (FpiSsm *ssm, FpDevice *dev, GError *error) -{ - g_autoptr(GBytes) reply = NULL; - g_autoptr(GBytes) id = NULL; - g_autoptr(GBytes) mac = NULL; - FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (dev); - - if (error) - { - fpi_sdcp_device_identify_complete (FP_SDCP_DEVICE (dev), NULL, NULL, error); - return; - } - - reply = g_byte_array_free_to_bytes (g_steal_pointer (&self->msg_in)); - id = g_bytes_new_from_bytes (reply, 0, 32); - mac = g_bytes_new_from_bytes (reply, 32, 32); - - fpi_sdcp_device_identify_complete (FP_SDCP_DEVICE (self), id, mac, NULL); -} - -static void -identify (FpSdcpDevice *dev) -{ - g_autoptr(GBytes) nonce = NULL; - FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (dev); - FpiSsm *ssm; - - G_DEBUG_HERE (); - - g_assert (self->proc); - - fpi_sdcp_device_get_identify_data (dev, &nonce); - - self->msg_out = g_byte_array_new (); - g_byte_array_append (self->msg_out, (const guint8 *) "I", 1); - g_byte_array_append (self->msg_out, g_bytes_get_data (nonce, NULL), g_bytes_get_size (nonce)); - - /* Expect 64 byte nonce */ - self->msg_in = g_byte_array_new (); - g_byte_array_set_size (self->msg_in, 64); - - ssm = fpi_ssm_new_full (FP_DEVICE (dev), send_recv_ssm, SEND_RECV_STATES, SEND_RECV_STATES, "identify"); - fpi_ssm_start (ssm, identify_cb); -} - -static void -probe (FpDevice *dev) -{ - g_auto(GStrv) argv = NULL; - FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (dev); - GError *error = NULL; - const char *env; - - /* We launch the test binary alread at probe time and quit only when - * the object is finalized. This allows testing reconnect properly. - * - * Also, we'll fail probe if something goes wrong executing it. - */ - env = fpi_device_get_virtual_env (FP_DEVICE (self)); - - if (!g_shell_parse_argv (env, NULL, &argv, &error)) - goto out; - - self->proc = g_subprocess_newv ((const char * const *) argv, - G_SUBPROCESS_FLAGS_STDIN_PIPE | G_SUBPROCESS_FLAGS_STDOUT_PIPE, - &error); - if (!self->proc) - goto out; - - self->proc_stdin = g_object_ref (g_subprocess_get_stdin_pipe (self->proc)); - self->proc_stdout = g_object_ref (g_subprocess_get_stdout_pipe (self->proc)); - - -out: - fpi_device_probe_complete (dev, "virtual-sdcp", NULL, error); -} - -static void -dev_close (FpDevice *dev) -{ - /* No-op, needs to be defined. */ - fpi_device_close_complete (dev, NULL); -} - -static void -fpi_device_virtual_sdcp_init (FpDeviceVirtualSdcp *self) -{ -} - -static void -fpi_device_virtual_sdcp_finalize (GObject *obj) -{ - FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (obj); - - /* Just kill the subprocess, no need to be graceful here. */ - if (self->proc) - g_subprocess_force_exit (self->proc); - - g_clear_object (&self->proc); - g_clear_object (&self->proc_stdin); - g_clear_object (&self->proc_stdout); - - G_OBJECT_CLASS (fpi_device_virtual_sdcp_parent_class)->finalize (obj); -} - -static const FpIdEntry driver_ids[] = { - { .virtual_envvar = "FP_VIRTUAL_SDCP" }, - { .virtual_envvar = NULL } -}; - -static void -fpi_device_virtual_sdcp_class_init (FpDeviceVirtualSdcpClass *klass) -{ - GObjectClass *obj_class = G_OBJECT_CLASS (klass); - FpDeviceClass *dev_class = FP_DEVICE_CLASS (klass); - FpSdcpDeviceClass *sdcp_class = FP_SDCP_DEVICE_CLASS (klass); - - obj_class->finalize = fpi_device_virtual_sdcp_finalize; - - dev_class->id = FP_COMPONENT; - dev_class->full_name = "Virtual SDCP device talking to MS test code"; - dev_class->type = FP_DEVICE_TYPE_VIRTUAL; - dev_class->id_table = driver_ids; - dev_class->nr_enroll_stages = 1; - - /* The SDCP base class may need to override this in the long run */ - dev_class->probe = probe; - dev_class->close = dev_close; - - sdcp_class->connect = connect; - sdcp_class->reconnect = reconnect; - - sdcp_class->enroll_begin = enroll_begin; - sdcp_class->enroll_commit = enroll_commit; - - sdcp_class->identify = identify; -} diff --git a/libfprint/fp-sdcp-device-private.h b/libfprint/fp-sdcp-device-private.h index 249c33c9..aa29445e 100644 --- a/libfprint/fp-sdcp-device-private.h +++ b/libfprint/fp-sdcp-device-private.h @@ -1,6 +1,7 @@ /* * FpSdcpDevice - A base class for SDCP enabled devices * Copyright (C) 2020 Benjamin Berg + * Copyright (C) 2025 Joshua Grisham * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -21,38 +22,26 @@ #include "fpi-sdcp-device.h" -#include -#include -#include -#include - typedef struct { - GError *enroll_pre_commit_error; - - /* XXX: Do we want a separate SDCP session object? - */ - - GPtrArray *intermediate_cas; - - /* Host random for the connection */ - guint8 host_random[32]; - - NSSInitContext *nss_init_context; - PK11SlotInfo *slot; - SECKEYPrivateKey *host_key_private; - SECKEYPublicKey *host_key_public; - - /* Master secret is required for reconnects. - * TODO: We probably want to serialize this to disk so it can survive - * fprintd idle shutdown. */ - PK11SymKey *master_secret; - PK11SymKey *mac_secret; - + GBytes *host_private_key; + GBytes *host_public_key; + GBytes *host_random; + GBytes *reconnect_random; + GBytes *identify_nonce; + GVariant *data; } FpSdcpDevicePrivate; +void fpi_sdcp_device_get_application_secret (FpSdcpDevice *self, + GBytes **application_secret); +void fpi_sdcp_device_set_application_secret (FpSdcpDevice *self, + GBytes *application_secret); +void fpi_sdcp_device_unset_application_secret (FpSdcpDevice *self); + +void fpi_sdcp_device_open (FpSdcpDevice *self); void fpi_sdcp_device_connect (FpSdcpDevice *self); void fpi_sdcp_device_reconnect (FpSdcpDevice *self); +void fpi_sdcp_device_list (FpSdcpDevice *self); void fpi_sdcp_device_enroll (FpSdcpDevice *self); void fpi_sdcp_device_identify (FpSdcpDevice *self); diff --git a/libfprint/fp-sdcp-device.c b/libfprint/fp-sdcp-device.c index 4cdac4e5..5c0d02cc 100644 --- a/libfprint/fp-sdcp-device.c +++ b/libfprint/fp-sdcp-device.c @@ -1,6 +1,7 @@ /* * FpSdcpDevice - A base class for SDCP enabled devices * Copyright (C) 2020 Benjamin Berg + * Copyright (C) 2025 Joshua Grisham * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -32,15 +33,13 @@ G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (FpSdcpDevice, fp_sdcp_device, FP_TYPE_DEVICE) -#if 0 -/* XXX: We'll very likely want/need some properties on this class. */ enum { PROP_0, + PROP_SDCP_DATA, N_PROPS }; static GParamSpec *properties[N_PROPS]; -#endif /*******************************************************/ @@ -49,13 +48,16 @@ static void fp_sdcp_device_open (FpDevice *device) { FpSdcpDevice *self = FP_SDCP_DEVICE (device); - FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); - /* Try a reconnect if we still have the mac secret. */ - if (priv->mac_secret) - fpi_sdcp_device_reconnect (self); - else - fpi_sdcp_device_connect (self); + fpi_sdcp_device_open (self); +} + +static void +fp_sdcp_device_list (FpDevice *device) +{ + FpSdcpDevice *self = FP_SDCP_DEVICE (device); + + fpi_sdcp_device_list (self); } static void @@ -82,13 +84,12 @@ fp_sdcp_device_finalize (GObject *object) FpSdcpDevice *self = (FpSdcpDevice *) object; FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); - g_clear_pointer (&priv->intermediate_cas, g_ptr_array_unref); - g_clear_pointer (&priv->slot, PK11_FreeSlot); - g_clear_pointer (&priv->host_key_private, SECKEY_DestroyPrivateKey); - g_clear_pointer (&priv->host_key_public, SECKEY_DestroyPublicKey); - g_clear_pointer (&priv->master_secret, PK11_FreeSymKey); - g_clear_pointer (&priv->mac_secret, PK11_FreeSymKey); - g_clear_pointer (&priv->nss_init_context, NSS_ShutdownContext); + g_clear_pointer (&priv->host_private_key, g_bytes_unref); + g_clear_pointer (&priv->host_public_key, g_bytes_unref); + g_clear_pointer (&priv->host_random, g_bytes_unref); + g_clear_pointer (&priv->reconnect_random, g_bytes_unref); + g_clear_pointer (&priv->identify_nonce, g_bytes_unref); + g_clear_pointer (&priv->data, g_variant_unref); G_OBJECT_CLASS (fp_sdcp_device_parent_class)->finalize (object); } @@ -99,8 +100,36 @@ fp_sdcp_device_get_property (GObject *object, GValue *value, GParamSpec *pspec) { + FpSdcpDevice *self = (FpSdcpDevice *) object; + FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + switch (prop_id) { + case PROP_SDCP_DATA: + g_value_set_variant (value, priv->data); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +fp_sdcp_device_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + FpSdcpDevice *self = FP_SDCP_DEVICE (object); + FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + + switch (prop_id) + { + case PROP_SDCP_DATA: + g_clear_pointer (&priv->data, g_variant_unref); + priv->data = g_value_dup_variant (value); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } @@ -120,22 +149,30 @@ fp_sdcp_device_class_init (FpSdcpDeviceClass *klass) object_class->finalize = fp_sdcp_device_finalize; object_class->get_property = fp_sdcp_device_get_property; + object_class->set_property = fp_sdcp_device_set_property; object_class->constructed = fp_sdcp_device_constructed; fp_device_class->open = fp_sdcp_device_open; + fp_device_class->list = fp_sdcp_device_list; fp_device_class->enroll = fp_sdcp_device_enroll; fp_device_class->verify = fp_sdcp_device_identify; fp_device_class->identify = fp_sdcp_device_identify; -#if 0 + properties[PROP_SDCP_DATA] = + g_param_spec_variant ("sdcp-data", + "SDCP Data", + "SDCP-related device data that should be persisted and used with the " + "device during the current system boot", + G_VARIANT_TYPE_ANY, + NULL, + G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE); + g_object_class_install_properties (object_class, N_PROPS, properties); -#endif + + fpi_device_class_auto_initialize_features (fp_device_class); } static void fp_sdcp_device_init (FpSdcpDevice *self) { - FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); - - priv->intermediate_cas = g_ptr_array_new_with_free_func ((GDestroyNotify) g_bytes_unref); } diff --git a/libfprint/fpi-log.c b/libfprint/fpi-log.c new file mode 100644 index 00000000..679b2570 --- /dev/null +++ b/libfprint/fpi-log.c @@ -0,0 +1,56 @@ +/* + * FpiLog - Internal logging functions + * Copyright (C) 2020 Benjamin Berg + * Copyright (C) 2025 Joshua Grisham + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "fpi-log.h" + +#undef fp_dbg_hex_dump_bytes +#undef fp_dbg_hex_dump_gbytes + +void +fp_dbg_hex_dump_bytes (const gchar *log_domain, + const guint8 *buf, + gsize len) +{ + g_autoptr(GString) line = NULL; + + line = g_string_new (""); + + for (gint i = 0; i < len; i++) + { + g_string_append_printf (line, "%02x ", buf[i]); + if ((i + 1) % 16 == 0) + { + g_log (log_domain, G_LOG_LEVEL_DEBUG, "%s", line->str); + g_string_set_size (line, 0); + } + } + + if (line->len) + g_log (log_domain, G_LOG_LEVEL_DEBUG, "%s", line->str); +} + +void +fp_dbg_hex_dump_gbytes (const gchar *log_domain, + GBytes *gbytes) +{ + gsize len = 0; + const guint8 *buf = g_bytes_get_data (gbytes, &len); + fp_dbg_hex_dump_bytes (log_domain, buf, len); +} diff --git a/libfprint/fpi-log.h b/libfprint/fpi-log.h index cd8f1bd5..1222854e 100644 --- a/libfprint/fpi-log.h +++ b/libfprint/fpi-log.h @@ -96,3 +96,32 @@ * Same as BUG_ON() but is always true. */ #define BUG() BUG_ON (1) + +/* + * Custom-defined logging functions are wrapped in macros for convenience so + * that the caller does not have to pass G_LOG_DOMAIN every time. + */ + +void fp_dbg_hex_dump_bytes (const gchar *log_domain, + const guint8 *buf, + gsize len); + +/** + * fp_dbg_hex_dump_bytes: + * @buf: Bytes buffer to dump + * @len: Length of @buf to dump + * + * Prints hex dump of @buf to fp_dbg() + */ +#define fp_dbg_hex_dump_bytes(buf, len) fp_dbg_hex_dump_bytes (G_LOG_DOMAIN, buf, len) + +void fp_dbg_hex_dump_gbytes (const gchar *log_domain, + GBytes *gbytes); + +/** + * fp_dbg_hex_dump_gbytes: + * @gbytes: #GBytes to dump + * + * Prints hex dump of @gbytes to fp_dbg() + */ +#define fp_dbg_hex_dump_gbytes(gbytes) fp_dbg_hex_dump_gbytes (G_LOG_DOMAIN, gbytes) diff --git a/libfprint/fpi-sdcp-device.c b/libfprint/fpi-sdcp-device.c index f594cca0..19683f09 100644 --- a/libfprint/fpi-sdcp-device.c +++ b/libfprint/fpi-sdcp-device.c @@ -1,6 +1,7 @@ /* * FpSdcpDevice - A base class for SDCP enabled devices * Copyright (C) 2020 Benjamin Berg + * Copyright (C) 2025 Joshua Grisham * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -20,18 +21,17 @@ #define FP_COMPONENT "sdcp_device" #include "fpi-log.h" -#include -#include - #include "fpi-compat.h" -#include "fp-sdcp-device-private.h" -#include "fpi-sdcp-device.h" #include "fpi-print.h" +#include "fp-sdcp-device-private.h" +#include "fpi-sdcp.h" +#include "fpi-sdcp-device.h" + /** * SECTION: fpi-sdcp-device * @title: Internal FpSdcpDevice - * @short_description: Internal SDCP Device routines + * @short_description: Internal SDCP device routines * * Internal SDCP handling routines. See #FpSdcpDevice for public routines. */ @@ -39,19 +39,6 @@ G_DEFINE_BOXED_TYPE (FpiSdcpClaim, fpi_sdcp_claim, fpi_sdcp_claim_copy, fpi_sdcp_claim_free) -/* - * Static, but could be created at runtime using: - * oid_data = SECOID_FindOIDByTag (SEC_OID_SECG_EC_SECP256R1); - * ec_parameters_der.len = oid_data->oid.len + 2; - * ec_parameters_der.data = ec_params_data = g_malloc0 (oid_data->oid.len + 2); - * ec_parameters_der.data[0] = SEC_ASN1_OBJECT_ID; - * ec_parameters_der.data[1] = oid_data->oid.len; - */ -const SECItem SDCPECParamsDER = { - .len = 10, - .data = (guint8[]){ 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07 } -}; - /** * fpi_sdcp_claim_new: * @@ -76,16 +63,16 @@ fpi_sdcp_claim_new (void) * Release the memory used by an #FpiSdcpClaim. */ void -fpi_sdcp_claim_free (FpiSdcpClaim * claim) +fpi_sdcp_claim_free (FpiSdcpClaim *claim) { g_return_if_fail (claim); - g_clear_pointer (&claim->cert_m, g_bytes_unref); - g_clear_pointer (&claim->pk_d, g_bytes_unref); - g_clear_pointer (&claim->pk_f, g_bytes_unref); - g_clear_pointer (&claim->h_f, g_bytes_unref); - g_clear_pointer (&claim->s_m, g_bytes_unref); - g_clear_pointer (&claim->s_d, g_bytes_unref); + g_clear_pointer (&claim->model_certificate, g_bytes_unref); + g_clear_pointer (&claim->device_public_key, g_bytes_unref); + g_clear_pointer (&claim->firmware_public_key, g_bytes_unref); + g_clear_pointer (&claim->firmware_hash, g_bytes_unref); + g_clear_pointer (&claim->model_signature, g_bytes_unref); + g_clear_pointer (&claim->device_signature, g_bytes_unref); g_free (claim); } @@ -105,48 +92,22 @@ fpi_sdcp_claim_copy (FpiSdcpClaim *other) res = fpi_sdcp_claim_new (); - if (other->cert_m) - res->cert_m = g_bytes_ref (other->cert_m); - if (other->pk_d) - res->pk_d = g_bytes_ref (other->pk_d); - if (other->pk_f) - res->pk_f = g_bytes_ref (other->pk_f); - if (other->h_f) - res->h_f = g_bytes_ref (other->h_f); - if (other->s_m) - res->s_m = g_bytes_ref (other->s_m); - if (other->s_d) - res->s_d = g_bytes_ref (other->s_d); + if (other->model_certificate) + res->model_certificate = g_bytes_ref (other->model_certificate); + if (other->device_public_key) + res->device_public_key = g_bytes_ref (other->device_public_key); + if (other->firmware_public_key) + res->firmware_public_key = g_bytes_ref (other->firmware_public_key); + if (other->firmware_hash) + res->firmware_hash = g_bytes_ref (other->firmware_hash); + if (other->model_signature) + res->model_signature = g_bytes_ref (other->model_signature); + if (other->device_signature) + res->device_signature = g_bytes_ref (other->device_signature); return res; } -static void -dump_bytes (GBytes *d) -{ - g_autoptr(GString) line = NULL; - const guint8 *dump_data; - gsize dump_len; - - dump_data = g_bytes_get_data (d, &dump_len); - - line = g_string_new (""); - /* Dump the buffer. */ - for (gint i = 0; i < dump_len; i++) - { - g_string_append_printf (line, "%02x ", dump_data[i]); - if ((i + 1) % 16 == 0) - { - g_debug ("%s", line->str); - g_string_set_size (line, 0); - } - } - - if (line->len) - g_debug ("%s", line->str); - -} - /* Manually redefine what G_DEFINE_* macro does */ static inline gpointer fp_sdcp_device_get_instance_private (FpSdcpDevice *self) @@ -157,376 +118,146 @@ fp_sdcp_device_get_instance_private (FpSdcpDevice *self) g_type_class_get_instance_private_offset (sdcp_class)); } -/** - * fpi_sdcp_generate_random: - * @buffer: Buffer to place random bytes into - * @len: Number of bytes to generate - * @error: Error out - * - * Returns: #TRUE on success - **/ -FP_GNUC_ACCESS (write_only, 1, 2) -static gboolean -fpi_sdcp_generate_random (guint8 *buffer, gsize len, GError **error) -{ - /* Just use a counter in emulation mode. Not random, but all - * we need is something predictable and not repeating immediately. - */ - if (g_strcmp0 (g_getenv ("FP_DEVICE_EMULATION"), "1") == 0) - { - static guint8 emulation_rand = 0; - gsize i; - - for (i = 0; i < len; i++) - { - buffer[i] = emulation_rand; - emulation_rand += 1; - } - - return TRUE; - } - - /* Generating random numbers is basic enough to assume it works */ - if (PK11_GenerateRandom (buffer, len) != SECSuccess) - { - g_propagate_error (error, - fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, - "Error generating random numbers using NSS!")); - return FALSE; - } - - return TRUE; -} - -/** - * fpi_sdcp_kdf: - * @self: The #FpSdcpDevice - * @baseKey: The key to base it on - * @data_a: (nullable): First data segment to concatenate - * @data_b: (nullable): Second data segment to concatenate - * @out_key_2: (nullable) (out): Second output key or %NULL. - * @error: (out): #GError in case the return value is %NULL - * - * Convenience function to calculate a KDF with a specific label - * and up to two data segments that are concatinated. The returned - * keys will be 32bytes (256bit) in length. If @out_key_2 is set - * then two keys will be generated. - * - * Returns: A new #PK11SymKey of length @bitlength - **/ -static PK11SymKey * -fpi_sdcp_kdf (FpSdcpDevice *self, - PK11SymKey *baseKey, - const gchar *label, - GBytes *data_a, - GBytes *data_b, - PK11SymKey **out_key_2, - GError **error) -{ - PK11SymKey * res = NULL; - CK_SP800_108_KDF_PARAMS kdf_params; - CK_SP800_108_COUNTER_FORMAT counter_format; - CK_SP800_108_DKM_LENGTH_FORMAT length_format; - CK_DERIVED_KEY additional_key; - CK_ATTRIBUTE additional_key_attrs[2]; - CK_ULONG attr_type, attr_len; - CK_OBJECT_HANDLE out_key_handle = 0; - CK_PRF_DATA_PARAM data_param[5]; - SECItem params; - - kdf_params.prfType = CKM_SHA256_HMAC; - kdf_params.pDataParams = data_param; - - /* First item is the counter */ - counter_format.bLittleEndian = FALSE; - counter_format.ulWidthInBits = 32; - data_param[0].type = CK_SP800_108_ITERATION_VARIABLE; - data_param[0].pValue = &counter_format; - data_param[0].ulValueLen = sizeof (counter_format); - kdf_params.ulNumberOfDataParams = 1; - - /* Then the label */ - data_param[kdf_params.ulNumberOfDataParams].type = CK_SP800_108_BYTE_ARRAY; - data_param[kdf_params.ulNumberOfDataParams].pValue = (guint8 *) label; - data_param[kdf_params.ulNumberOfDataParams].ulValueLen = strlen (label) + 1; - kdf_params.ulNumberOfDataParams += 1; - - /* Then the context a */ - if (data_a) - { - data_param[kdf_params.ulNumberOfDataParams].type = CK_SP800_108_BYTE_ARRAY; - data_param[kdf_params.ulNumberOfDataParams].pValue = (guint8 *) g_bytes_get_data (data_a, NULL); - data_param[kdf_params.ulNumberOfDataParams].ulValueLen = g_bytes_get_size (data_a); - kdf_params.ulNumberOfDataParams += 1; - } - - /* Then the context b */ - if (data_b) - { - data_param[kdf_params.ulNumberOfDataParams].type = CK_SP800_108_BYTE_ARRAY; - data_param[kdf_params.ulNumberOfDataParams].pValue = (guint8 *) g_bytes_get_data (data_b, NULL); - data_param[kdf_params.ulNumberOfDataParams].ulValueLen = g_bytes_get_size (data_b); - kdf_params.ulNumberOfDataParams += 1; - } - - /* And the output length */ - length_format.dkmLengthMethod = CK_SP800_108_DKM_LENGTH_SUM_OF_KEYS; - length_format.bLittleEndian = FALSE; - length_format.ulWidthInBits = 32; - data_param[kdf_params.ulNumberOfDataParams].type = CK_SP800_108_DKM_LENGTH; - data_param[kdf_params.ulNumberOfDataParams].pValue = &length_format; - data_param[kdf_params.ulNumberOfDataParams].ulValueLen = sizeof (length_format); - kdf_params.ulNumberOfDataParams += 1; - - kdf_params.ulAdditionalDerivedKeys = 0; - kdf_params.pAdditionalDerivedKeys = NULL; - if (out_key_2) - { - attr_type = CKK_SHA256_HMAC; - attr_len = 256 / 8; - additional_key_attrs[0].type = CKA_KEY_TYPE; - additional_key_attrs[0].pValue = &attr_type; - additional_key_attrs[0].ulValueLen = sizeof (attr_type); - additional_key_attrs[1].type = CKA_VALUE_LEN; - additional_key_attrs[1].pValue = &attr_len; - additional_key_attrs[1].ulValueLen = sizeof (attr_len); - - additional_key.pTemplate = additional_key_attrs; - additional_key.ulAttributeCount = 2; - additional_key.phKey = &out_key_handle; - - kdf_params.ulAdditionalDerivedKeys = 1; - kdf_params.pAdditionalDerivedKeys = &additional_key; - } - - params.len = sizeof (kdf_params); - params.data = (guint8 *) &kdf_params; - res = PK11_Derive (baseKey, - CKM_SP800_108_COUNTER_KDF, - ¶ms, - CKM_SHA256_HMAC, - CKA_SIGN, - 256 / 8); - if (!res) - { - g_propagate_error (error, - fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, - "Error deriving secret (label: %s): %d", - label, - PORT_GetError ())); - } - - if (out_key_2) - *out_key_2 = PK11_SymKeyFromHandle (PK11_GetSlotFromKey (baseKey), res, CKO_DATA, CKM_NULL, out_key_handle, FALSE, NULL); - - return res; -} - -/** - * fpi_sdcp_mac: - * @self: The #FpSdcpDevice - * @baseKey: The key to base it on - * @data_a: (nullable): Data segment a to concatenate - * @data_b: (nullable): Data segment b to concatenate - * @error: (out): #GError in case the return value is %NULL - * - * Convenience function to calculate a MAC with a specific label - * and a generic data segments that is concatenated. - * - * Returns: A new #PK11SymKey - **/ -static GBytes * -fpi_sdcp_mac (FpSdcpDevice *self, - const gchar *label, - GBytes *data_a, - GBytes *data_b, - GError **error) -{ - g_autoptr(GBytes) res = NULL; - FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); - SECStatus r; - SECItem input, output; - g_autofree guint8 *data = NULL; - gsize label_len; - gsize data_a_len = 0; - gsize data_b_len = 0; - gsize length; - - label_len = strlen (label) + 1; - length = label_len; - if (data_a) - data_a_len = g_bytes_get_size (data_a); - if (data_b) - data_b_len = g_bytes_get_size (data_b); - - length += data_a_len + data_b_len; - - data = g_malloc (length); - - memcpy (data, label, label_len); - if (data_a) - memcpy (data + label_len, g_bytes_get_data (data_a, NULL), data_a_len); - if (data_b) - memcpy (data + label_len + data_a_len, g_bytes_get_data (data_b, NULL), data_b_len); - - input.len = length; - input.data = data; - output.len = 32; - output.data = g_malloc0 (32); - res = g_bytes_new_take (output.data, output.len); - - r = PK11_SignWithSymKey (priv->mac_secret, - CKM_SHA256_HMAC, - NULL, - &output, - &input); - if (r != SECSuccess) - { - g_propagate_error (error, - fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, - "Error calculating MAC (label: %s): %d", - label, - r)); - } - - return g_steal_pointer (&res); -} +/* Example values from Microsoft's SDCP documentation to use when testing (FP_DEVICE_EMULATION=1) */ +static const guchar test_host_private_key[] = { + 0x84, 0x00, 0xed, 0x14, 0x57, 0x9c, 0xdf, 0x11, 0x58, 0x64, 0x77, 0xe8, 0x36, 0xe8, 0xcb, 0x52, + 0x70, 0x84, 0x41, 0xc1, 0xc2, 0xa4, 0x47, 0xc2, 0x18, 0xc5, 0xbb, 0xc2, 0xd1, 0x18, 0xfb, 0xc7 +}; +static const guchar test_host_public_key[] = { + 0x04, 0x52, 0xf0, 0x56, 0xff, 0xb9, 0xc6, 0x72, 0x86, 0x54, 0x77, 0x1a, 0x36, 0x29, 0xb7, 0x70, + 0x76, 0x7b, 0x19, 0xa2, 0x10, 0x6a, 0x49, 0x16, 0xfb, 0x81, 0xba, 0x06, 0xef, 0x67, 0x97, 0xc4, + 0xa3, 0xdf, 0x67, 0x2a, 0xde, 0x0e, 0x91, 0x16, 0xd1, 0xab, 0xe2, 0x78, 0xa8, 0x22, 0x3a, 0xbd, + 0xe4, 0x95, 0x8d, 0x62, 0xd4, 0xff, 0x68, 0x82, 0x15, 0x9f, 0x06, 0x17, 0xc6, 0xf8, 0xce, 0x10, + 0xbf +}; +static const gchar test_host_random[] = { + 0xd8, 0x77, 0x40, 0x3a, 0xbe, 0x82, 0xf4, 0xd9, 0x7e, 0x14, 0x48, 0xc5, 0x05, 0x2d, 0x83, 0xa5, + 0x32, 0xa4, 0x5e, 0x56, 0xef, 0x04, 0x9c, 0xbb, 0xf9, 0x81, 0x13, 0x75, 0x20, 0xe7, 0x13, 0xbf +}; +static const gchar test_reconnect_random[] = { + 0x8a, 0x74, 0x51, 0xc1, 0xd3, 0xa8, 0xdc, 0xa1, 0xc1, 0x33, 0x0c, 0xa5, 0x0d, 0x73, 0x45, 0x4b, + 0x35, 0x1a, 0x49, 0xf4, 0x6c, 0x8e, 0x9d, 0xce, 0xe1, 0x5c, 0x96, 0x4d, 0x29, 0x5c, 0x31, 0xc9 +}; +static const gchar test_identify_nonce[] = { + 0x3a, 0x1b, 0x50, 0x6f, 0x5b, 0xec, 0x08, 0x90, 0x59, 0xac, 0xef, 0xb9, 0xb4, 0x4d, 0xfb, 0xde, + 0xa7, 0xa5, 0x99, 0xee, 0x9a, 0xa2, 0x67, 0xe5, 0x25, 0x26, 0x64, 0xd6, 0x0b, 0x79, 0x80, 0x53 +}; /* FpiSdcpDevice */ /* Internal functions of FpSdcpDevice */ + +void +fpi_sdcp_device_get_application_secret (FpSdcpDevice *self, + GBytes **application_secret) +{ + g_autoptr(GVariant) data = NULL; + g_autoptr(GVariant) application_secret_var = NULL; + const guint8 *application_secret_data; + gsize application_secret_len = 0; + + g_return_if_fail (*application_secret == NULL); + + g_object_get (G_OBJECT (self), "sdcp-data", &data, NULL); + + if (!data) + return; + + if (!g_variant_check_format_string (data, "(@ay)", FALSE)) + { + fp_warn ("SDCP data is not in expected format."); + return; + } + + g_variant_get (data, "(@ay)", &application_secret_var); + + application_secret_data = g_variant_get_fixed_array (application_secret_var, + &application_secret_len, + sizeof (guint8)); + + *application_secret = g_bytes_new (application_secret_data, application_secret_len); +} + +void +fpi_sdcp_device_set_application_secret (FpSdcpDevice *self, + GBytes *application_secret) +{ + GVariant *application_secret_var; + GVariant *data; + + g_return_if_fail (application_secret); + + application_secret_var = g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE, + g_bytes_get_data (application_secret, NULL), + g_bytes_get_size (application_secret), + sizeof (guint8)); + data = g_variant_new ("(@ay)", application_secret_var); + + g_object_set (G_OBJECT (self), "sdcp-data", data, NULL); +} + +void +fpi_sdcp_device_unset_application_secret (FpSdcpDevice *self) +{ + g_object_set (G_OBJECT (self), "sdcp-data", NULL); +} + +void +fpi_sdcp_device_open (FpSdcpDevice *self) +{ + FpSdcpDeviceClass *cls = FP_SDCP_DEVICE_GET_CLASS (self); + + g_return_if_fail (FP_IS_SDCP_DEVICE (self)); + g_return_if_fail (fpi_device_get_current_action (FP_DEVICE (self)) == FPI_DEVICE_ACTION_OPEN); + + cls->open (self); +} + void fpi_sdcp_device_connect (FpSdcpDevice *self) { - G_GNUC_UNUSED g_autofree void * ec_params_data = NULL; FpSdcpDeviceClass *cls = FP_SDCP_DEVICE_GET_CLASS (self); FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); GError *error = NULL; - SECStatus r = SECSuccess; + g_clear_pointer (&priv->host_private_key, g_bytes_unref); + g_clear_pointer (&priv->host_public_key, g_bytes_unref); + g_clear_pointer (&priv->host_random, g_bytes_unref); - /* Disable loading p11-kit's user configuration */ - g_setenv ("P11_KIT_NO_USER_CONFIG", "1", TRUE); - - /* Initialise NSS; Same as NSS_NoDB_Init but using a context. */ - if (!priv->nss_init_context) + if (g_strcmp0 (g_getenv ("FP_DEVICE_EMULATION"), "1") != 0) { - priv->nss_init_context = NSS_InitContext ("", "", "", "", NULL, - NSS_INIT_READONLY | - NSS_INIT_NOCERTDB | - NSS_INIT_NOMODDB | - NSS_INIT_FORCEOPEN | - NSS_INIT_NOROOTINIT | - NSS_INIT_OPTIMIZESPACE); - } - if (!priv->nss_init_context) - goto nss_error; - - g_clear_pointer (&priv->slot, PK11_FreeSlot); - g_clear_pointer (&priv->host_key_private, SECKEY_DestroyPrivateKey); - g_clear_pointer (&priv->host_key_public, SECKEY_DestroyPublicKey); - priv->host_key_private = NULL; - - /* SDCP Connect: 3.i. Generate an ephemeral ECDH key pair */ - /* Look up the OID data for our curve. */ - - /* Just use a counter in emulation mode. Not random, but all - * we need is something predictable and not repeating immediately. - */ - if (g_strcmp0 (g_getenv ("FP_DEVICE_EMULATION"), "1") == 0) - { - /* To generate, use the #if 0 code below and remove the readOnly flag */ - priv->slot = SECMOD_OpenUserDB ("configdir='sdcp-key-db' tokenDescription='libfprint CI testing' flags=readOnly"); - if (!priv->slot) + /* SDCP Connect: 3.i. Generate host ephemeral ECDH key pair */ + fpi_sdcp_generate_host_key (&priv->host_private_key, &priv->host_public_key, &error); + if (error) { - g_message ("Could not open key DB for testing"); - exit (77); + fpi_sdcp_device_connect_complete (self, + NULL, NULL, NULL, + error); + return; } -#if 0 - if (PK11_NeedUserInit (priv->slot)) - if (PK11_InitPin (priv->slot, "", "") != SECSuccess) - goto nss_error; - - if (priv->slot == NULL) - goto nss_error; - g_debug ("logged in: %i, need: %i", PK11_IsLoggedIn (priv->slot, NULL), PK11_NeedLogin (priv->slot)); - g_debug ("read only: %i", PK11_IsReadOnly (priv->slot)); - g_debug ("need user init: %i", PK11_NeedUserInit (priv->slot)); - //PK11_SetPasswordFunc (pwfunc); - - /* SDCP Connect: 3.i. Generate an ephemeral ECDH key pair */ - /* Look up the OID data for our curve. */ - oid_data = SECOID_FindOIDByTag (SEC_OID_SECG_EC_SECP256R1); - if (!oid_data) - goto nss_error; - - priv->host_key_private = PK11_GenerateKeyPair (priv->slot, CKM_EC_KEY_PAIR_GEN, - (SECItem *) &SDCPECParamsDER, - &priv->host_key_public, - TRUE, FALSE, - NULL); - - PK11_SetPrivateKeyNickname (priv->host_key_private, "CI testing"); - PK11_SetPublicKeyNickname (priv->host_key_public, "CI testing"); -#else - - g_assert (!PK11_NeedUserInit (priv->slot)); - g_assert (PK11_IsReadOnly (priv->slot)); - - SECKEYPrivateKeyList *priv_key_list = NULL; - SECKEYPublicKeyList *pub_key_list = NULL; - - priv_key_list = PK11_ListPrivKeysInSlot (priv->slot, (char *) "CI testing", NULL); - pub_key_list = PK11_ListPublicKeysInSlot (priv->slot, (char *) "CI testing"); - g_assert (priv_key_list != NULL && pub_key_list != NULL); - g_assert (!PR_CLIST_IS_EMPTY (&priv_key_list->list) && !PR_CLIST_IS_EMPTY (&pub_key_list->list)); - - priv->host_key_private = SECKEY_CopyPrivateKey (((SECKEYPrivateKeyListNode *) PR_LIST_HEAD (&priv_key_list->list))->key); - priv->host_key_public = SECKEY_CopyPublicKey (((SECKEYPublicKeyListNode *) PR_LIST_HEAD (&pub_key_list->list))->key); - - SECKEY_DestroyPrivateKeyList (priv_key_list); - SECKEY_DestroyPublicKeyList (pub_key_list); -#endif + /* SDCP Connect: 3.ii. Generate host random */ + priv->host_random = fpi_sdcp_generate_random (&error); + if (error) + { + fpi_sdcp_device_connect_complete (self, + NULL, NULL, NULL, + error); + return; + } } else { - /* Create a slot for PK11 operation */ - priv->slot = PK11_GetBestSlot (CKM_EC_KEY_PAIR_GEN, NULL); - if (priv->slot == NULL) - goto nss_error; - - priv->host_key_private = PK11_GenerateKeyPair (priv->slot, CKM_EC_KEY_PAIR_GEN, - (SECItem *) &SDCPECParamsDER, - &priv->host_key_public, - FALSE, TRUE, - NULL); - } - - if (r != SECSuccess) - goto nss_error; - - /* SDCP Connect: 3.ii. Generate host random */ - if (!fpi_sdcp_generate_random (priv->host_random, sizeof (priv->host_random), &error)) - { - fpi_sdcp_device_connect_complete (self, - NULL, NULL, NULL, - error); - return; + /* Use Microsoft's SDCP documentation example values in emulation mode */ + priv->host_private_key = g_bytes_new (test_host_private_key, sizeof (test_host_private_key)); + priv->host_public_key = g_bytes_new (test_host_public_key, sizeof (test_host_public_key)); + priv->host_random = g_bytes_new (test_host_random, sizeof (test_host_random)); } /* SDCP Connect: 3.iii. Send the Connect message */ cls->connect (self); return; - -nss_error: - if (r == SECSuccess) - r = PORT_GetError (); - fpi_sdcp_device_connect_complete (self, - NULL, NULL, NULL, - fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, - "Error calling NSS crypto routine: %d", r)); } void @@ -536,11 +267,22 @@ fpi_sdcp_device_reconnect (FpSdcpDevice *self) FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); GError *error = NULL; - /* SDCP Reconnect: 2.i. Generate host random */ - if (!fpi_sdcp_generate_random (priv->host_random, sizeof (priv->host_random), &error)) + g_clear_pointer (&priv->reconnect_random, g_bytes_unref); + + if (g_strcmp0 (g_getenv ("FP_DEVICE_EMULATION"), "1") != 0) { - fpi_sdcp_device_reconnect_complete (self, NULL, error); - return; + /* SDCP Reconnect: 2.i. Generate host random */ + priv->reconnect_random = fpi_sdcp_generate_random (&error); + if (error) + { + fpi_sdcp_device_reconnect_complete (self, NULL, error); + return; + } + } + else + { + /* Use Microsoft's SDCP documentation example value in emulation mode */ + priv->reconnect_random = g_bytes_new (test_reconnect_random, sizeof (test_reconnect_random)); } /* SDCP Reconnect: 2.ii. Send the Reconnect message */ @@ -550,6 +292,17 @@ fpi_sdcp_device_reconnect (FpSdcpDevice *self) fpi_sdcp_device_connect (self); } +void +fpi_sdcp_device_list (FpSdcpDevice *self) +{ + FpSdcpDeviceClass *cls = FP_SDCP_DEVICE_GET_CLASS (self); + + g_return_if_fail (FP_IS_SDCP_DEVICE (self)); + g_return_if_fail (fpi_device_get_current_action (FP_DEVICE (self)) == FPI_DEVICE_ACTION_LIST); + + cls->list (self); +} + void fpi_sdcp_device_enroll (FpSdcpDevice *self) { @@ -567,7 +320,7 @@ fpi_sdcp_device_enroll (FpSdcpDevice *self) /* For enrollment, all we need to do is start the process. But just to be sure, * clear a bit of internal state. */ - cls->enroll_begin (self); + cls->enroll (self); } void @@ -583,11 +336,22 @@ fpi_sdcp_device_identify (FpSdcpDevice *self) g_return_if_fail (action == FPI_DEVICE_ACTION_IDENTIFY || action == FPI_DEVICE_ACTION_VERIFY); - /* Generate a new nonce. */ - if (!fpi_sdcp_generate_random (priv->host_random, sizeof (priv->host_random), &error)) + g_clear_pointer (&priv->identify_nonce, g_bytes_unref); + + if (g_strcmp0 (g_getenv ("FP_DEVICE_EMULATION"), "1") != 0) { - fpi_device_action_error (FP_DEVICE (self), error); - return; + /* Generate a new nonce. */ + priv->identify_nonce = fpi_sdcp_generate_random (&error); + if (error) + { + fpi_device_action_error (FP_DEVICE (self), error); + return; + } + } + else + { + /* Use Microsoft's SDCP documentation example value in emulation mode */ + priv->identify_nonce = g_bytes_new (test_identify_nonce, sizeof (test_identify_nonce)); } cls->identify (self); @@ -597,72 +361,92 @@ fpi_sdcp_device_identify (FpSdcpDevice *self) /* Private API */ /** - * fp_sdcp_device_set_intermediate_cas: - * @self: The #FpSdcpDevice - * @ca_1: (transfer none): DER encoded intermediate CA certificate #1 - * @ca_2: (transfer none): DER encoded intermediate CA certificate #2 + * fpi_sdcp_device_open_complete: + * @self: a #FpSdcpDevice fingerprint device + * @error: A #GError or %NULL on success * - * Set the intermediate CAs used by the device. + * Reports completion of open operation. Responsible for triggering SDCP connect + * or reconnect as necessary. */ void -fpi_sdcp_device_set_intermediat_cas (FpSdcpDevice *self, - GBytes *ca_1, - GBytes *ca_2) +fpi_sdcp_device_open_complete (FpSdcpDevice *self, + GError *error) { - FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + FpSdcpDeviceClass *cls = FP_SDCP_DEVICE_GET_CLASS (self); + g_autoptr(GBytes) application_secret = NULL; - g_ptr_array_set_size (priv->intermediate_cas, 0); - if (ca_1) - g_ptr_array_add (priv->intermediate_cas, g_bytes_ref (ca_1)); - if (ca_2) - g_ptr_array_add (priv->intermediate_cas, g_bytes_ref (ca_2)); + if (!error) + { + fpi_sdcp_device_get_application_secret (self, &application_secret); + + /* Try a reconnect if implemented and we already have an application_secret */ + if (cls->reconnect && application_secret) + fpi_sdcp_device_reconnect (self); + + /* Connect if we don't already have an application_secret */ + else if (!application_secret) + fpi_sdcp_device_connect (self); + + /* Complete open if we are already connected */ + else + fpi_device_open_complete (FP_DEVICE (self), NULL); + } + else + { + fpi_device_open_complete (FP_DEVICE (self), error); + } } -/* FIXME: This is (transfer full), but other getters have (transfer none) -* for drivers. Kind of inconsistent, but it is convenient here. */ /** * fp_sdcp_device_get_connect_data: - * @r_h: (out) (transfer full): The host random - * @pk_h: (out) (transfer full): The host public key + * @self: a #FpSdcpDevice fingerprint device + * @host_random: (out) (transfer full): The host-generated random + * @host_public_key: (out) (transfer full): The host public key * * Get data required to connect to (i.e. open) the device securely. */ void fpi_sdcp_device_get_connect_data (FpSdcpDevice *self, - GBytes **r_h, - GBytes **pk_h) + GBytes **host_random, + GBytes **host_public_key) { FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); - g_return_if_fail (r_h != NULL); - g_return_if_fail (pk_h != NULL); + g_return_if_fail (host_random != NULL); + g_return_if_fail (host_public_key != NULL); + g_return_if_fail (priv->host_random); + g_return_if_fail (priv->host_public_key); - *r_h = g_bytes_new (priv->host_random, sizeof (priv->host_random)); - - g_assert (priv->host_key_public->u.ec.publicValue.len == 65); - *pk_h = g_bytes_new (priv->host_key_public->u.ec.publicValue.data, priv->host_key_public->u.ec.publicValue.len); + *host_random = g_bytes_new_from_bytes (priv->host_random, 0, + g_bytes_get_size (priv->host_random)); + *host_public_key = g_bytes_new_from_bytes (priv->host_public_key, 0, + g_bytes_get_size (priv->host_public_key)); } /** * fp_sdcp_device_get_reconnect_data: - * @r_h: (out) (transfer full): The host random + * @self: a #FpSdcpDevice fingerprint device + * @reconnect_random: (out) (transfer full): The host-generated random * * Get data required to reconnect to (i.e. open) to the device securely. */ void fpi_sdcp_device_get_reconnect_data (FpSdcpDevice *self, - GBytes **r_h) + GBytes **reconnect_random) { FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); - g_return_if_fail (r_h != NULL); + g_return_if_fail (reconnect_random != NULL); + g_return_if_fail (priv->reconnect_random); - *r_h = g_bytes_new (priv->host_random, sizeof (priv->host_random)); + *reconnect_random = g_bytes_new_from_bytes (priv->reconnect_random, 0, + g_bytes_get_size (priv->reconnect_random)); } /** * fp_sdcp_device_get_identify_data: - * @r_h: (out) (transfer full): The host random + * @self: a #FpSdcpDevice fingerprint device + * @nonce: (out) (transfer full): A new host-generated nonce * * Get data required to identify a new print. */ @@ -673,420 +457,123 @@ fpi_sdcp_device_get_identify_data (FpSdcpDevice *self, FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); g_return_if_fail (nonce != NULL); + g_return_if_fail (priv->identify_nonce); - *nonce = g_bytes_new (priv->host_random, sizeof (priv->host_random)); + *nonce = g_bytes_new_from_bytes (priv->identify_nonce, 0, + g_bytes_get_size (priv->identify_nonce)); } -/* Returns the certificates public key after validation. */ -static SECKEYPublicKey * -fpi_sdcp_validate_cert (FpSdcpDevice *self, - FpiSdcpClaim *claim, - GError **error) +/** + * fp_sdcp_device_set_identify_data: + * @self: a #FpSdcpDevice fingerprint device + * @nonce: A driver-specified nonce + * + * Sets data required to identify a new print. + * + * Most drivers should not use this function, but instead use the automatically + * generated values retrieved from fpi_sdcp_device_get_identify_data() when + * executing the device-specific Identify command. + * + * In cases where a device's Identify command does not accept a randomly + * generated nonce, this function can be used to override the randomly generated + * nonce to the nonce that was actually sent to the device. + */ +void +fpi_sdcp_device_set_identify_data (FpSdcpDevice *self, + GBytes *nonce) { FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); - CERTValInParam in_params[1] = { 0, }; - CERTValOutParam out_params[2] = { 0, }; - const void *cert_m_data; - gsize cert_m_length; - CERTCertDBHandle *cert_db = NULL; - CERTCertificate *cert_m = NULL; - g_autoptr(GPtrArray) intermediate_cas = NULL; - PLArenaPool *res_arena = NULL; - SECKEYPublicKey *res = NULL; - SECStatus r; - gint i; + g_return_if_fail (nonce != NULL); - g_debug ("cert_m:"); - dump_bytes (claim->cert_m); - cert_m_data = g_bytes_get_data (claim->cert_m, &cert_m_length); - cert_m = CERT_DecodeCertFromPackage ((char *) cert_m_data, cert_m_length); - if (!cert_m) - { - /* So, the MS test client we use for the virtual-sdcp driver does not return - * a certificate (yeah ... why?). This special case is purely for testing - * purposes and should be removed by fixing the test client! - */ - if (g_str_equal (fp_device_get_driver (FP_DEVICE (self)), "virtual_sdcp") && cert_m_length == 65) - { - /* Create a new public key directly from the buffer rather than from the certificate. */ - res_arena = PORT_NewArena (DER_DEFAULT_CHUNKSIZE); - g_assert (res_arena); + g_clear_pointer (&priv->identify_nonce, g_bytes_unref); - res = (SECKEYPublicKey *) PORT_ArenaZAlloc (res_arena, sizeof (SECKEYPublicKey)); - g_assert (res); - - res->arena = res_arena; - res->pkcs11Slot = 0; - res->pkcs11ID = CK_INVALID_HANDLE; - - res->keyType = ecKey; - res->u.ec.DEREncodedParams = SDCPECParamsDER; - res->u.ec.publicValue.len = 65; - res->u.ec.publicValue.data = (guint8 *) PORT_ArenaAlloc (res->arena, 65); - memcpy (res->u.ec.publicValue.data, cert_m_data, 65); - - goto out; - } - - g_propagate_error (error, - fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, - "Failed to read cert_m: %d", PORT_GetError ())); - goto out; - } - -#if 0 - /* The following code would be a better way of specifying the intermediate CAs - * (instead of inserting them into the certificate store), but it does not - * work because the feature has simply not been implemented in PKIX. - * The code here is left purely as a reference and warning. - */ - CERTCertList *intermediate_cas = NULL; - - /* Setup list for the intermediate CAs. */ - intermediate_cas = CERT_NewCertList (); - for (i = 0; i < priv->intermediate_cas->len; i++) - { - CERTCertificate *cert = NULL; - const void *data; - gsize length; - - data = g_bytes_get_data ((GBytes *) g_ptr_array_index (priv->intermediate_cas, i), - &length); - cert = CERT_DecodeCertFromPackage ((char *) data, length); - if (!cert) - { - g_propagate_error (error, - fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, - "Failed to read intermediate cert: %d", PORT_GetError ())); - goto out; - } - /* Adding takes the reference. */ - r = CERT_AddCertToListTail (intermediate_cas, cert); - - if (r != SECSuccess) - { - g_propagate_error (error, - fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, - "Failed to add cert to cert list: %d", r)); - goto out; - } - } -#endif - - /* Import intermediate certificates into cert DB. */ - cert_db = CERT_GetDefaultCertDB (); - if (!cert_db) - { - g_propagate_error (error, - fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, - "No default certificate DB!")); - goto out; - } - - intermediate_cas = g_ptr_array_new_full (priv->intermediate_cas->len, g_free); - for (i = 0; i < priv->intermediate_cas->len; i++) - { - gsize length; - SECItem *item = NULL; - - item = g_new0 (SECItem, 1); - item->type = siDERCertBuffer; - item->data = (guint8 *) g_bytes_get_data ((GBytes *) g_ptr_array_index (priv->intermediate_cas, i), - &length); - item->len = length; - g_ptr_array_add (intermediate_cas, item); - } - r = CERT_ImportCerts (cert_db, certUsageVerifyCA, - intermediate_cas->len, (SECItem **) intermediate_cas->pdata, - NULL, - FALSE, FALSE, NULL); - if (r != SECSuccess) - { - g_propagate_error (error, - fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, - "Failed to import intermediate CAs: %d", PORT_GetError ())); - goto out; - } - - /* We assume we have the root CA in the system store already. */ - in_params[0].type = cert_pi_end; - - out_params[0].type = cert_po_end; // cert_po_errorLog; - out_params[0].value.pointer.log = NULL; - out_params[1].type = cert_po_end; - - r = CERT_PKIXVerifyCert (cert_m, - certUsageAnyCA, /* XXX: is this correct? */ - in_params, - out_params, - NULL); - if (r != SECSuccess) - { - g_propagate_error (error, - fpi_device_error_new_msg (FP_DEVICE_ERROR_UNTRUSTED, - "Failed to verify device certificate: %d", PORT_GetError ())); - goto out; - } - - /* All seems good, extract the public key in order to return it. */ - res = CERT_ExtractPublicKey (cert_m); - if (!res) - { - g_propagate_error (error, - fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, - "Failed to extract public key from certificate: %d", PORT_GetError ())); - goto out; - } - -out: - g_clear_pointer (&cert_m, CERT_DestroyCertificate); - if (out_params[0].value.pointer.log) - PORT_FreeArena (out_params[0].value.pointer.log->arena, FALSE); - return res; + priv->identify_nonce = g_steal_pointer (&nonce); } -/* FIXME: How to provide intermediate CAs provided? Same call or separate channel? */ /** * fpi_sdcp_device_connect_complete: * @self: a #FpSdcpDevice fingerprint device - * @r_d: The device random nonce + * @device_random: The device random * @claim: The device #FpiSdcpClaim * @mac: The MAC authenticating @claim * @error: A #GError or %NULL on success * - * Reports completion of connect (i.e. open) operation. + * Reports completion of connect operation. Responsible for performing SDCP key + * agreement, deriving secrets necessary for processing all other SDCP-related + * payloads, and verifying the device connection is trusted. */ void fpi_sdcp_device_connect_complete (FpSdcpDevice *self, - GBytes *r_d, + GBytes *device_random, FpiSdcpClaim *claim, GBytes *mac, GError *error) { - g_autoptr(GBytes) r_h = NULL; - g_autoptr(GBytes) claim_hash_bytes = NULL; - g_autoptr(GBytes) claim_mac = NULL; FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); - SECKEYPublicKey firmware_key_public = { 0, }; - SECKEYPublicKey device_key_public = { 0, }; - SECKEYPublicKey *model_key_public = NULL; - HASHContext *hash_ctx; - guint8 hash_out[SHA256_LENGTH]; - guint hash_len = 0; + FpSdcpDeviceClass *cls = FP_SDCP_DEVICE_GET_CLASS (self); + g_autoptr(GBytes) application_secret = NULL; FpiDeviceAction action; - PK11SymKey *a = NULL; - PK11SymKey *enc_secret = NULL; - gsize length; - SECItem sig, hash; - SECStatus r; action = fpi_device_get_current_action (FP_DEVICE (self)); g_return_if_fail (action == FPI_DEVICE_ACTION_OPEN); + g_return_if_fail (priv->host_private_key); + g_return_if_fail (priv->host_random); if (error) { - if (r_d || claim || mac) + if (device_random || claim || mac) { - g_warning ("Driver provided connect information but also reported error."); - g_clear_pointer (&r_d, g_bytes_unref); + g_clear_pointer (&device_random, g_bytes_unref); g_clear_pointer (&claim, fpi_sdcp_claim_free); g_clear_pointer (&mac, g_bytes_unref); + fp_warn ("Driver provided SDCP Connect information but also reported error."); } fpi_device_open_complete (FP_DEVICE (self), error); return; } - if (!r_d || !claim || !mac || - (!claim->cert_m || !claim->pk_d || !claim->pk_f || !claim->h_f || !claim->s_m || !claim->s_d)) + if (!device_random || !claim || !mac || + (!claim->model_certificate || !claim->device_public_key || !claim->firmware_public_key + || !claim->firmware_hash || !claim->model_signature || !claim->device_signature)) { - g_warning ("Driver did not provide all required information to callback, returning error instead."); - g_clear_pointer (&r_d, g_bytes_unref); + fp_dbg ("Driver did not provide all required information to callback; returning error instead."); + g_clear_pointer (&device_random, g_bytes_unref); g_clear_pointer (&claim, fpi_sdcp_claim_free); g_clear_pointer (&mac, g_bytes_unref); fpi_device_open_complete (FP_DEVICE (self), fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, - "Driver called connect complete with incomplete arguments.")); + "Driver called connect complete with " + "incomplete arguments")); return; } - /* Device key is of same type as host key */ - g_assert (g_bytes_get_size (claim->pk_f) == 65); - firmware_key_public.keyType = ecKey; - firmware_key_public.u.ec.DEREncodedParams = SDCPECParamsDER; - firmware_key_public.u.ec.publicValue.len = 65; - firmware_key_public.u.ec.publicValue.data = (guint8 *) g_bytes_get_data (claim->pk_f, NULL); - - /* SDCP Connect: 5.i. Perform key agreement */ - a = PK11_PubDeriveWithKDF (priv->host_key_private, - &firmware_key_public, - TRUE, - NULL, - NULL, - CKM_ECDH1_DERIVE, - CKM_SP800_108_COUNTER_KDF, - CKA_DERIVE, - 32, /* 256 bit (HMAC) secret */ - CKD_NULL, - NULL, - NULL); - - if (!a) + /* Verify connect and store the application_secret */ + if (!fpi_sdcp_verify_connect (priv->host_private_key, + priv->host_random, + device_random, + claim, + mac, + !cls->ignore_device_certificate, + !cls->ignore_device_signatures, + &application_secret, + &error)) { - error = fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, - "Error doing key agreement: %d", PORT_GetError ()); - goto out; + fpi_device_open_complete (FP_DEVICE (self), error); + return; } - /* SDCP Connect: 5.ii. Derive master secret */ - g_clear_pointer (&priv->master_secret, PK11_FreeSymKey); + fpi_sdcp_device_set_application_secret (self, application_secret); - r_h = g_bytes_new (priv->host_random, sizeof (priv->host_random)); + /* Clear no longer needed private data */ + g_clear_pointer (&priv->host_private_key, g_bytes_unref); + g_clear_pointer (&priv->host_public_key, g_bytes_unref); + g_clear_pointer (&priv->host_random, g_bytes_unref); - priv->master_secret = fpi_sdcp_kdf (self, - a, - "master secret", - r_h, - r_d, - NULL, - &error); - if (!priv->master_secret) - goto out; - - /* SDCP Connect: 5.iii. Derive MAC secret and symetric key */ - - /* NOTE: symetric key is never used, as such we just don't derive it! */ - g_clear_pointer (&priv->mac_secret, PK11_FreeSymKey); - priv->mac_secret = fpi_sdcp_kdf (self, - priv->master_secret, - "application keys", - NULL, - NULL, - &enc_secret, - &error); - if (!priv->mac_secret) - goto out; - - /* SDCP Connect: 5.iv. Validate the MAC over H(claim) */ - hash_ctx = HASH_Create (HASH_AlgSHA256); - if (!hash_ctx) - { - error = fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, - "Could not create hash context"); - goto out; - } - HASH_Begin (hash_ctx); - HASH_Update (hash_ctx, g_bytes_get_data (claim->cert_m, NULL), g_bytes_get_size (claim->cert_m)); - HASH_Update (hash_ctx, g_bytes_get_data (claim->pk_d, NULL), g_bytes_get_size (claim->pk_d)); - HASH_Update (hash_ctx, g_bytes_get_data (claim->pk_f, NULL), g_bytes_get_size (claim->pk_f)); - HASH_Update (hash_ctx, g_bytes_get_data (claim->h_f, NULL), g_bytes_get_size (claim->h_f)); - HASH_Update (hash_ctx, g_bytes_get_data (claim->s_m, NULL), g_bytes_get_size (claim->s_m)); - HASH_Update (hash_ctx, g_bytes_get_data (claim->s_d, NULL), g_bytes_get_size (claim->s_d)); - HASH_End (hash_ctx, hash_out, &hash_len, sizeof (hash_out)); - g_clear_pointer (&hash_ctx, HASH_Destroy); - g_assert (hash_len == sizeof (hash_out)); - - claim_hash_bytes = g_bytes_new (hash_out, sizeof (hash_out)); - g_debug ("H(c):"); - dump_bytes (claim_hash_bytes); - - claim_mac = fpi_sdcp_mac (self, "connect", claim_hash_bytes, NULL, &error); - if (!claim_mac) - goto out; - - g_debug ("MAC(s, \"connect\"||H(c)):"); - dump_bytes (claim_mac); - - if (!g_bytes_equal (mac, claim_mac)) - { - error = fpi_device_error_new_msg (FP_DEVICE_ERROR_UNTRUSTED, - "Device MAC over H(c) is incorrect."); - goto out; - } - - /* SDCP Connect: 5.v. Unpack the claim (SKIP, already done) */ - /* SDCP Connect: 5.vi. Verify claim */ - - /* First, validate the certificate (and return its public key). */ - model_key_public = fpi_sdcp_validate_cert (self, claim, &error); - if (!model_key_public) - goto out; - - /* Verify(pk_m, H(pk_d), s_m) */ - sig.data = (guint8 *) g_bytes_get_data (claim->s_m, &length); - sig.len = length; - memset (hash_out, 0, sizeof (hash_out)); - r = PK11_HashBuf (HASH_GetHashOidTagByHashType (HASH_AlgSHA256), - hash_out, - g_bytes_get_data (claim->pk_d, NULL), - g_bytes_get_size (claim->pk_d)); - if (r != SECSuccess) - { - error = fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, - "Failed to hash device public key!"); - goto out; - } - - hash.data = hash_out; - hash.len = sizeof (hash_out); - r = PK11_Verify (model_key_public, &sig, &hash, NULL); - if (r != SECSuccess) - { - error = fpi_device_error_new_msg (FP_DEVICE_ERROR_UNTRUSTED, - "Verification of device public key failed: %d", PORT_GetError ()); - goto out; - } - - device_key_public.keyType = ecKey; - device_key_public.u.ec.DEREncodedParams = SDCPECParamsDER; - device_key_public.u.ec.publicValue.len = g_bytes_get_size (claim->pk_d); - device_key_public.u.ec.publicValue.data = (guint8 *) g_bytes_get_data (claim->pk_d, NULL); - - /* Verify(pk_d, H(C001||h_f||pk_f), s_d) */ - sig.data = (guint8 *) g_bytes_get_data (claim->s_d, &length); - sig.len = length; - - hash_ctx = HASH_Create (HASH_AlgSHA256); - if (!hash_ctx) - { - error = fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, - "Could not create hash context"); - goto out; - } - HASH_Begin (hash_ctx); - HASH_Update (hash_ctx, (guint8 *) "\xC0\x01", 2); - HASH_Update (hash_ctx, g_bytes_get_data (claim->h_f, NULL), g_bytes_get_size (claim->h_f)); - HASH_Update (hash_ctx, g_bytes_get_data (claim->pk_f, NULL), g_bytes_get_size (claim->pk_f)); - HASH_End (hash_ctx, hash_out, &hash_len, sizeof (hash_out)); - g_clear_pointer (&hash_ctx, HASH_Destroy); - g_assert (hash_len == sizeof (hash_out)); - - hash.data = hash_out; - hash.len = sizeof (hash_out); - r = PK11_Verify (&device_key_public, &sig, &hash, NULL); - if (r != SECSuccess) - { - error = fpi_device_error_new_msg (FP_DEVICE_ERROR_UNTRUSTED, - "Verification of boot process failed: %d", PORT_GetError ()); - goto out; - } - - /* XXX/FIXME: We should be checking H(f) against a list of compromised firmwares. - * We would need a way to distribute and load it though. - */ - -out: - g_clear_pointer (&a, PK11_FreeSymKey); - g_clear_pointer (&enc_secret, PK11_FreeSymKey); - g_clear_pointer (&model_key_public, SECKEY_DestroyPublicKey); - - if (error) - g_clear_pointer (&priv->mac_secret, PK11_FreeSymKey); - - fpi_device_open_complete (FP_DEVICE (self), error); + fpi_device_open_complete (FP_DEVICE (self), NULL); } /** @@ -1102,20 +589,21 @@ fpi_sdcp_device_reconnect_complete (FpSdcpDevice *self, GBytes *mac, GError *error) { - g_autoptr(GError) err = NULL; FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + g_autoptr(GBytes) application_secret = NULL; FpiDeviceAction action; action = fpi_device_get_current_action (FP_DEVICE (self)); g_return_if_fail (action == FPI_DEVICE_ACTION_OPEN); + g_return_if_fail (priv->reconnect_random); if (error) { if (mac) { - g_warning ("Driver provided a MAC but also reported an error."); - g_bytes_unref (mac); + fp_warn ("Driver provided a reconnect MAC but also reported an error."); + g_clear_pointer (&mac, g_bytes_unref); } /* Silently try a normal connect instead. */ @@ -1123,27 +611,16 @@ fpi_sdcp_device_reconnect_complete (FpSdcpDevice *self, } else if (mac) { - g_autoptr(GBytes) mac_verify = NULL; - g_autoptr(GBytes) host_random = NULL; + fpi_sdcp_device_get_application_secret (self, &application_secret); - /* We got a MAC, so we can check whether the device - * still agrees with us on the shared secret. */ - host_random = g_bytes_new (priv->host_random, sizeof (priv->host_random)); - mac_verify = fpi_sdcp_mac (self, "reconnect", host_random, NULL, &err); - if (!mac_verify) + if (fpi_sdcp_verify_reconnect (application_secret, priv->reconnect_random, mac, &error)) { - fpi_device_open_complete (FP_DEVICE (self), g_steal_pointer (&err)); - return; - } - - if (g_bytes_equal (mac, mac_verify)) - { - g_debug ("Reconnect succeeded"); + fp_dbg ("SDCP Reconnect succeeded"); fpi_device_open_complete (FP_DEVICE (self), NULL); } else { - g_message ("Fast reconnect with SDCP device failed, doing a full connect."); + fp_dbg ("SDCP Reconnect failed; doing a full connect."); fpi_sdcp_device_connect (self); } } @@ -1151,125 +628,131 @@ fpi_sdcp_device_reconnect_complete (FpSdcpDevice *self, { fpi_device_open_complete (FP_DEVICE (self), fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, - "Driver called reconnect complete with wrong arguments.")); + "Driver called reconnect complete with wrong arguments")); } + + /* Clear no longer needed private data */ + g_clear_pointer (&priv->reconnect_random, g_bytes_unref); } /** - * fpi_sdcp_device_enroll_set_nonce: + * fpi_sdcp_device_list_complete: * @self: a #FpSdcpDevice fingerprint device - * @nonce: The device generated nonce + * @ids: A #GPtrArray of #GBytes of each SDCP enrollment ID stored on the device + * @error: A #GError or %NULL on success * - * Called during enroll to inform the SDCP base class about the nonce - * that the device chose. This can be called at any point, but must be - * called before calling fpi_sdcp_device_enroll_ready(). + * Convenience function to create the minimally required #FpPrint list for + * #FpSdcpDevice prints using the provided @ids, then uses that #FpPrint list to + * report completion of the list operation. + * + * If the device provides additional attributes that should be stored on each + * #FpPrint as part of the list operation, a #GPtrArray of #FpPrint can instead + * be created with the additional attributes and fpi_device_list_complete() can + * be used instead of this function. + * + * Please note that the @ids array will be freed using g_ptr_array_unref() and + * the elements are destroyed automatically. As such, you must use + * g_ptr_array_new_with_free_func() with `(GDestroyNotify) g_bytes_unref` as the + * free func when creating the #GPtrArray. */ void -fpi_sdcp_device_enroll_set_nonce (FpSdcpDevice *self, - GBytes *nonce) +fpi_sdcp_device_list_complete (FpSdcpDevice *self, + GPtrArray *ids, + GError *error) { - g_autoptr(GBytes) id = NULL; - GVariant *id_var; - FpPrint *print; - GVariant *data; + g_autoptr(GPtrArray) prints = NULL; + gint prints_len = 0; + FpiDeviceAction action; - g_return_if_fail (FP_IS_SDCP_DEVICE (self)); - g_return_if_fail (fpi_device_get_current_action (FP_DEVICE (self)) == FPI_DEVICE_ACTION_ENROLL); + action = fpi_device_get_current_action (FP_DEVICE (self)); - g_return_if_fail (nonce || g_bytes_get_size (nonce) != 32); - - fpi_device_get_enroll_data (FP_DEVICE (self), &print); - - id = fpi_sdcp_mac (self, "enroll", nonce, NULL, NULL); - if (!id) - { - g_warning ("Could not generate enroll MAC"); - return; - } - - id_var = g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE, - g_bytes_get_data (id, NULL), - g_bytes_get_size (id), - 1); - data = g_variant_new ("(@ay)", id_var); - - /* Set to true once committed */ - fpi_print_set_device_stored (print, FALSE); - - /* Attach the ID to the print */ - g_object_set (print, "fpi-data", data, NULL); -} - -/** - * fpi_sdcp_device_enroll_ready: - * @self: a #FpSdcpDevice fingerprint device - * @error: a #GError or %NULL on success - * - * Called when the print is ready to be committed to device memory. - * For each enroll step, fpi_device_enroll_progress() must first - * be called until the enroll is ready to be committed. - */ -void -fpi_sdcp_device_enroll_ready (FpSdcpDevice *self, - GError *error) -{ - g_autoptr(GVariant) data = NULL; - g_autoptr(GVariant) id_var = NULL; - g_autoptr(GBytes) id = NULL; - FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); - FpSdcpDeviceClass *cls = FP_SDCP_DEVICE_GET_CLASS (self); - FpPrint *print; - - g_return_if_fail (FP_IS_SDCP_DEVICE (self)); - g_return_if_fail (fpi_device_get_current_action (FP_DEVICE (self)) == FPI_DEVICE_ACTION_ENROLL); - - fpi_device_get_enroll_data (FP_DEVICE (self), &print); + g_return_if_fail (action == FPI_DEVICE_ACTION_LIST); if (error) { + fpi_device_list_complete (FP_DEVICE (self), NULL, error); + return; + } + + prints = g_ptr_array_new_with_free_func (g_object_unref); + + /* Allow an empty array (prints_len=0) but if ids has been passed, use it */ + if (ids) + prints_len = ids->len; + + for (gint i = 0; i < prints_len; i++) + { + FpPrint *print = fp_print_new (FP_DEVICE (self)); + fpi_print_set_type (print, FPI_PRINT_SDCP); + fpi_print_set_device_stored (print, FALSE); + fpi_sdcp_device_set_print_id (print, g_ptr_array_index (ids, i)); + g_ptr_array_add (prints, g_object_ref_sink (print)); + } + + fpi_device_list_complete (FP_DEVICE (self), g_steal_pointer (&prints), NULL); + + g_clear_pointer (&ids, g_ptr_array_unref); +} + +/** + * fpi_sdcp_device_enroll_commit: + * @self: a #FpSdcpDevice fingerprint device + * @nonce: The device generated nonce + * @error: a #GError or %NULL on success + * + * Called when the print is ready to be committed to device memory. + * During enrollment, fpi_device_enroll_progress() must be called for each + * successful stage before the print can be committed. + * The @nonce generated by the device-specific EnrollmentNonce response must be + * provided in order for the enrollment ID to be generated. + * The driver's enroll_commit() vfunc will be triggered upon successfully + * generating the enrollment ID. + */ +void +fpi_sdcp_device_enroll_commit (FpSdcpDevice *self, + GBytes *nonce, + GError *error) +{ + FpSdcpDeviceClass *cls = FP_SDCP_DEVICE_GET_CLASS (self); + g_autoptr(GBytes) application_secret = NULL; + GBytes *id = NULL; + FpPrint *print; + + g_return_if_fail (FP_IS_SDCP_DEVICE (self)); + g_return_if_fail (fpi_device_get_current_action (FP_DEVICE (self)) == FPI_DEVICE_ACTION_ENROLL); + g_return_if_fail (nonce != NULL); + + fpi_device_get_enroll_data (FP_DEVICE (self), &print); + fpi_sdcp_device_get_application_secret (self, &application_secret); + + id = fpi_sdcp_generate_enrollment_id (application_secret, nonce, &error); + if (!id || error) + { + fp_warn ("Could not generate SDCP enrollment ID"); + + /* clear potentially non-functioning application_secret */ + fpi_sdcp_device_unset_application_secret (self); + fpi_device_enroll_complete (FP_DEVICE (self), NULL, error); g_object_set (print, "fpi-data", NULL, NULL); return; } - /* TODO: The following will need to ensure that the ID has been generated */ + /* Set to true once committed */ + fpi_print_set_device_stored (print, FALSE); - g_object_get (G_OBJECT (print), "fpi-data", &data, NULL); + /* Attach the ID to the print */ + fpi_sdcp_device_set_print_id (print, id); - if (data) - { - const guint8 *id_data; - gsize id_len; + cls->enroll_commit (self, id); - g_variant_get (data, - "(@ay)", - &id_var); - - id_data = g_variant_get_fixed_array (id_var, &id_len, 1); - id = g_bytes_new (id_data, id_len); - } - - g_debug ("ID/enroll mac:"); - dump_bytes (id); - - if (!id) - { - g_warning ("Driver failed to call fpi_sdcp_device_enroll_set_nonce, aborting enroll."); - - /* NOTE: Cancel the enrollment, i.e. don't commit */ - priv->enroll_pre_commit_error = fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, - "Device/driver did not provide a nonce as required by protocol, aborting enroll!"); - cls->enroll_commit (self, NULL); - } - else - { - cls->enroll_commit (self, g_steal_pointer (&id)); - } + g_clear_pointer (&id, g_bytes_unref); } /** * fpi_sdcp_device_enroll_commit_complete: * @self: a #FpSdcpDevice fingerprint device + * @error: a #GError or %NULL on success * * Called when device has committed the given print to memory. * This finalizes the enroll operation. @@ -1278,27 +761,12 @@ void fpi_sdcp_device_enroll_commit_complete (FpSdcpDevice *self, GError *error) { - g_autoptr(GVariant) data = NULL; - FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + g_autoptr(GBytes) id = NULL; FpPrint *print; g_return_if_fail (FP_IS_SDCP_DEVICE (self)); g_return_if_fail (fpi_device_get_current_action (FP_DEVICE (self)) == FPI_DEVICE_ACTION_ENROLL); - if (priv->enroll_pre_commit_error) - { - if (error) - { - g_warning ("Cancelling enroll after error failed with: %s", error->message); - g_error_free (error); - } - - fpi_device_enroll_complete (FP_DEVICE (self), - NULL, - g_steal_pointer (&priv->enroll_pre_commit_error)); - return; - } - if (error) { fpi_device_enroll_complete (FP_DEVICE (self), NULL, error); @@ -1306,10 +774,11 @@ fpi_sdcp_device_enroll_commit_complete (FpSdcpDevice *self, } fpi_device_get_enroll_data (FP_DEVICE (self), &print); - g_object_get (G_OBJECT (print), "fpi-data", &data, NULL); - if (!data) + + fpi_sdcp_device_get_print_id (print, &id); + if (!id) { - g_error ("Inconsistent state, the print must have the enrolled ID attached at this point"); + g_error ("Inconsistent state; the print must have the enrolled ID attached at this point"); return; } @@ -1365,68 +834,72 @@ fpi_sdcp_device_identify_complete (FpSdcpDevice *self, GBytes *mac, GError *error) { - g_autoptr(GBytes) mac_verify = NULL; - g_autoptr(GBytes) host_random = NULL; FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); - GError *err = NULL; + g_autoptr(GBytes) application_secret = NULL; FpPrint *identified_print; - GVariant *id_var; - GVariant *data; FpiDeviceAction action; g_return_if_fail (FP_IS_SDCP_DEVICE (self)); action = fpi_device_get_current_action (FP_DEVICE (self)); g_return_if_fail (action == FPI_DEVICE_ACTION_IDENTIFY || action == FPI_DEVICE_ACTION_VERIFY); + g_return_if_fail (priv->identify_nonce); if (error) { + g_clear_pointer (&priv->identify_nonce, g_bytes_unref); fpi_device_action_error (FP_DEVICE (self), error); return; } - if (!id || !mac || g_bytes_get_size (id) != 32 || g_bytes_get_size (mac) != 32) + /* No error and no valid id/mac provided means that there was no match from the device */ + if (!id || !mac || g_bytes_get_size (id) != SDCP_ENROLLMENT_ID_SIZE + || g_bytes_get_size (mac) != SDCP_MAC_SIZE) { - fpi_device_action_error (FP_DEVICE (self), - fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, - "Driver returned incorrect ID/MAC for identify operation")); + g_clear_pointer (&priv->identify_nonce, g_bytes_unref); + if (action == FPI_DEVICE_ACTION_VERIFY) + { + fpi_device_verify_report (FP_DEVICE (self), FPI_MATCH_FAIL, NULL, NULL); + fpi_device_verify_complete (FP_DEVICE (self), NULL); + } + else + { + fpi_device_identify_report (FP_DEVICE (self), NULL, NULL, NULL); + fpi_device_identify_complete (FP_DEVICE (self), NULL); + } return; } - host_random = g_bytes_new (priv->host_random, sizeof (priv->host_random)); - mac_verify = fpi_sdcp_mac (self, "identify", host_random, id, &err); - if (!mac_verify) - { - fpi_device_action_error (FP_DEVICE (self), - err); - return; - } + fpi_sdcp_device_get_application_secret (self, &application_secret); - if (!g_bytes_equal (mac, mac_verify)) + if (!fpi_sdcp_verify_identify (application_secret, priv->identify_nonce, id, mac, &error)) { + g_clear_pointer (&priv->identify_nonce, g_bytes_unref); + + /* clear potentially non-functioning application_secret */ + fpi_sdcp_device_unset_application_secret (self); + fpi_device_action_error (FP_DEVICE (self), fpi_device_error_new_msg (FP_DEVICE_ERROR_UNTRUSTED, - "Reported match from the device cannot be trusted!")); + "SDCP AuthorizedIdentity verification " + "failed: %s", + error->message)); return; } + /* Clear no longer needed private data */ + g_clear_pointer (&priv->identify_nonce, g_bytes_unref); + /* Create a new print */ identified_print = fp_print_new (FP_DEVICE (self)); fpi_print_set_type (identified_print, FPI_PRINT_SDCP); - fpi_print_set_device_stored (identified_print, TRUE); - - id_var = g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE, - g_bytes_get_data (id, NULL), - g_bytes_get_size (id), - 1); - data = g_variant_new ("(@ay)", id_var); /* Set to true once committed */ fpi_print_set_device_stored (identified_print, FALSE); /* Attach the ID to the print */ - g_object_set (identified_print, "fpi-data", data, NULL); + fpi_sdcp_device_set_print_id (identified_print, id); /* The surrounding API expects a match/no-match against a given set. */ @@ -1467,3 +940,71 @@ fpi_sdcp_device_identify_complete (FpSdcpDevice *self, fpi_device_identify_complete (FP_DEVICE (self), NULL); } } + +/** + * fpi_sdcp_device_get_print_id: + * @print: an SDCP device #FpPrint + * @id: (out) (transfer full): the ID gotten from the @print data + * + * Gets the SDCP enrollment ID from the @print data. + * + * The returned @id may be %NULL if the data was not set or in the wrong format. + */ +void +fpi_sdcp_device_get_print_id (FpPrint *print, + GBytes **id) +{ + g_autoptr(GVariant) id_var = NULL; + g_autoptr(GVariant) data = NULL; + const guint8 *id_data; + gsize id_len; + + g_return_if_fail (print); + g_return_if_fail (*id == NULL); + + g_object_get (G_OBJECT (print), "fpi-data", &data, NULL); + + if (!data) + { + fp_warn ("SDCP print data has not been set."); + return; + } + + if (!g_variant_check_format_string (data, "(@ay)", FALSE)) + { + fp_warn ("SDCP print data is not in expected format."); + return; + } + + g_variant_get (data, "(@ay)", &id_var); + + id_data = g_variant_get_fixed_array (id_var, &id_len, sizeof (guint8)); + + *id = g_bytes_new (id_data, id_len); +} + +/** + * fpi_sdcp_device_set_print_id: + * @print: an SDCP device #FpPrint + * @id: the ID to set in the @print data + * + * Sets the SDCP enrollment ID in the @print data. + */ +void +fpi_sdcp_device_set_print_id (FpPrint *print, + GBytes *id) +{ + GVariant *id_var; + GVariant *data; + + g_return_if_fail (print); + g_return_if_fail (id); + + id_var = g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE, + g_bytes_get_data (id, NULL), + g_bytes_get_size (id), + 1); + data = g_variant_new ("(@ay)", id_var); + + g_object_set (G_OBJECT (print), "fpi-data", data, NULL); +} diff --git a/libfprint/fpi-sdcp-device.h b/libfprint/fpi-sdcp-device.h index d5a4fcf7..7de1d2a0 100644 --- a/libfprint/fpi-sdcp-device.h +++ b/libfprint/fpi-sdcp-device.h @@ -1,6 +1,7 @@ /* * FpSdcpDevice - A base class for SDCP enabled devices * Copyright (C) 2020 Benjamin Berg + * Copyright (C) 2025 Joshua Grisham * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -19,18 +20,29 @@ #pragma once -#include #include "fpi-device.h" + #include "fp-sdcp-device.h" +#define SDCP_PUBLIC_KEY_SIZE 65 +#define SDCP_APPLICATION_SECRET_SIZE 32 +#define SDCP_RANDOM_SIZE 32 +#define SDCP_MAC_SIZE 32 +#define SDCP_NONCE_SIZE 32 +#define SDCP_ENROLLMENT_ID_SIZE 32 +#define SDCP_SIGNATURE_SIZE 64 + /** * FpiSdcpClaim: - * @cert_m: The per-model ECDSA certificate (x509 ASN.1 DER encoded) - * @pk_d: The device public key (65 bytes) - * @pk_f: The firmware public key (65 bytes) - * @h_f: The firmware hash - * @s_m: Signature over @pk_d using the per-model private key (64 bytes) - * @s_d: Signature over h_f and pk_f using the device private key (64 bytes) + * @model_certificate: Microsoft-issued per-model certificate encoded in x509 + * ASN.1 DER format (`cert_m`) + * @device_public_key: The per-device ECDSA public key (`pk_d`) + * @firmware_public_key: The ephemeral public key generated by the device + * firmware (`pk_f`) + * @firmware_hash: Hash of the firmware and firmware public key (`h_f`) + * @model_signature: Device public key signed by the model key (`s_m`) + * @device_signature: Firmware hash and public key signed by the device private + * key (`s_d`) * * Structure to hold the claim as produced by the device during a secure * connect. See the SDCP specification for more details. @@ -41,12 +53,12 @@ struct _FpiSdcpClaim { /*< public >*/ - GBytes *cert_m; - GBytes *pk_d; - GBytes *pk_f; - GBytes *h_f; - GBytes *s_m; - GBytes *s_d; + GBytes *model_certificate; /* cert_m */ + GBytes *device_public_key; /* pk_d */ + GBytes *firmware_public_key; /* pk_f */ + GBytes *firmware_hash; /* h_f */ + GBytes *model_signature; /* s_m */ + GBytes *device_signature; /* s_d */ }; typedef struct _FpiSdcpClaim FpiSdcpClaim; @@ -60,30 +72,48 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC (FpiSdcpClaim, fpi_sdcp_claim_free) /** * FpSdcpDeviceClass: - * @connect: Establish SDCP connection. Similar to open in #FpDeviceClass - * but called connect to mirror the SDCP specification. + * @ignore_device_certificate: Set to %TRUE to skip validating the device's + * #FpiSdcpClaim.model_certificate against the SDCP truststore. + * @ignore_device_signatures: Set to %TRUE to skip verifying the device's + * #FpiSdcpClaim.model_signature and #FpiSdcpClaim.device_signature. + * @open: Open the device. Similar to #FpDeviceClass.open except that + * completion with fpi_sdcp_device_open_complete() will also take care of + * executing @connect and @reconnect as necessary. + * @connect: Establish SDCP connection. * @reconnect: Perform a faster reconnect. Drivers do not need to provide this * function. If reconnect fails, then a normal connect will be tried. - * @enroll_begin: Start the enrollment procedure. In the absence of an error, - * the driver must call fpi_sdcp_device_enroll_set_nonce() at any point. It - * also must report enrollment progress using fpi_device_enroll_progress(). - * It should also store available metadata about the print in device memory. - * The operation is completed with fpi_sdcp_device_enroll_ready(). - * @enroll_commit: Complete the enrollment procedure. This commits the newly - * enrolled print to the device memory. Will only be called if enroll_begin - * succeeded. The passed id may be %NULL, in that case the driver must - * abort the enrollment process. id is owned by the base class and remains - * valid throughout the operation. + * @list: List prints stored on the device. The driver must create a #GPtrArray + * of #GBytes with each enrollment ID stored on the device and use it to call + * fpi_sdcp_device_list_complete() in order to complete the operation. + * @enroll: Start the enrollment procedure and capture all samples. The driver + * must report enrollment progress using fpi_device_enroll_progress(). It + * should also store available metadata about the print in device memory. The + * driver must call fpi_sdcp_device_enroll_commit() when all enrollment stages + * are complete and the print is ready to be commited to the device. + * @enroll_commit: Commit the newly-enrolled print to the device memory using + * the passed id. id may be %NULL, in which case the driver must abort the + * enrollment process. id is owned by the base class and remains valid + * throughout the operation. On completion, the driver must call + * fpi_sdcp_device_enroll_commit_complete(). * @identify: Start identification process. On completion, the driver must call * fpi_sdcp_device_identify_complete(). To request the user to retry the * fpi_sdcp_device_identify_retry() function is used. * - * * These are the main entry points for drivers implementing SDCP. * * Drivers *must* eventually call the corresponding function to finish the * operation. * + * The following #FpDeviceClass entry points are also compatible and can be set + * on the #FpDeviceClass if supported for a given device: + * - #FpDeviceClass.probe + * - #FpDeviceClass.close + * - #FpDeviceClass.delete + * - #FpDeviceClass.clear_storage + * - #FpDeviceClass.cancel + * - #FpDeviceClass.suspend + * - #FpDeviceClass.resume + * * XXX: Is the use of fpi_device_action_error() acceptable? * * Drivers *must* also handle cancellation properly for any long running @@ -97,46 +127,59 @@ struct _FpSdcpDeviceClass { FpDeviceClass parent_class; - void (*connect) (FpSdcpDevice *dev); - void (*reconnect) (FpSdcpDevice *dev); - void (*close) (FpSdcpDevice *dev); - void (*enroll_begin) (FpSdcpDevice *dev); - void (*enroll_commit) (FpSdcpDevice *dev, + gboolean ignore_device_certificate; + gboolean ignore_device_signatures; + + void (*open) (FpSdcpDevice *sdcp_device); + void (*connect) (FpSdcpDevice *sdcp_device); + void (*reconnect) (FpSdcpDevice *sdcp_device); + void (*list) (FpSdcpDevice *sdcp_device); + void (*enroll) (FpSdcpDevice *sdcp_device); + void (*enroll_commit) (FpSdcpDevice *sdcp_device, GBytes *id); - void (*identify) (FpSdcpDevice *dev); + void (*identify) (FpSdcpDevice *sdcp_device); }; -void fpi_sdcp_device_set_intermediat_cas (FpSdcpDevice *self, - GBytes *ca_1, - GBytes *ca_2); +void fpi_sdcp_device_open_complete (FpSdcpDevice *self, + GError *error); void fpi_sdcp_device_get_connect_data (FpSdcpDevice *self, - GBytes **r_h, - GBytes **pk_h); + GBytes **host_random, + GBytes **host_public_key); void fpi_sdcp_device_connect_complete (FpSdcpDevice *self, - GBytes *r_d, + GBytes *device_random, FpiSdcpClaim *claim, GBytes *mac, GError *error); void fpi_sdcp_device_get_reconnect_data (FpSdcpDevice *self, - GBytes **r_h); + GBytes **reconnect_random); void fpi_sdcp_device_reconnect_complete (FpSdcpDevice *self, GBytes *mac, GError *error); -void fpi_sdcp_device_enroll_set_nonce (FpSdcpDevice *self, - GBytes *nonce); -void fpi_sdcp_device_enroll_ready (FpSdcpDevice *self, - GError *error); +void fpi_sdcp_device_list_complete (FpSdcpDevice *self, + GPtrArray *ids, + GError *error); + +void fpi_sdcp_device_enroll_commit (FpSdcpDevice *self, + GBytes *nonce, + GError *error); void fpi_sdcp_device_enroll_commit_complete (FpSdcpDevice *self, GError *error); void fpi_sdcp_device_get_identify_data (FpSdcpDevice *self, GBytes **nonce); +void fpi_sdcp_device_set_identify_data (FpSdcpDevice *self, + GBytes *nonce); void fpi_sdcp_device_identify_retry (FpSdcpDevice *self, GError *error); void fpi_sdcp_device_identify_complete (FpSdcpDevice *self, GBytes *id, GBytes *mac, GError *error); + +void fpi_sdcp_device_get_print_id (FpPrint *print, + GBytes **id); +void fpi_sdcp_device_set_print_id (FpPrint *print, + GBytes *id); diff --git a/libfprint/fpi-sdcp.c b/libfprint/fpi-sdcp.c new file mode 100644 index 00000000..c3d8ef17 --- /dev/null +++ b/libfprint/fpi-sdcp.c @@ -0,0 +1,1053 @@ +/* + * Secure Device Connection Protocol (SDCP) support implementation + * Copyright (C) 2025 Joshua Grisham + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#define FP_COMPONENT "sdcp" +#include "fpi-log.h" + +#include "fpi-sdcp.h" +#include "sdcp/fpi-sdcp-truststore-resource.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define SDCP_PRIVATE_KEY_SIZE 32 +#define SDCP_KEY_AGREEMENT_SIZE 32 +#define SDCP_MASTER_SECRET_SIZE 32 +#define SDCP_APPLICATION_KEYS_SIZE 64 + +/******************************************************************************/ + +static void +print_openssl_errors (void) +{ + gulong e = 0; + e = ERR_get_error (); + while (e != 0) + { + fp_dbg ("OpenSSL error: %s", ERR_error_string (e, NULL)); + e = ERR_get_error (); + } +} + +static void +print_certificate (X509 *certificate) +{ + BIO *bio = BIO_new (BIO_s_mem ()); + BUF_MEM *bio_mem = NULL; + + X509_print (bio, certificate); + BIO_get_mem_ptr (bio, &bio_mem); + fp_dbg ("SDCP Device reported the following model certificate:\n%.*s\n", + (int) bio_mem->length, bio_mem->data); + BIO_free (bio); +} + +/******************************************************************************/ + +static gboolean +fpi_sdcp_verify_signature (EVP_PKEY *pkey, + const gchar *label, + GBytes *data_a, + GBytes *data_b, + GBytes *signature, + GError **error) +{ + EVP_MD_CTX *mdctx = NULL; + EVP_PKEY_CTX *pctx = NULL; + + mdctx = EVP_MD_CTX_create (); + pctx = EVP_PKEY_CTX_new(pkey, NULL); + EVP_MD_CTX_set_pkey_ctx(mdctx, pctx); + + if (!EVP_DigestVerifyInit(mdctx, NULL, EVP_sha256 (), NULL, pkey)) + goto out_error; + + if (label) + if (!EVP_DigestVerifyUpdate (mdctx, label, strlen (label))) + goto out_error; + + if (data_a) + if (!EVP_DigestVerifyUpdate (mdctx, + g_bytes_get_data (data_a, NULL), + g_bytes_get_size (data_a))) + goto out_error; + + if (data_b) + if (!EVP_DigestVerifyUpdate (mdctx, + g_bytes_get_data (data_b, NULL), + g_bytes_get_size (data_b))) + goto out_error; + + if (!EVP_DigestVerifyUpdate (mdctx, + g_bytes_get_data (signature, NULL), + g_bytes_get_size (signature))) + goto out_error; + + EVP_PKEY_CTX_free (pctx); + EVP_MD_CTX_free (mdctx); + + return TRUE; + +out_error: + g_propagate_error (error, + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "OpenSSL error verifying signature for label '%s'", + label)); + print_openssl_errors (); + g_clear_pointer (&pctx, EVP_PKEY_CTX_free); + g_clear_pointer (&mdctx, EVP_MD_CTX_free); + return FALSE; +} + +static X509_STORE *truststore = NULL; + +static X509_STORE * +fpi_sdcp_get_truststore (GError **error) +{ + g_autoptr(GResource) truststore_resource = NULL; + const gchar* truststore_resource_path = "/org/freedesktop/fprint/sdcp/truststore/"; + char **trustcert_names = NULL; + gchar *trustcert_path = NULL; + GBytes *trustcert_gb = NULL; + const gchar *trustcert_ptr; + gsize trustcert_len = 0; + BIO *bio = NULL; + X509 *trustcert = NULL; + + if (truststore) + return truststore; + + truststore = X509_STORE_new (); + if (!truststore) + { + fp_dbg ("Failed initializing SDCP X509 certificate store"); + goto out_error; + } + + fpi_sdcp_truststore_register_resource (); + truststore_resource = fpi_sdcp_truststore_get_resource (); + trustcert_names = g_resources_enumerate_children (truststore_resource_path, + G_RESOURCE_LOOKUP_FLAGS_NONE, error); + if (*error) + { + fp_dbg ("Error loading SDCP truststore certificates: %s", (*error)->message); + goto out; + } + for (int i = 0; trustcert_names[i]; i++) + { + fp_dbg ("Adding certificate to SDCP truststore: %s", trustcert_names[i]); + trustcert_path = g_strconcat (truststore_resource_path, trustcert_names[i], NULL); + trustcert_gb = g_resource_lookup_data (truststore_resource, trustcert_path, 0, error); + if (*error) + { + fp_dbg ("Error loading SDCP truststore certificate '%s': %s", + trustcert_names[i], (*error)->message); + goto out; + } + g_free (trustcert_path); + + trustcert_ptr = g_bytes_get_data (trustcert_gb, &trustcert_len); + //fp_dbg ("%s:\n%s", trustcert_names[i], trustcert_ptr); + + bio = BIO_new (BIO_s_mem ()); + if (BIO_write (bio, trustcert_ptr, trustcert_len) != trustcert_len) + { + fp_dbg ("Failed reading '%s' to buffer", trustcert_names[i]); + goto out_error; + } + g_bytes_unref (trustcert_gb); + trustcert = PEM_read_bio_X509 (bio, NULL, NULL, NULL); + //print_certificate (trustcert); + if (!X509_STORE_add_cert (truststore, trustcert)) + { + fp_dbg ("Failed adding '%s' to X509 store", trustcert_names[i]); + goto out_error; + } + BIO_free (bio); + X509_free (trustcert); + } + + g_strfreev (trustcert_names); + g_resource_unref (truststore_resource); + + return truststore; + +out_error: + g_propagate_error (error, + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "OpenSSL error setting up certificate verification")); + print_openssl_errors (); + goto out; +out: + g_clear_pointer (&trustcert_names, g_strfreev); + g_clear_pointer (&trustcert, X509_free); + return NULL; +} + +static gboolean +fpi_sdcp_verify_certificate (X509 *certificate, + GError **error) +{ + X509_STORE *sdcp_truststore = NULL; + X509_VERIFY_PARAM *param = NULL; + X509_STORE_CTX *ctx = NULL; + + sdcp_truststore = fpi_sdcp_get_truststore (error); + if (*error) + goto out_error; + + param = X509_VERIFY_PARAM_new (); + if (!param) + goto out_error; + + /* + * Most model certificates expire after one year and are not renewed via a + * vendor firmware update, so we should ignore the validity time check. + */ + if (!X509_VERIFY_PARAM_set_flags (param, X509_V_FLAG_NO_CHECK_TIME)) + goto out_error; + + /* Do not allow broken certificates */ + if (!X509_VERIFY_PARAM_set_flags (param, X509_V_FLAG_X509_STRICT)) + goto out_error; + + /* set X509_V_FLAG_PARTIAL_CHAIN if we want to skip adding all root and intermediate certs */ + /* + if (!X509_VERIFY_PARAM_set_flags (param, X509_V_FLAG_PARTIAL_CHAIN)) + goto out_error; + */ + + if (!X509_STORE_set1_param (sdcp_truststore, param)) + goto out_error; + + X509_VERIFY_PARAM_free (param); + ctx = X509_STORE_CTX_new (); + + if(!X509_STORE_CTX_init (ctx, sdcp_truststore, certificate, NULL)) + goto out_verify_error; + + if (!X509_verify_cert (ctx)) + goto out_verify_error; + + X509_STORE_CTX_free (ctx); + + return TRUE; + +out_error: + g_propagate_error (error, + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "OpenSSL error setting up certificate verification")); + print_openssl_errors (); + goto out; +out_verify_error: + g_propagate_error (error, + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "OpenSSL error verifying model certificate")); + fp_dbg ("OpenSSL verification error: %s", + X509_verify_cert_error_string (X509_STORE_CTX_get_error (ctx))); +out: + g_clear_pointer (¶m, X509_VERIFY_PARAM_free); + g_clear_pointer (&ctx, X509_STORE_CTX_free); + return FALSE; +} + +static GBytes * +fpi_sdcp_kdf (GBytes *key, + const gchar *label, + GBytes *context_a, + GBytes *context_b, + gsize length, + GError **error) +{ + g_autoptr(GBytes) res = NULL; + guint8 *secret = NULL; + EVP_KDF *kdf; + EVP_KDF_CTX *kdf_ctx; + OSSL_PARAM params[8]; + int i = 0; + + g_assert (key); + g_assert (strlen (label) > 0); + g_assert (length > 0); + + params[i++] = OSSL_PARAM_construct_utf8_string (OSSL_KDF_PARAM_MODE, (gchar *) "counter", 0); + params[i++] = OSSL_PARAM_construct_utf8_string (OSSL_KDF_PARAM_MAC, (gchar *) "HMAC", 0); + params[i++] = OSSL_PARAM_construct_utf8_string (OSSL_KDF_PARAM_DIGEST, (gchar *) "SHA2-256", 0); + + params[i++] = OSSL_PARAM_construct_octet_string (OSSL_KDF_PARAM_KEY, + (void *) (g_bytes_get_data (key, NULL)), + g_bytes_get_size (key)); + + params[i++] = OSSL_PARAM_construct_octet_string (OSSL_KDF_PARAM_SALT, + (void *) label, + strlen (label)); + + if (context_a) + params[i++] = OSSL_PARAM_construct_octet_string (OSSL_KDF_PARAM_INFO, + (void *) (g_bytes_get_data (context_a, NULL)), + g_bytes_get_size (context_a)); + + if (context_b) + params[i++] = OSSL_PARAM_construct_octet_string (OSSL_KDF_PARAM_INFO, + (void *) (g_bytes_get_data (context_b, NULL)), + g_bytes_get_size (context_b)); + + params[i++] = OSSL_PARAM_construct_end (); + + kdf = EVP_KDF_fetch (NULL, "KBKDF", NULL); + kdf_ctx = EVP_KDF_CTX_new (kdf); + + secret = g_malloc0 (length); + if (!EVP_KDF_derive (kdf_ctx, secret, length, params)) + { + g_propagate_error (error, fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "OpenSSL error during key derivation " + "for label '%s'", label)); + g_free (secret); + EVP_KDF_CTX_free (kdf_ctx); + EVP_KDF_free (kdf); + print_openssl_errors (); + return NULL; + } + + EVP_KDF_CTX_free (kdf_ctx); + EVP_KDF_free (kdf); + + res = g_bytes_new_take (secret, length); + + return g_steal_pointer (&res); +} + +static GBytes * +fpi_sdcp_mac (GBytes *application_secret, + const gchar *label, + GBytes *data_a, + GBytes *data_b, + GError **error) +{ + g_autoptr(GBytes) res = NULL; + + EVP_MAC *hmac; + EVP_MAC_CTX *hmac_ctx; + guint8 *mac = NULL; + gsize mac_len = 0; + + OSSL_PARAM params[] = { + OSSL_PARAM_construct_utf8_string (OSSL_MAC_PARAM_DIGEST, (gchar *) "SHA256", 0), + OSSL_PARAM_construct_end (), + }; + + hmac = EVP_MAC_fetch (NULL, "hmac", NULL); + hmac_ctx = EVP_MAC_CTX_new (hmac); + + if (!EVP_MAC_init (hmac_ctx, + g_bytes_get_data (application_secret, NULL), + g_bytes_get_size (application_secret), + params)) + goto out_error; + + if (label) + if (!EVP_MAC_update (hmac_ctx, (guchar *) label, strlen (label) + 1)) /* +1 due to unsigned */ + goto out_error; + + if (data_a) + if (!EVP_MAC_update (hmac_ctx, g_bytes_get_data (data_a, NULL), g_bytes_get_size (data_a))) + goto out_error; + + if (data_b) + if (!EVP_MAC_update (hmac_ctx, g_bytes_get_data (data_b, NULL), g_bytes_get_size (data_b))) + goto out_error; + + mac = g_malloc0 (SDCP_MAC_SIZE); + if (!EVP_MAC_final (hmac_ctx, mac, &mac_len, SDCP_MAC_SIZE)) + goto out_error; + + if (mac_len != SDCP_MAC_SIZE) + goto out_error; + + EVP_MAC_CTX_free (hmac_ctx); + EVP_MAC_free (hmac); + + res = g_bytes_new_take (mac, mac_len); + return g_steal_pointer (&res); + +out_error: + g_propagate_error (error, fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "OpenSSL error generating MAC for label '%s'", + label)); + print_openssl_errors (); + g_clear_pointer (&mac, g_free); + g_clear_pointer (&hmac_ctx, EVP_MAC_CTX_free); + g_clear_pointer (&hmac, EVP_MAC_free); + return NULL; +} + +static GBytes * +fpi_sdcp_get_private_key (EVP_PKEY *pkey, + GError **error) +{ + g_autoptr(GBytes) res = NULL; + BIGNUM *priv_bn = NULL; + guint8 *priv = NULL; + gsize priv_len = 0; + + g_assert_nonnull (pkey); + + if (!EVP_PKEY_get_bn_param (pkey, OSSL_PKEY_PARAM_PRIV_KEY, &priv_bn)) + goto out_error; + + priv_len = BN_num_bytes (priv_bn); + if (priv_len != SDCP_PRIVATE_KEY_SIZE) + goto out_error; + + priv = g_malloc0 (priv_len); + if (!BN_bn2bin (priv_bn, priv)) + goto out_error; + + BN_clear_free (priv_bn); + + res = g_bytes_new_take (priv, priv_len); + + return g_steal_pointer (&res); + +out_error: + g_propagate_error (error, fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "OpenSSL error getting private key bytes")); + print_openssl_errors (); + g_clear_pointer (&priv, g_free); + g_clear_pointer (&priv_bn, BN_clear_free); + return NULL; +} + +static GBytes * +fpi_sdcp_get_public_key (EVP_PKEY *pkey, + GError **error) +{ + g_autoptr(GBytes) res = NULL; + guint8 *pub = NULL; + gsize pub_len = 0; + + g_assert_nonnull (pkey); + + pub = g_malloc0 (SDCP_PUBLIC_KEY_SIZE); + if (!EVP_PKEY_get_octet_string_param (pkey, OSSL_PKEY_PARAM_PUB_KEY, + pub, SDCP_PUBLIC_KEY_SIZE, &pub_len)) + goto out_error; + + if (pub_len != SDCP_PUBLIC_KEY_SIZE) + goto out_error; + + res = g_bytes_new_take (pub, pub_len); + + return g_steal_pointer (&res); + +out_error: + g_propagate_error (error, fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "OpenSSL error getting public key bytes")); + print_openssl_errors (); + g_clear_pointer (&pub, g_free); + return NULL; +} + +static EVP_PKEY * +fpi_sdcp_get_private_pkey (GBytes *private_key, + GError **error) +{ + BIGNUM *private_key_bn = NULL; + EC_GROUP *group = NULL; + EC_POINT *public_key_point = NULL; + g_autofree guint8 *public_key_buf = NULL; + EVP_PKEY_CTX *ctx = NULL; + EVP_PKEY *key = NULL; + OSSL_PARAM_BLD *param_bld = NULL; + OSSL_PARAM *params = NULL; + + if (g_bytes_get_size (private_key) != SDCP_PRIVATE_KEY_SIZE) + goto out_error; + + /* import private key as a BIGNUM */ + private_key_bn = BN_bin2bn (g_bytes_get_data (private_key, NULL), + g_bytes_get_size (private_key), + NULL); + + /* set up public key based on imported private_key_bn */ + group = EC_GROUP_new_by_curve_name (NID_X9_62_prime256v1); + public_key_point = EC_POINT_new (group); + if (!EC_POINT_mul (group, public_key_point, private_key_bn, NULL, NULL, NULL)) + goto out_error; + public_key_buf = g_malloc0 (SDCP_PUBLIC_KEY_SIZE); + if (!EC_POINT_point2oct (group, public_key_point, POINT_CONVERSION_UNCOMPRESSED, + public_key_buf, SDCP_PUBLIC_KEY_SIZE, NULL)) + goto out_error; + EC_POINT_free (public_key_point); + EC_GROUP_free (group); + + /* set up parameters */ + + param_bld = OSSL_PARAM_BLD_new (); + if (!OSSL_PARAM_BLD_push_utf8_string(param_bld, OSSL_PKEY_PARAM_GROUP_NAME, + SN_X9_62_prime256v1, sizeof (SN_X9_62_prime256v1))) + goto out_error; + if (!OSSL_PARAM_BLD_push_octet_string(param_bld, OSSL_PKEY_PARAM_PUB_KEY, + public_key_buf, SDCP_PUBLIC_KEY_SIZE)) + goto out_error; + if (!OSSL_PARAM_BLD_push_BN(param_bld, OSSL_PKEY_PARAM_PRIV_KEY, private_key_bn)) + goto out_error; + + params = OSSL_PARAM_BLD_to_param (param_bld); + + /* import pkey from params */ + ctx = EVP_PKEY_CTX_new_from_name (NULL, "EC", NULL); + if (!EVP_PKEY_fromdata_init (ctx)) + goto out_error; + EVP_PKEY_fromdata (ctx, &key, EVP_PKEY_KEYPAIR, params); + + EVP_PKEY_CTX_free (ctx); + OSSL_PARAM_free (params); + OSSL_PARAM_BLD_free (param_bld); + BN_clear_free (private_key_bn); + + return g_steal_pointer (&key); + +out_error: + g_propagate_error (error, fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "OpenSSL error getting private key")); + print_openssl_errors (); + g_clear_pointer (&key, EVP_PKEY_free); + g_clear_pointer (&ctx, EVP_PKEY_CTX_free); + g_clear_pointer (¶ms, OSSL_PARAM_free); + g_clear_pointer (¶m_bld, OSSL_PARAM_BLD_free); + g_clear_pointer (&public_key_point, EC_POINT_free); + g_clear_pointer (&group, EC_GROUP_free); + g_clear_pointer (&private_key_bn, BN_clear_free); + return NULL; +} + +static EVP_PKEY * +fpi_sdcp_get_public_pkey (GBytes *public_key, + GError **error) +{ + EVP_PKEY_CTX *ctx = NULL; + EVP_PKEY *key = NULL; + OSSL_PARAM params[3]; + + params[0] = OSSL_PARAM_construct_utf8_string (OSSL_PKEY_PARAM_GROUP_NAME, + (char *) SN_X9_62_prime256v1, + sizeof (SN_X9_62_prime256v1)); + params[1] = OSSL_PARAM_construct_octet_string (OSSL_PKEY_PARAM_PUB_KEY, + (char *) g_bytes_get_data (public_key, NULL), + g_bytes_get_size (public_key)); + params[2] = OSSL_PARAM_construct_end (); + + ctx = EVP_PKEY_CTX_new_from_name (NULL, "EC", NULL); + if (!ctx) + goto out_error; + + if (!EVP_PKEY_fromdata_init (ctx)) + goto out_error; + + if (!EVP_PKEY_fromdata(ctx, &key, EVP_PKEY_PUBLIC_KEY, params)) + goto out_error; + + EVP_PKEY_CTX_free (ctx); + + return g_steal_pointer (&key); + +out_error: + g_propagate_error (error, fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "OpenSSL error getting public key")); + print_openssl_errors (); + g_clear_pointer (&key, EVP_PKEY_free); + g_clear_pointer (&ctx, EVP_PKEY_CTX_free); + return NULL; +} + +static GBytes * +fpi_sdcp_hash_claim (FpiSdcpClaim *claim, + GError **error) +{ + g_autoptr(GBytes) res = NULL; + EVP_MD_CTX *sh256_ctx; + EVP_MD *sha256; + guint8 *hash = NULL; + guint hash_len = 0; + + sh256_ctx = EVP_MD_CTX_create (); + sha256 = EVP_MD_fetch (NULL, "SHA256", NULL); + if (!EVP_DigestInit_ex (sh256_ctx, sha256, NULL)) + goto out_error; + + if (!EVP_DigestUpdate (sh256_ctx, + g_bytes_get_data (claim->model_certificate, NULL), + g_bytes_get_size (claim->model_certificate))) + goto out_error; + + if (!EVP_DigestUpdate (sh256_ctx, + g_bytes_get_data (claim->device_public_key, NULL), + g_bytes_get_size (claim->device_public_key))) + goto out_error; + + if (!EVP_DigestUpdate (sh256_ctx, + g_bytes_get_data (claim->firmware_public_key, NULL), + g_bytes_get_size (claim->firmware_public_key))) + goto out_error; + + if (!EVP_DigestUpdate (sh256_ctx, + g_bytes_get_data (claim->firmware_hash, NULL), + g_bytes_get_size (claim->firmware_hash))) + goto out_error; + + if (!EVP_DigestUpdate (sh256_ctx, + g_bytes_get_data (claim->model_signature, NULL), + g_bytes_get_size (claim->model_signature))) + goto out_error; + + if (!EVP_DigestUpdate (sh256_ctx, + g_bytes_get_data (claim->device_signature, NULL), + g_bytes_get_size (claim->device_signature))) + goto out_error; + + hash = g_malloc0 (EVP_MAX_MD_SIZE); + if (!EVP_DigestFinal_ex (sh256_ctx, hash, &hash_len)) + goto out_error; + + EVP_MD_CTX_free (sh256_ctx); + EVP_MD_free (sha256); + + res = g_bytes_new_take (hash, hash_len); + return g_steal_pointer (&res); + +out_error: + g_propagate_error (error, fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "OpenSSL error hashing the SDCP claim")); + print_openssl_errors (); + g_clear_pointer (&sh256_ctx, EVP_MD_CTX_free); + g_clear_pointer (&sha256, EVP_MD_free); + g_clear_pointer (&hash, g_free); + return NULL; +} + +static GBytes * +fpi_sdcp_key_agreement (GBytes *host_private_key, + GBytes *firmware_public_key, + GError **error) +{ + g_autoptr(GBytes) res = NULL; + EVP_PKEY_CTX *ctx = NULL; + EVP_PKEY *host_pkey = NULL; + EVP_PKEY *device_pkey = NULL; + guint8 *key_agreement = NULL; + gsize key_agreement_len = 0; + + host_pkey = fpi_sdcp_get_private_pkey (host_private_key, error); + if (*error) + goto out; + + device_pkey = fpi_sdcp_get_public_pkey (firmware_public_key, error); + if (*error) + goto out; + + ctx = EVP_PKEY_CTX_new_from_pkey (NULL, host_pkey, NULL); + + if (!EVP_PKEY_derive_init (ctx)) + goto out_error; + + if (!EVP_PKEY_derive_set_peer (ctx, device_pkey)) + goto out_error; + + /* Get the size by passing NULL as the buffer */ + if (!EVP_PKEY_derive (ctx, NULL, &key_agreement_len)) + goto out_error; + + if (key_agreement_len != SDCP_KEY_AGREEMENT_SIZE) + goto out_error; + + /* Then get the derived shared secret using the fetched size */ + key_agreement = g_malloc0 (key_agreement_len); + if (!EVP_PKEY_derive (ctx, key_agreement, &key_agreement_len)) + goto out_error; + + EVP_PKEY_CTX_free (ctx); + EVP_PKEY_free (device_pkey); + EVP_PKEY_free (host_pkey); + + res = g_bytes_new_take (key_agreement, key_agreement_len); + + return g_steal_pointer (&res); + +out_error: + g_propagate_error (error, fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "OpenSSL error deriving key agreement")); + print_openssl_errors (); +out: + g_clear_pointer (&key_agreement, g_free); + g_clear_pointer (&ctx, EVP_PKEY_CTX_free); + g_clear_pointer (&host_pkey, EVP_PKEY_free); + g_clear_pointer (&device_pkey, EVP_PKEY_free); + return NULL; +} + +/******************************************************************************/ + +void +fpi_sdcp_generate_host_key (GBytes **private_key, + GBytes **public_key, + GError **error) +{ + EVP_PKEY *key = EVP_EC_gen ("P-256"); + + if (*private_key) + g_bytes_unref (*private_key); + *private_key = fpi_sdcp_get_private_key (key, error); + if (*error) + goto out; + + if (*public_key) + g_bytes_unref (*public_key); + *public_key = fpi_sdcp_get_public_key (key, error); + if (*error) + goto out; + + EVP_PKEY_free (key); + + return; + +out: + print_openssl_errors (); + g_clear_pointer (&key, EVP_PKEY_free); + return; +} + +GBytes * +fpi_sdcp_generate_random (GError **error) +{ + g_autoptr(GBytes) res = NULL; + guint8 *random = g_malloc0 (SDCP_RANDOM_SIZE); + + if (!RAND_bytes (random, SDCP_RANDOM_SIZE)) + { + g_propagate_error (error, + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "OpenSSL error generating random")); + print_openssl_errors (); + g_free (random); + return NULL; + } + + res = g_bytes_new_take (random, SDCP_RANDOM_SIZE); + + return g_steal_pointer (&res); +} + +gboolean +fpi_sdcp_verify_connect (GBytes *host_private_key, + GBytes *host_random, + GBytes *device_random, + FpiSdcpClaim *claim, + GBytes *mac, + gboolean validate_certificate, + gboolean verify_signatures, + GBytes **application_secret, + GError **error) +{ + g_autoptr(GBytes) claim_hash = NULL; + g_autoptr(GBytes) claim_mac = NULL; + g_autoptr(GBytes) key_agreement = NULL; + g_autoptr(GBytes) master_secret = NULL; + g_autoptr(GBytes) application_keys = NULL; + g_autoptr(GBytes) out_app_secret = NULL; + guint8 *app_secret_buf = NULL; + + const guint8 *cert_ptr; + gsize cert_len = 0; + X509 *cert = NULL; + EVP_PKEY *cert_public_pkey = NULL; + EVP_PKEY *device_public_pkey = NULL; + + fp_dbg ("SDCP Verify ConnectResponse"); + + /* SDCP Connect: 5.i. Perform key agreement */ + + key_agreement = fpi_sdcp_key_agreement (host_private_key, claim->firmware_public_key, error); + if (*error) + goto out_error; + + fp_dbg ("key_agreement:"); + fp_dbg_hex_dump_gbytes (key_agreement); + + /* SDCP Connect: 5.ii. Derive master secret */ + + master_secret = fpi_sdcp_kdf (key_agreement, "master secret", host_random, device_random, + SDCP_MASTER_SECRET_SIZE, error); + if (*error) + goto out_error; + + fp_dbg ("master_secret:"); + fp_dbg_hex_dump_gbytes (master_secret); + + /* SDCP Connect: 5.iii. Derive MAC secret and symetric key */ + + application_keys = fpi_sdcp_kdf (master_secret, "application keys", NULL, NULL, + SDCP_APPLICATION_KEYS_SIZE, error); + if (*error) + goto out_error; + + fp_dbg ("application_keys:"); + fp_dbg_hex_dump_gbytes (application_keys); + + /* slice first half of application_keys as application_secret */ + g_assert (g_bytes_get_size (application_keys) >= SDCP_APPLICATION_SECRET_SIZE); + app_secret_buf = g_malloc0 (SDCP_APPLICATION_SECRET_SIZE); + memcpy (app_secret_buf, + g_bytes_get_data (application_keys, NULL), + SDCP_APPLICATION_SECRET_SIZE); + + out_app_secret = g_bytes_new_take (app_secret_buf, SDCP_APPLICATION_SECRET_SIZE); + + fp_dbg ("application_secret:"); + fp_dbg_hex_dump_gbytes (out_app_secret); + + /* SDCP Connect: 5.iv. Validate the MAC over H(claim) */ + + claim_hash = fpi_sdcp_hash_claim (claim, error); + if (*error) + goto out_error; + + claim_mac = fpi_sdcp_mac (out_app_secret, "connect", claim_hash, NULL, error); + if (*error) + goto out_error; + + fp_dbg ("Device MAC:"); + fp_dbg_hex_dump_gbytes (mac); + + fp_dbg ("Host MAC:"); + fp_dbg_hex_dump_gbytes (claim_mac); + + if (g_bytes_equal (mac, claim_mac)) + { + fp_dbg ("SDCP ConnectResponse claim validated successfully"); + } + else + { + fp_warn ("SDCP ConnectResponse claim validation failed"); + goto out; + } + + + /* SDCP Connect: 5.v. Unpack the claim (SKIP; already done) */ + /* SDCP Connect: 5.vi. Verify the claim */ + + if (validate_certificate) + { + /* Validate the certificate */ + + cert_ptr = g_bytes_get_data (claim->model_certificate, &cert_len); + cert = d2i_X509 (NULL, &cert_ptr, cert_len); + if (!cert) + { + g_propagate_error (error, + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Error parsing model certificate")); + goto out; + } + + print_certificate (cert); + + if (fpi_sdcp_verify_certificate (cert, error)) + { + fp_dbg ("SDCP model ceritifcate verified successfully"); + } + else + { + fp_dbg ("SDCP model certificate verification failed"); + goto out; + } + + if (verify_signatures) + { + /* Get the certificate's public key */ + + cert_public_pkey = X509_get_pubkey (cert); + if (!cert_public_pkey) + { + g_propagate_error (error, + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Error getting public key from model certificate")); + goto out; + } + + /* Verify(pk_m, H(pk_d), s_m) */ + + fp_dbg ("model_signature:"); + fp_dbg_hex_dump_gbytes (claim->model_signature); + + if (!fpi_sdcp_verify_signature (cert_public_pkey, + NULL, + claim->device_public_key, + NULL, + claim->model_signature, + error)) + { + fp_warn ("SDCP model signature verification failed"); + goto out_error; + } + + fp_dbg ("SDCP model signature verified successfully"); + EVP_PKEY_free (cert_public_pkey); + + /* Verify(pk_d, H(C001||h_f||pk_f), s_d) */ + + fp_dbg ("device_public_key:"); + fp_dbg_hex_dump_gbytes (claim->device_public_key); + + fp_dbg ("firmware_hash:"); + fp_dbg_hex_dump_gbytes (claim->firmware_hash); + + fp_dbg ("firmware_public_key:"); + fp_dbg_hex_dump_gbytes (claim->firmware_public_key); + + device_public_pkey = fpi_sdcp_get_public_pkey (claim->device_public_key, error); + if (*error) + goto out_error; + + if (!fpi_sdcp_verify_signature (device_public_pkey, + "C001", + claim->firmware_hash, + claim->firmware_public_key, + claim->model_signature, + error)) + { + fp_warn ("SDCP device signature verification failed"); + goto out_error; + } + + fp_dbg ("SDCP device signature verified successfully"); + EVP_PKEY_free (device_public_pkey); + } + X509_free (cert); + } + + /* output application_secret */ + + if (*application_secret) + g_bytes_unref (*application_secret); + + *application_secret = g_steal_pointer (&out_app_secret); + + return TRUE; + +out_error: + print_openssl_errors (); +out: + if (!out_app_secret) + g_clear_pointer (&app_secret_buf, g_free); + g_clear_pointer (&device_public_pkey, EVP_PKEY_free); + g_clear_pointer (&cert_public_pkey, EVP_PKEY_free); + g_clear_pointer (&cert, X509_free); + return FALSE; +} + +gboolean +fpi_sdcp_verify_reconnect (GBytes *application_secret, + GBytes *random, + GBytes *mac, + GError **error) +{ + g_autoptr(GBytes) host_mac = NULL; + + fp_dbg ("SDCP Verify ReconnectResponse"); + + host_mac = fpi_sdcp_mac (application_secret, "reconnect", random, NULL, error); + if (*error) + return FALSE; + + fp_dbg ("Device MAC:"); + fp_dbg_hex_dump_gbytes (mac); + + fp_dbg ("Host MAC:"); + fp_dbg_hex_dump_gbytes (host_mac); + + if (g_bytes_equal (mac, host_mac)) + { + fp_dbg ("SDCP ReconnectResponse verified successfully"); + return TRUE; + } + else + { + fp_warn ("SDCP ReconnectResponse verification failed"); + return FALSE; + } +} + +gboolean +fpi_sdcp_verify_identify (GBytes *application_secret, + GBytes *nonce, + GBytes *id, + GBytes *mac, + GError **error) +{ + g_autoptr(GBytes) host_mac = NULL; + + fp_dbg ("SDCP Verify AuthorizedIdentity"); + + host_mac = fpi_sdcp_mac (application_secret, "identify", nonce, id, error); + if (*error) + return FALSE; + + fp_dbg ("Device MAC:"); + fp_dbg_hex_dump_gbytes (mac); + + fp_dbg ("Host MAC:"); + fp_dbg_hex_dump_gbytes (host_mac); + + if (g_bytes_equal (mac, host_mac)) + { + fp_dbg ("SDCP AuthorizedIdentity verified successfully"); + return TRUE; + } + else + { + fp_warn ("SDCP AuthorizedIdentity verification failed"); + return FALSE; + } +} + +GBytes * +fpi_sdcp_generate_enrollment_id (GBytes *application_secret, + GBytes *nonce, + GError **error) +{ + g_autoptr(GBytes) id = NULL; + + id = fpi_sdcp_mac (application_secret, "enroll", nonce, NULL, error); + if (*error) + return NULL; + + fp_dbg ("Generated SDCP Enrollment ID:"); + fp_dbg_hex_dump_gbytes (id); + + return g_steal_pointer (&id); +} diff --git a/libfprint/fpi-sdcp.h b/libfprint/fpi-sdcp.h new file mode 100644 index 00000000..a32dc05e --- /dev/null +++ b/libfprint/fpi-sdcp.h @@ -0,0 +1,132 @@ +/* + * Secure Device Connection Protocol (SDCP) support implementation + * Copyright (C) 2025 Joshua Grisham + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include "fpi-compat.h" + +#include "fpi-sdcp-device.h" + +/** + * fpi_sdcp_generate_host_key: + * @private_key: (out) (transfer full): The host private key (sk_h) + * @public_key: (out) (transfer full): The host public key (pk_h) + * @error: (out): #GError in case the out values are %NULL + * + * Function to generate a new ephemeral ECDH key pair for use with SDCP. + **/ +void fpi_sdcp_generate_host_key (GBytes **private_key, + GBytes **public_key, + GError **error); + +/** + * fpi_sdcp_generate_random: + * @error: (out): #GError in case the return value is %NULL + * + * Returns: A new #GBytes with a secure random of length %SDCP_RANDOM_SIZE + **/ +GBytes *fpi_sdcp_generate_random (GError **error); + +/** + * fpi_sdcp_verify_connect: + * @host_private_key: Private key generated using fpi_sdcp_generate_host_key() (sk_h) + * @host_random: Random generated using fpi_sdcp_generate_random() (r_h) + * @device_random: The random provided in the device's ConnectResponse (r_d) + * @claim: #FpiSdcpClaim provided in the device's ConnectResponse (c) + * @mac: The MAC provided in the device's ConnectResponse (m) + * @validate_certificate: If the model certificate (cert_m) should be parsed and + * its trust chain validated as issued from Microsoft's well-known issuers + * @verify_signatures: If the model signature (s_m) and device signature (s_d) + * should be validated against the certificate and keys provided in the claim + * @application_secret: (out) (transfer full): A new #GBytes with the derived + * application secret (s) of length %SDCP_APPLICATION_SECRET_SIZE + * @error: (out): #GError in case the return value is %NULL + * + * High level function which internally handles the derivation of all necessary + * SDCP-related keys and secrets from the device's ConnectResponse and derives + * the application secret for use with all other SDCP-related functions. + * + * This function will also perform a validation of the ConnectResponse MAC and + * optionally perform additional verifications based on the provided + * @validate_certificate and @verify_signatures booleans. If any of these these + * validations fail then %NULL will be returned, indicating that the SDCP secure + * connection channel could not be established. + * + * Returns: %TRUE if the @application_secret was successfully derived and the + * ConnectResponse has been successfully verified + **/ +gboolean fpi_sdcp_verify_connect (GBytes *host_private_key, + GBytes *host_random, + GBytes *device_random, + FpiSdcpClaim *claim, + GBytes *mac, + gboolean validate_certificate, + gboolean verify_signatures, + GBytes **application_secret, + GError **error); + +/** + * fpi_sdcp_verify_reconnect: + * @application_secret: The host's derived application secret (s) + * @random: The host-generated random sent to the device's Reconnect command (r) + * @mac: The MAC provided in the device's ReconnectResponse (m) + * @error: (out): #GError in case the return value is %FALSE + * + * Verifies the SDCP ReconnectResponse. + * + * Returns: %TRUE if the ReconnectResponse is verified successfully + **/ +gboolean fpi_sdcp_verify_reconnect (GBytes *application_secret, + GBytes *random, + GBytes *mac, + GError **error); + +/** + * fpi_sdcp_verify_identify: + * @application_secret: The host's derived application secret (s) + * @nonce: The host-generated nonce sent to the device's Identify command (n) + * @id: The ID provided in the device's AuthorizedIdentity (id) + * @mac: The MAC provided in the device's AuthorizedIdentity (m) + * @error: (out): #GError in case the return value is %FALSE + * + * Verifies the SDCP ReconnectResponse. + * + * Returns: %TRUE if the ReconnectResponse is verified successfully + **/ +gboolean fpi_sdcp_verify_identify (GBytes *application_secret, + GBytes *nonce, + GBytes *id, + GBytes *mac, + GError **error); + +/** + * fpi_sdcp_generate_enrollment_id: + * @application_secret: The host's derived application secret (s) + * @nonce: The nonce received from the device in response to the EnrollBegin + * command (n) + * @error: (out): #GError in case the return value is %NULL + * + * Generates a new id for use with the device's EnrollCommit command. + * + * Returns: A new #GBytes with the generated enrollment id of length + * %SDCP_ENROLLMENT_ID_SIZE + **/ +GBytes *fpi_sdcp_generate_enrollment_id (GBytes *application_secret, + GBytes *nonce, + GError **error); diff --git a/libfprint/meson.build b/libfprint/meson.build index f83dd71b..f132fe60 100644 --- a/libfprint/meson.build +++ b/libfprint/meson.build @@ -21,6 +21,7 @@ libfprint_private_sources = [ 'fpi-device.c', 'fpi-image-device.c', 'fpi-image.c', + 'fpi-log.c', 'fpi-print.c', 'fpi-ssm.c', 'fpi-usb-transfer.c', @@ -47,8 +48,8 @@ libfprint_private_headers = [ 'fpi-log.h', 'fpi-minutiae.h', 'fpi-print.h', - 'fpi-usb-transfer.h', 'fpi-ssm.h', + 'fpi-usb-transfer.h', ] + spi_headers nbis_sources = [ @@ -143,8 +144,6 @@ driver_sources = { [ 'drivers/virtual-device.c' ], 'virtual_device_storage' : [ 'drivers/virtual-device-storage.c' ], - 'virtual_sdcp' : - [ 'drivers/virtual-sdcp.c' ], 'synaptics' : [ 'drivers/synaptics/synaptics.c', 'drivers/synaptics/bmkt_message.c' ], 'goodixmoc' : @@ -158,8 +157,6 @@ driver_sources = { } helper_sources = { - 'sdcp' : - [ 'fp-sdcp-device.c', 'fpi-sdcp-device.c' ], 'aeslib' : [ 'drivers/aeslib.c' ], 'aesx660' : @@ -168,6 +165,8 @@ helper_sources = { [ 'drivers/aes3k.c' ], 'openssl' : [ ], + 'sdcp' : + [ ], 'udev' : [ ], 'virtual' : @@ -183,6 +182,24 @@ foreach helper : driver_helpers drivers_sources += helper_sources[helper] endforeach +subdir('sdcp') +if 'sdcp' in driver_helpers + libfprint_sources += [ + 'fp-sdcp-device.c', + ] + libfprint_private_sources += [ + 'fpi-sdcp.c', + 'fpi-sdcp-device.c', + sdcp_truststore_resource_c, + ] + libfprint_public_headers += [ + 'fp-sdcp-device.h', + ] + libfprint_private_headers += [ + 'fpi-sdcp.h', + 'fpi-sdcp-device.h', + ] +endif fp_enums = gnome.mkenums_simple('fp-enums', sources: libfprint_public_headers, diff --git a/libfprint/sdcp/generate-gresource.py b/libfprint/sdcp/generate-gresource.py new file mode 100644 index 00000000..8acb7149 --- /dev/null +++ b/libfprint/sdcp/generate-gresource.py @@ -0,0 +1,25 @@ +#!/usr/bin/python3 + +import os +import sys + +if len(sys.argv) != 3: + print("generate-gresource.py: Generates SDCP Truststore GResource XML file from certificates in ./truststore/*.pem") + print("Usage: generate-gresource.py ") + sys.exit(1) + +gresource_prefix = "/org/freedesktop/fprint/sdcp" +relative_folder = "truststore" +full_folder = os.path.join(sys.argv[1], relative_folder) +output = sys.argv[2] + +files = [f for f in os.listdir(full_folder) if os.path.isfile(os.path.join(full_folder, f)) and f.endswith('.pem')] + +with open(output, 'w') as f: + f.write('\n') + f.write('\n') + f.write(f' \n') + for file in files: + f.write(f' {relative_folder}/{file}\n') + f.write(' \n') + f.write('\n') diff --git a/libfprint/sdcp/meson.build b/libfprint/sdcp/meson.build new file mode 100644 index 00000000..79920c82 --- /dev/null +++ b/libfprint/sdcp/meson.build @@ -0,0 +1,32 @@ +sdcp_truststore_gresource_xml = custom_target('sdcp-truststore.gresource', + input : 'generate-gresource.py', + output : 'sdcp-truststore.gresource.xml', + command : [find_program('python3'), '@INPUT@', meson.current_source_dir(), '@OUTPUT@'], +) + +sdcp_truststore_resource_h = custom_target('fpi-sdcp-truststore-resource.h', + input : sdcp_truststore_gresource_xml, + output : 'fpi-sdcp-truststore-resource.h', + command : ['glib-compile-resources', + '--target=@OUTPUT@', + '--sourcedir=' + meson.current_source_dir(), + '--internal', + '--generate', + '--c-name', 'fpi_sdcp_truststore', + '--manual-register', + '@INPUT@'] +) + +sdcp_truststore_resource_c = custom_target('fpi-sdcp-truststore-resource.c', + depends : [sdcp_truststore_resource_h], + input : sdcp_truststore_gresource_xml, + output : 'fpi-sdcp-truststore-resource.c', + command : ['glib-compile-resources', + '--target=@OUTPUT@', + '--sourcedir=' + meson.current_source_dir(), + '--internal', + '--generate', + '--c-name', 'fpi_sdcp_truststore', + '--manual-register', + '@INPUT@'] +) diff --git a/libfprint/sdcp/truststore/Microsoft ECC Devices Root Certificate Authority 2017.pem b/libfprint/sdcp/truststore/Microsoft ECC Devices Root Certificate Authority 2017.pem new file mode 100644 index 00000000..50e05f3f --- /dev/null +++ b/libfprint/sdcp/truststore/Microsoft ECC Devices Root Certificate Authority 2017.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDIDCCAqWgAwIBAgIQKs9yK9kUXqlMVB+fSF1UMjAKBggqhkjOPQQDAzCBlDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v +bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE+MDwGA1UEAxM1TWlj +cm9zb2Z0IEVDQyBEZXZpY2VzIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw +MTcwHhcNMTcxMTA5MTk0MDQ4WhcNNDIxMTA5MTk0ODE5WjCBlDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV +BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE+MDwGA1UEAxM1TWljcm9zb2Z0IEVD +QyBEZXZpY2VzIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAARiivDX0DS0EXoGlfbd2PwxSC87Cszr6/aAjSx6pMwU +4kzXcId0dhrjSkPSIO5UCz50ggQGQiTwqRzyhM44FlEyzbzl6OHGDwR1vAg3wdmm +WEXWySzyAZKsfkwg0G7bPkijgbkwgbYwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFBTaW/EOZkfRXRNfW3rr618BCLVJMBAGCSsGAQQBgjcV +AQQDAgEAMGUGA1UdIAReMFwwBgYEVR0gADBSBgwrBgEEAYI3TIN9AQEwQjBABggr +BgEFBQcCARY0aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1Jl +cG9zaXRvcnkuaHRtADAKBggqhkjOPQQDAwNpADBmAjEAxxAFFL8juLXiulXvZgBQ +pGGPCcV2Tr3CorZ4p/uO2/rtBemqhL3CjKAm40VlhEz8AjEArE5fhA54SEDjoTwZ +VUosaqXa8ych31qjZI+e1ttbOPebAZTt9ac7+lTzJcLTEQch +-----END CERTIFICATE----- diff --git a/libfprint/sdcp/truststore/Windows Hello 2096ADCC CA 2021.pem b/libfprint/sdcp/truststore/Windows Hello 2096ADCC CA 2021.pem new file mode 100644 index 00000000..72ec3efb --- /dev/null +++ b/libfprint/sdcp/truststore/Windows Hello 2096ADCC CA 2021.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDwzCCA0qgAwIBAgITMwAAAAuNaMBkOddK4wAAAAAACzAKBggqhkjOPQQDAjCB +hDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl +ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEuMCwGA1UEAxMl +V2luZG93cyBIZWxsbyBTZWN1cmUgRGV2aWNlcyBQQ0EgMjAxODAeFw0yMTEyMDky +MzI1MDFaFw0zMDEyMDkyMzM1MDFaMFYxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVN +aWNyb3NvZnQgQ29ycG9yYXRpb24xJzAlBgNVBAMTHldpbmRvd3MgSGVsbG8gMjA5 +NkFEQ0MgQ0EgMjAyMTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJWgPvl44Gei +RrTuA3f1eT60pAlBWM7ym7WSchqz3hge1WS8RUxPVedu0f7MCe/R6O6RVjV7HWq4 +c6jo9FwoiAGjggHGMIIBwjAQBgkrBgEEAYI3FQEEAwIBADAdBgNVHQ4EFgQUvzdI +40pjLelTo7qJApjAaUcqmbkwVAYDVR0gBE0wSzBJBgRVHSAAMEEwPwYIKwYBBQUH +AgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0 +b3J5Lmh0bTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTaykvQTFYDJ1+X63WjAsO/RZz4 +sTBoBgNVHR8EYTBfMF2gW6BZhldodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtp +b3BzL2NybC9XaW5kb3dzJTIwSGVsbG8lMjBTZWN1cmUlMjBEZXZpY2VzJTIwUENB +JTIwMjAxOC5jcmwwdQYIKwYBBQUHAQEEaTBnMGUGCCsGAQUFBzAChllodHRwOi8v +d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL1dpbmRvd3MlMjBIZWxsbyUy +MFNlY3VyZSUyMERldmljZXMlMjBQQ0ElMjAyMDE4LmNydDAKBggqhkjOPQQDAgNn +ADBkAjAeGyYlzf+uBQXI/EW84I5CGFbo/U6dL4k1Y83f2p94d0wrNwjUb/yprb4+ +L9+OKfQCME8PgRyJQxsvsne+WI6gr0ZzJilotoiRdvDzlMK4+hx5TGJWTV17AsAo +z2330epHQQ== +-----END CERTIFICATE----- diff --git a/libfprint/sdcp/truststore/Windows Hello Secure Devices PCA 2018.pem b/libfprint/sdcp/truststore/Windows Hello Secure Devices PCA 2018.pem new file mode 100644 index 00000000..690ee90f --- /dev/null +++ b/libfprint/sdcp/truststore/Windows Hello Secure Devices PCA 2018.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEVjCCA9ygAwIBAgITMwAAAANsz+3iRHAZvwAAAAAAAzAKBggqhkjOPQQDAzCB +lDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl +ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE+MDwGA1UEAxM1 +TWljcm9zb2Z0IEVDQyBEZXZpY2VzIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +IDIwMTcwHhcNMTgwMTI1MTk0OTM4WhcNMzMwMTI1MTk1OTM4WjCBhDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAc +BgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEuMCwGA1UEAxMlV2luZG93cyBI +ZWxsbyBTZWN1cmUgRGV2aWNlcyBQQ0EgMjAxODB2MBAGByqGSM49AgEGBSuBBAAi +A2IABB3dCAIDJXUg4nGLrSgJgukG7oPFOmxLcZJQTiDpcrT8UyrvXcyatM12uJSX +RLJxDsmxFgOhZSu56F1f8jAu3bErIPy+AIjqH6d/mYSYfHE+TTSDaZsIy3iyS73X +Pr5noKOCAfwwggH4MBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBTaykvQTFYD +J1+X63WjAsO/RZz4sTBlBgNVHSAEXjBcMAYGBFUdIAAwUgYMKwYBBAGCN0yDfQEB +MEIwQAYIKwYBBQUHAgEWNGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMv +RG9jcy9SZXBvc2l0b3J5Lmh0bQAwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEw +CwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUFNpb8Q5m +R9FdE19beuvrXwEItUkwegYDVR0fBHMwcTBvoG2ga4ZpaHR0cDovL3d3dy5taWNy +b3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwRUNDJTIwRGV2aWNlcyUy +MFJvb3QlMjBDZXJ0aWZpY2F0ZSUyMEF1dGhvcml0eSUyMDIwMTcuY3JsMIGHBggr +BgEFBQcBAQR7MHkwdwYIKwYBBQUHMAKGa2h0dHA6Ly93d3cubWljcm9zb2Z0LmNv +bS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwRUNDJTIwRGV2aWNlcyUyMFJvb3Ql +MjBDZXJ0aWZpY2F0ZSUyMEF1dGhvcml0eSUyMDIwMTcuY3J0MAoGCCqGSM49BAMD +A2gAMGUCMFYqrXJMuYyzI4D1X/ghlGYPdnfiewPdMF7LkMp45gstEuX3ZzFYcebz +ZMEEs4vp4gIxALkgYbnQXjqkoor+HfwnYQuYFowCnCB/7vPLHwo3YrGOztmanqzm +GtS48agrsbRAmw== +-----END CERTIFICATE----- diff --git a/meson.build b/meson.build index 8008cdd5..a8846ae0 100644 --- a/meson.build +++ b/meson.build @@ -112,7 +112,6 @@ virtual_drivers = [ 'virtual_image', 'virtual_device', 'virtual_device_storage', - 'virtual_sdcp', ] default_drivers = [ 'upektc_img', @@ -209,12 +208,12 @@ driver_helper_mapping = { 'aes2660' : [ 'aeslib', 'aesx660' ], 'aes3500' : [ 'aeslib', 'aes3k' ], 'aes4000' : [ 'aeslib', 'aes3k' ], - 'uru4000' : [ 'openssl' ], + 'egismoc' : [ 'sdcp' ], 'elanspi' : [ 'udev' ], + 'uru4000' : [ 'openssl' ], 'virtual_image' : [ 'virtual' ], 'virtual_device' : [ 'virtual' ], 'virtual_device_storage' : [ 'virtual' ], - 'virtual_sdcp' : [ 'sdcp' ], } driver_helpers = [] diff --git a/tests/egismoc/custom.pcapng b/tests/egismoc/custom.pcapng index fcd8119ed5643faaa19e43c62c678d40ad71abb0..c435cb849311a9b8d8cfd032d7325cb703d7a8b1 100644 GIT binary patch literal 88960 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 G5B~=kmAbtE literal 73752 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 diff --git a/tests/egismoc/device b/tests/egismoc/device index 6bd912a0..28544ba3 100644 --- a/tests/egismoc/device +++ b/tests/egismoc/device @@ -1,17 +1,21 @@ P: /devices/pci0000:00/0000:00:14.0/usb3/3-5 -N: bus/usb/003/012=12010002FF0000407A1C820581110102030109022700010100A0320904000003FF000000070581020002000705020200020007058303400005 +N: bus/usb/003/002=12010002FF0000407A1C820581110102030109022700010100A0320904000003FF000000070581020002000705020200020007058303400005 E: BUSNUM=003 -E: CURRENT_TAGS=:snap_cups_ippeveprinter:snap_cups_cupsd: -E: DEVNAME=/dev/bus/usb/003/012 -E: DEVNUM=012 +E: CURRENT_TAGS=:snap_cups_ippeveprinter:snap_android-platform-tools_fastboot:snap_android-platform-tools_adb:snap_cups_cupsd: +E: DEVNAME=/dev/bus/usb/003/002 +E: DEVNUM=002 E: DEVTYPE=usb_device E: DRIVER=usb +E: ID_AUTOSUSPEND=1 E: ID_BUS=usb E: ID_MODEL=ETU905A80-E E: ID_MODEL_ENC=ETU905A80-E E: ID_MODEL_ID=0582 E: ID_PATH=pci-0000:00:14.0-usb-0:5 E: ID_PATH_TAG=pci-0000_00_14_0-usb-0_5 +E: ID_PATH_WITH_USB_REVISION=pci-0000:00:14.0-usbv2-0:5 +E: ID_PERSIST=0 +E: ID_PROCESSING=1 E: ID_REVISION=1181 E: ID_SERIAL=EGIS_ETU905A80-E_0E7828PBS393 E: ID_SERIAL_SHORT=0E7828PBS393 @@ -30,10 +34,10 @@ E: ID_VENDOR_ENC=EGIS E: ID_VENDOR_FROM_DATABASE=LighTuning Technology Inc. E: ID_VENDOR_ID=1c7a E: MAJOR=189 -E: MINOR=267 +E: MINOR=257 E: PRODUCT=1c7a/582/1181 E: SUBSYSTEM=usb -E: TAGS=:snap_cups_ippeveprinter:snap_cups_cupsd: +E: TAGS=:snap_cups_ippeveprinter:snap_android-platform-tools_fastboot:snap_cups_cupsd:snap_android-platform-tools_adb: E: TYPE=255/0/0 A: authorized=1\n A: avoid_reset_quirk=0\n @@ -50,8 +54,8 @@ A: bmAttributes=a0\n A: busnum=3\n A: configuration= H: descriptors=12010002FF0000407A1C820581110102030109022700010100A0320904000003FF000000070581020002000705020200020007058303400005 -A: dev=189:267\n -A: devnum=12\n +A: dev=189:257\n +A: devnum=2\n A: devpath=5\n L: driver=../../../../../bus/usb/drivers/usb L: firmware_node=../../../../LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:51/device:52/device:57 @@ -66,20 +70,20 @@ A: physical_location/lid=no\n A: physical_location/panel=unknown\n A: physical_location/vertical_position=center\n L: port=../3-0:1.0/usb3-port5 -A: power/active_duration=1425996\n +A: power/active_duration=2329204\n A: power/async=enabled\n A: power/autosuspend=2\n A: power/autosuspend_delay_ms=2000\n -A: power/connected_duration=1426656\n -A: power/control=on\n -A: power/level=on\n -A: power/persist=0\n +A: power/connected_duration=96447632\n +A: power/control=auto\n +A: power/level=auto\n +A: power/persist=1\n A: power/runtime_active_kids=0\n -A: power/runtime_active_time=1426124\n -A: power/runtime_enabled=forbidden\n +A: power/runtime_active_time=2345067\n +A: power/runtime_enabled=enabled\n A: power/runtime_status=active\n -A: power/runtime_suspended_time=0\n -A: power/runtime_usage=1\n +A: power/runtime_suspended_time=94056717\n +A: power/runtime_usage=0\n A: power/wakeup=disabled\n A: power/wakeup_abort_count=\n A: power/wakeup_active=\n @@ -96,13 +100,13 @@ A: rx_lanes=1\n A: serial=0E7828PBS393\n A: speed=480\n A: tx_lanes=1\n -A: urbnum=2803\n +A: urbnum=10257\n A: version= 2.00\n P: /devices/pci0000:00/0000:00:14.0/usb3 -N: bus/usb/003/001=12010002090001406B1D020002060302010109021900010100E0000904000001090000000705810304000C +N: bus/usb/003/001=12010002090001406B1D020015060302010109021900010100E0000904000001090000000705810304000C E: BUSNUM=003 -E: CURRENT_TAGS=:seat:snap_cups_cupsd:snap_cups_ippeveprinter: +E: CURRENT_TAGS=:snap_cups_ippeveprinter:seat:snap_android-platform-tools_fastboot:snap_cups_cupsd:snap_android-platform-tools_adb: E: DEVNAME=/dev/bus/usb/003/001 E: DEVNUM=001 E: DEVTYPE=usb_device @@ -116,28 +120,28 @@ E: ID_MODEL_FROM_DATABASE=2.0 root hub E: ID_MODEL_ID=0002 E: ID_PATH=pci-0000:00:14.0 E: ID_PATH_TAG=pci-0000_00_14_0 -E: ID_REVISION=0602 -E: ID_SERIAL=Linux_6.2.0-34-generic_xhci-hcd_xHCI_Host_Controller_0000:00:14.0 +E: ID_REVISION=0615 +E: ID_SERIAL=Linux_6.15.1_xhci-hcd_xHCI_Host_Controller_0000:00:14.0 E: ID_SERIAL_SHORT=0000:00:14.0 E: ID_USB_INTERFACES=:090000: E: ID_USB_MODEL=xHCI_Host_Controller E: ID_USB_MODEL_ENC=xHCI\x20Host\x20Controller E: ID_USB_MODEL_ID=0002 -E: ID_USB_REVISION=0602 -E: ID_USB_SERIAL=Linux_6.2.0-34-generic_xhci-hcd_xHCI_Host_Controller_0000:00:14.0 +E: ID_USB_REVISION=0615 +E: ID_USB_SERIAL=Linux_6.15.1_xhci-hcd_xHCI_Host_Controller_0000:00:14.0 E: ID_USB_SERIAL_SHORT=0000:00:14.0 -E: ID_USB_VENDOR=Linux_6.2.0-34-generic_xhci-hcd -E: ID_USB_VENDOR_ENC=Linux\x206.2.0-34-generic\x20xhci-hcd +E: ID_USB_VENDOR=Linux_6.15.1_xhci-hcd +E: ID_USB_VENDOR_ENC=Linux\x206.15.1\x20xhci-hcd E: ID_USB_VENDOR_ID=1d6b -E: ID_VENDOR=Linux_6.2.0-34-generic_xhci-hcd -E: ID_VENDOR_ENC=Linux\x206.2.0-34-generic\x20xhci-hcd +E: ID_VENDOR=Linux_6.15.1_xhci-hcd +E: ID_VENDOR_ENC=Linux\x206.15.1\x20xhci-hcd E: ID_VENDOR_FROM_DATABASE=Linux Foundation E: ID_VENDOR_ID=1d6b E: MAJOR=189 E: MINOR=256 -E: PRODUCT=1d6b/2/602 +E: PRODUCT=1d6b/2/615 E: SUBSYSTEM=usb -E: TAGS=:snap_cups_cupsd:seat:snap_cups_ippeveprinter: +E: TAGS=:snap_cups_ippeveprinter:seat:snap_android-platform-tools_fastboot:snap_cups_cupsd:snap_android-platform-tools_adb: E: TYPE=9/0/1 A: authorized=1\n A: authorized_default=1\n @@ -150,11 +154,11 @@ A: bMaxPacketSize0=64\n A: bMaxPower=0mA\n A: bNumConfigurations=1\n A: bNumInterfaces= 1\n -A: bcdDevice=0602\n +A: bcdDevice=0615\n A: bmAttributes=e0\n A: busnum=3\n A: configuration= -H: descriptors=12010002090001406B1D020002060302010109021900010100E0000904000001090000000705810304000C +H: descriptors=12010002090001406B1D020015060302010109021900010100E0000904000001090000000705810304000C A: dev=189:256\n A: devnum=1\n A: devpath=0\n @@ -164,20 +168,20 @@ A: idProduct=0002\n A: idVendor=1d6b\n A: interface_authorized_default=1\n A: ltm_capable=no\n -A: manufacturer=Linux 6.2.0-34-generic xhci-hcd\n +A: manufacturer=Linux 6.15.1 xhci-hcd\n A: maxchild=12\n -A: power/active_duration=337953872\n +A: power/active_duration=2362684\n A: power/async=enabled\n A: power/autosuspend=0\n A: power/autosuspend_delay_ms=0\n -A: power/connected_duration=337978524\n +A: power/connected_duration=96447784\n A: power/control=auto\n A: power/level=auto\n A: power/runtime_active_kids=1\n -A: power/runtime_active_time=337962424\n +A: power/runtime_active_time=2368910\n A: power/runtime_enabled=enabled\n A: power/runtime_status=active\n -A: power/runtime_suspended_time=616\n +A: power/runtime_suspended_time=94033284\n A: power/runtime_usage=0\n A: power/wakeup=disabled\n A: power/wakeup_abort_count=\n @@ -195,12 +199,15 @@ A: rx_lanes=1\n A: serial=0000:00:14.0\n A: speed=480\n A: tx_lanes=1\n -A: urbnum=4969\n +A: urbnum=7078\n A: version= 2.00\n P: /devices/pci0000:00/0000:00:14.0 E: DRIVER=xhci_hcd +E: ID_AUTOSUSPEND=1 E: ID_MODEL_FROM_DATABASE=Alder Lake PCH USB 3.2 xHCI Host Controller +E: ID_PATH=pci-0000:00:14.0 +E: ID_PATH_TAG=pci-0000_00_14_0 E: ID_PCI_CLASS_FROM_DATABASE=Serial bus controller E: ID_PCI_INTERFACE_FROM_DATABASE=XHCI E: ID_PCI_SUBCLASS_FROM_DATABASE=USB controller @@ -214,10 +221,15 @@ E: SUBSYSTEM=pci A: ari_enabled=0\n A: broken_parity_status=0\n A: class=0x0c0330\n -H: config=8680ED51060490020130030C000080000400161D6000000000000000000000000000000000000000000000004D1470C8000000007000000000000000FF010000FD0134A089C27F8000000000000000003F6DD80F000000000000000000000000316000000000000000000000000000000180C2C1080000000000000000000000059087007805E0FE000000000000000009B014F01000400100000000C10A080000080E00001800008F50020000010000090000018680C00009001014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000B50F010112000000 +H: config=8680ED51060490020130030C000080000400161D6000000000000000000000000000000000000000000000004D1470C8000000007000000000000000FF010000FD0134A089C27F8000000000000000003F6DD80F000000000000000000000000316000000000000000000000000000000180C2C10800000000000000000000000590B7001804E0FE000000000000000009B014F01000400100000000C10A080000080E00001800008F50020000010000090000018680C00009001014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000B50F010112000000 A: consistent_dma_mask_bits=64\n A: d3cold_allowed=1\n A: dbc=disabled\n +A: dbc_bInterfaceProtocol=01\n +A: dbc_bcdDevice=0010\n +A: dbc_idProduct=0010\n +A: dbc_idVendor=1d6b\n +A: dbc_poll_interval_ms=64\n A: device=0x51ed\n A: dma_mask_bits=64\n L: driver=../../../bus/pci/drivers/xhci_hcd @@ -227,32 +239,39 @@ L: firmware_node=../../LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:51 A: index=7\n L: iommu=../../virtual/iommu/dmar1 L: iommu_group=../../../kernel/iommu_groups/8 -A: irq=145\n +A: irq=133\n A: label=Onboard - Other\n A: local_cpulist=0-15\n A: local_cpus=ffff\n A: modalias=pci:v00008086d000051EDsv0000144Dsd0000C870bc0Csc03i30\n A: msi_bus=1\n -A: msi_irqs/145=msi\n +A: msi_irqs/133=msi\n +A: msi_irqs/134=msi\n +A: msi_irqs/135=msi\n +A: msi_irqs/136=msi\n +A: msi_irqs/137=msi\n +A: msi_irqs/138=msi\n +A: msi_irqs/139=msi\n +A: msi_irqs/140=msi\n A: numa_node=-1\n -A: pools=poolinfo - 0.1\nbuffer-2048 0 0 2048 0\nbuffer-512 0 0 512 0\nbuffer-128 0 0 128 0\nbuffer-32 0 0 32 0\nxHCI 1KB stream ctx arrays 0 0 1024 0\nxHCI 256 byte stream ctx arrays 0 0 256 0\nxHCI input/output contexts 6 9 2112 9\nxHCI ring segments 26 34 4096 34\nbuffer-2048 0 0 2048 0\nbuffer-512 0 0 512 0\nbuffer-128 0 32 128 1\nbuffer-32 0 0 32 0\n +A: pools=poolinfo - 0.1\nbuffer-2048 0 0 2048 0\nbuffer-512 0 0 512 0\nbuffer-128 0 0 128 0\nbuffer-32 0 0 32 0\nxHCI 1KB stream ctx arrays 0 0 1024 0\nxHCI 256 byte stream ctx arrays 0 0 256 0\nxHCI input/output contexts 6 7 2112 7\nxHCI ring segments 27 27 4096 27\nbuffer-2048 0 0 2048 0\nbuffer-512 0 0 512 0\nbuffer-128 0 0 128 0\nbuffer-32 0 0 32 0\n A: power/async=enabled\n A: power/control=auto\n A: power/runtime_active_kids=1\n -A: power/runtime_active_time=337964621\n +A: power/runtime_active_time=2376480\n A: power/runtime_enabled=enabled\n A: power/runtime_status=active\n -A: power/runtime_suspended_time=438\n +A: power/runtime_suspended_time=94028360\n A: power/runtime_usage=0\n A: power/wakeup=enabled\n A: power/wakeup_abort_count=0\n A: power/wakeup_active=0\n -A: power/wakeup_active_count=7\n +A: power/wakeup_active_count=1\n A: power/wakeup_count=0\n -A: power/wakeup_expire_count=7\n -A: power/wakeup_last_time_ms=336554844\n -A: power/wakeup_max_time_ms=105\n -A: power/wakeup_total_time_ms=721\n +A: power/wakeup_expire_count=1\n +A: power/wakeup_last_time_ms=41666464\n +A: power/wakeup_max_time_ms=101\n +A: power/wakeup_total_time_ms=101\n A: power_state=D0\n A: resource=0x000000601d160000 0x000000601d16ffff 0x0000000000140204\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n A: revision=0x01\n diff --git a/tests/meson.build b/tests/meson.build index dd8da975..3c3af4e8 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -14,9 +14,6 @@ envs.prepend('LD_LIBRARY_PATH', meson.project_build_root() / 'libfprint') # random numbers rather than proper ones) envs.set('FP_DEVICE_EMULATION', '1') -# Path to SDCP virtual device binary, only used for virtual-sdcp test -envs.set('SDCP_VIRT_BINARY', get_option('sdcp_virt_binary')) - # Set a colon-separated list of native drivers we enable in tests envs.set('FP_DRIVERS_ALLOWLIST', ':'.join([ 'virtual_image', @@ -55,9 +52,9 @@ drivers_tests = [ 'nb1010', 'egis0570', 'egismoc', - 'egismoc-05a1', - 'egismoc-0586', - 'egismoc-0587', +# 'egismoc-05a1', # commented out until new capture with SDCP support can be provided +# 'egismoc-0586', # commented out until new capture with SDCP support can be provided +# 'egismoc-0587', # commented out until new capture with SDCP support can be provided 'fpcmoc', 'realtek', 'realtek-5816', @@ -249,13 +246,10 @@ else endforeach endif -test_utils = static_library('fprint-test-utils', - sources: [ - 'test-utils.c', - 'test-device-fake.c', - ], - dependencies: libfprint_private_dep, - install: false) +test_util_sources = [ + 'test-utils.c', + 'test-device-fake.c', +] unit_tests = [ 'fpi-device', @@ -270,6 +264,22 @@ if 'virtual_image' in drivers ] endif +if 'sdcp' in driver_helpers + test_util_sources += [ + 'test-sdcp-device-fake.c', + 'test-sdcp-utils.c', + ] + unit_tests += [ + 'fp-sdcp-device', + 'fpi-sdcp', + ] +endif + +test_utils = static_library('fprint-test-utils', + sources: test_util_sources, + dependencies: libfprint_private_dep, + install: false) + unit_tests_deps = { 'fpi-assembling' : [cairo_dep] } foreach test_name: unit_tests diff --git a/tests/sdcp-key-db/cert9.db b/tests/sdcp-key-db/cert9.db deleted file mode 100644 index 221ac06e5992db71412cc80a171e2bcb5e9ae59c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28672 zcmeI4UuauZ9LLY?O_OD96DgD6AIL!v%-H0ddvDV0L0#HK4c4~nTC{?co8)$6rX5>S z7(U4K!6Bmfus6ZsgY(Z2hM*Q?;EO(r*cTr}d~!v>hxy`Upy&6y_a<%J9(~Eax4Ea^ z^ZT9O_nh;|ZQwR-P8^?YbpyF}`s}*jm9Dr)Se7^{r4VA&oGEk02lW$i$9%VT={w0$ z@#s?@O=SNP$^AQG?9c2sV^_ycjU6-{93TJ!AOHd&00JNY0wC}|5;&Sn=5l#!rM1xv z&NW{0Tg{D5XJND6ZZ%Rfi`B|fRo3Q?RiBrflk;=3mzR(DS~-QsN7-SS9ol|9X!k}W z_c(BF(7`PnOC)m-yrx^Q9MO6Br->+SWfUvCG!@}3xpe3mNHv(?=kT1~mISbMgzxFVmauE^!N+VSNo z%`h`R_vCDCW=U42=NFd_Nne>$o6|9;OPT`|(G)6Ew@lqKcWFr3qoznriJB>D%G4a8 z#;2xknj>tb!j>Hsd1w{RLoQm8jaKBN6&YzoPFj(bR^+7>nQ7IaHdvFBjwI$A$I9uXuiL<3J%g!Zsp5l>ANi!yg_vv#wM@k0f)|yd}bymNRX0N5ATCc29GEwfA$qs(aEl zFSYyV<#xiHR_3NK4;&x>0w4eaAOHd&00JNY0w4eaATWFcUQ1XnCk|vXa&T)0COwfc zUqWOunaRn)G=Ogq^zaz5vME<+?lEBVE00ck)1V8`;KmY_l00ck)1crt{&i2x``)I*`0V}4oUH||9 diff --git a/tests/sdcp-key-db/key4.db b/tests/sdcp-key-db/key4.db deleted file mode 100644 index b27f777bd59e0e5fa1ea125f46a2ffe646b042e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36864 zcmeI5du&rx9LMi%yLRoic8*~kbMX!xvdQDzN4r8Wu$2OXJ-ZQ}vAf;6S=TXGJHTiR z9fn6l0ud7g;~>aj5LrOQp%O_5qW_3-21Ic9h%hlhAw0w&c+R=^dbf;4{13nUl=GIAPAe|*R&jn`d2xZ)H-^K1nKtsulaM<;(d_hQu}pYTZD@IPNdz|r6#lz>W_Oljeb~d}di5@cDKvxFUN;zzL{9X=RL1CG{Y78eM zyyN303wRfarU4S+NytO09#ZwtD(T{pNZ}%dn-uw^@Q`8xDP&RvaWR2fA*G~_hQ-8*vzt2S)1H(PPcaE}y#gX*1-f2=<}A{< zNaG@nOEeA_8t;N zv~x|!B}$@;bWkPfph_~kDoF~eB&$^=S+Xh#QB@MGills0sSl&1)Tlnp-Dem%Bua;T zJ%@ZP2e+OtmRi18YJIWn(^s1qN40#h)F#GKn;1)NVl2t2MsmQCoNuZmC!H#5bs5rS z-9M6^G)mu~o=%XYQ|ppZZ4jYc(O`B?&mQatqy8Nn;B{@pzfls&Zx{*AbX z8*nQ(Ab#(GxP8RMC7rmxk!8F^Tr3ogg%{PM_;%E2w&tyxvFb(&n~;L4QGs4fMv{rU z9Zkv0cw$)DxOpw@mBSXWC7Ux+d(w7n_+;V!<9p4YdkrjWU^!$nCn_!mE6Oxmt5?ln zLq-#uXR@FA<$~?A#S<4)Sx@g;cgBR;(=YDXw(;?+ug69tMl5g4Kf3YFzalTL?d*Ky z&O^b9yx%9-zFz%X&Ec$-$2wa_GCPAL=bg;_#^oPuy0AU;=Qqi1Tg&GhDBtkW;db1_ zGAwSy5B#P(%hDvic-qKXOkLfDy(>m0UlKOm`E|vs^}!?JE5f00e*l z5C8&{fYHcUmFxeTjN>Li0Rlh(2mk>f00e*l5C8%|00;m9AOHl0909X2&5Hk@K)L?E z!Z@xBxfQ~KfB+Bx0zd!=00AHX1b_e#00KY&2t2?9QjDBczcqq#{ol(tdLLjUuyh~* z1b_e#00KY&2mk>f00e*l5C8&0gn-j1S?_jlL300pgZ(|mF&7uG0RbQY1b_e#00KY& z2mk>f00e*l5V(&B#Ip?vlUvi6Xt+Mm5PCkq%ON?Rm+J!RM-uWVz5*@OZ}4wNWdrW_ zzwXRLXWNrawhr&16XP;23)hd_EX>ULY1G^MoR(|1I@a9fe*b;${cqx4KK<_Y$|Xym z?SA;iHF3Y|*x4;xO8mzqkA2QF{P}O=mGK-lAxmmaUo1zX%bJ>N&t=YBm~*ge-?H{? z-<|w%%ctX#W1|CoVU%-k#XdQ+VWD-N9*#OO`c`k2w4vPXFO9 zY4-nEg3(iB_3J<0yug!-WEm&3gT6XqNHS(wwbv#71_Pn10RR91 diff --git a/tests/test-fp-sdcp-device.c b/tests/test-fp-sdcp-device.c new file mode 100644 index 00000000..baa2e323 --- /dev/null +++ b/tests/test-fp-sdcp-device.c @@ -0,0 +1,143 @@ +/* + * FpSdcpDevice Unit tests + * Copyright (C) 2025 Joshua Grisham + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include + +#include "test-sdcp-device-fake.h" +#include "test-sdcp-utils.h" + +static void +test_set_get_print_id (void) +{ + g_autoptr(FpDevice) device = g_object_new (FPI_TYPE_SDCP_DEVICE_FAKE, NULL); + g_autoptr(FpPrint) print = fp_print_new (device); + g_autoptr(GBytes) id1 = NULL; + g_autoptr(GBytes) id2 = NULL; + g_autoptr(GError) error = NULL; + + id1 = fpi_sdcp_generate_random (&error); + + g_assert_nonnull (id1); + g_assert_no_error (error); + + fpi_sdcp_device_set_print_id (print, id1); + fpi_sdcp_device_get_print_id (print, &id2); + + g_assert_nonnull (id2); + g_assert_true (g_bytes_equal (id1, id2)); +} + +static void +test_identify (void) +{ + g_autoptr(FpDevice) device = g_object_new (FPI_TYPE_SDCP_DEVICE_FAKE, NULL); + g_autoptr(GPtrArray) match_prints = g_ptr_array_new_with_free_func (g_object_unref); + g_autoptr(FpPrint) template_print = fp_print_new (device); + g_autoptr(FpPrint) print = NULL; + g_autoptr(FpPrint) matched_print = NULL; + g_autoptr(GError) error = NULL; + + g_assert_true (fp_device_open_sync (device, NULL, &error)); + + print = fp_device_enroll_sync (device, template_print, NULL, NULL, NULL, &error); + g_ptr_array_add (match_prints, g_object_ref_sink (print)); + + g_assert_true (fp_device_identify_sync (device, match_prints, NULL, NULL, NULL, + &matched_print, &print, &error)); + + g_assert_true (fp_device_close_sync (device, NULL, &error)); +} + +static void +test_enroll (void) +{ + g_autoptr(FpDevice) device = g_object_new (FPI_TYPE_SDCP_DEVICE_FAKE, NULL); + g_autoptr(FpPrint) template_print = fp_print_new (device); + g_autoptr(GError) error = NULL; + + g_assert_true (fp_device_open_sync (device, NULL, &error)); + + g_assert_nonnull (fp_device_enroll_sync (device, template_print, NULL, NULL, NULL, &error)); + g_assert_no_error (error); + + g_assert_true (fp_device_close_sync (device, NULL, &error)); +} + +static void +test_list (void) +{ + g_autoptr(FpDevice) device = g_object_new (FPI_TYPE_SDCP_DEVICE_FAKE, NULL); + g_autoptr(GPtrArray) prints = NULL; + g_autoptr(GError) error = NULL; + + g_assert_true (fp_device_open_sync (device, NULL, &error)); + + prints = fp_device_list_prints_sync (device, NULL, &error); + + g_assert_nonnull (prints); + g_assert_true (prints->len == 1); + + /* TODO: Should we also check the print's "fpi-data" for the expected ID? */ + + g_assert_no_error (error); + + g_assert_true (fp_device_close_sync (device, NULL, &error)); +} + +static void +test_reconnect (void) +{ + g_autoptr(FpDevice) device = g_object_new (FPI_TYPE_SDCP_DEVICE_FAKE, NULL); + FpiSdcpDeviceFake *fake_device = FPI_SDCP_DEVICE_FAKE (device); + + g_assert_true (fp_device_open_sync (device, NULL, NULL)); + g_assert (fake_device->reconnect_called == FALSE); + + g_assert_true (fp_device_close_sync (device, NULL, NULL)); + + /* open a second time to reconnect */ + g_assert_true (fp_device_open_sync (device, NULL, NULL)); + g_assert (fake_device->reconnect_called == TRUE); + + g_assert_true (fp_device_close_sync (device, NULL, NULL)); +} + +static void +test_open (void) +{ + g_autoptr(FpDevice) device = g_object_new (FPI_TYPE_SDCP_DEVICE_FAKE, NULL); + + g_assert_true (fp_device_open_sync (device, NULL, NULL)); + g_assert_true (fp_device_close_sync (device, NULL, NULL)); +} + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/sdcp_device/open", test_open); + g_test_add_func ("/sdcp_device/reconnect", test_reconnect); + g_test_add_func ("/sdcp_device/list", test_list); + g_test_add_func ("/sdcp_device/enroll", test_enroll); + g_test_add_func ("/sdcp_device/identify", test_identify); + g_test_add_func ("/sdcp_device/set_get_print_id", test_set_get_print_id); + + return g_test_run (); +} diff --git a/tests/test-fpi-sdcp.c b/tests/test-fpi-sdcp.c new file mode 100644 index 00000000..e7a99d7a --- /dev/null +++ b/tests/test-fpi-sdcp.c @@ -0,0 +1,162 @@ +/* + * Secure Device Connection Protocol (SDCP) support unit tests + * Copyright (C) 2025 Joshua Grisham + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#define FP_COMPONENT "test_fpi_sdcp" +#include "fpi-log.h" + +#include "test-sdcp-utils.h" + +static void +test_generate_enrollment_id (void) +{ + g_autoptr(GBytes) id = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GBytes) application_secret = g_bytes_from_hex (application_secret_hex); + g_autoptr(GBytes) nonce = g_bytes_from_hex (enrollment_nonce_hex); + g_autoptr(GBytes) expected_id = g_bytes_from_hex (enrollment_enrollment_id_hex); + + id = fpi_sdcp_generate_enrollment_id (application_secret, nonce, &error); + + fp_dbg ("id:"); + fp_dbg_hex_dump_gbytes (id); + + fp_dbg ("expected:"); + fp_dbg_hex_dump_gbytes (expected_id); + + g_assert (g_bytes_equal (expected_id, id)); + g_assert_null (error); +} + +static void +test_verify_identify (void) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GBytes) application_secret = g_bytes_from_hex (application_secret_hex); + g_autoptr(GBytes) nonce = g_bytes_from_hex (identify_nonce_hex); + g_autoptr(GBytes) id = g_bytes_from_hex (identify_enrollment_id_hex); + g_autoptr(GBytes) mac = g_bytes_from_hex (identify_mac_hex); + + g_assert_true (fpi_sdcp_verify_identify (application_secret, nonce, id, mac, &error)); + g_assert_null (error); +} + +static void +test_verify_reconnect (void) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GBytes) application_secret = g_bytes_from_hex (application_secret_hex); + g_autoptr(GBytes) random = g_bytes_from_hex (reconnect_random_hex); + g_autoptr(GBytes) mac = g_bytes_from_hex (reconnect_mac_hex); + + g_assert_true (fpi_sdcp_verify_reconnect (application_secret, random, mac, &error)); + g_assert_null (error); +} + +static void +test_verify_connect (void) +{ + g_autoptr(GBytes) application_secret = NULL; + g_autoptr(GError) error = NULL; + + g_autoptr(GBytes) host_private_key = g_bytes_from_hex (host_private_key_hex); + g_autoptr(GBytes) host_random = g_bytes_from_hex (host_random_hex); + g_autoptr(GBytes) device_random = g_bytes_from_hex (device_random_hex); + g_autoptr(GBytes) connect_mac = g_bytes_from_hex (connect_mac_hex); + FpiSdcpClaim *claim = sdcp_test_claim (); + + g_autoptr(GBytes) expected_application_secret = g_bytes_from_hex (application_secret_hex); + + g_assert_true (fpi_sdcp_verify_connect (host_private_key, + host_random, + device_random, + claim, + connect_mac, + TRUE, + TRUE, + &application_secret, + &error)); + + g_assert_null (error); + g_assert (g_bytes_get_size (application_secret) == SDCP_APPLICATION_SECRET_SIZE); + + fp_dbg ("application_secret:"); + fp_dbg_hex_dump_gbytes (application_secret); + + fp_dbg ("expected:"); + fp_dbg_hex_dump_gbytes (expected_application_secret); + + g_assert_true (g_bytes_equal (expected_application_secret, application_secret)); + + fpi_sdcp_claim_free (claim); +} + +static void +test_generate_random (void) +{ + g_autoptr(GBytes) random = NULL; + g_autoptr(GError) error = NULL; + + random = fpi_sdcp_generate_random (&error); + + g_assert_null (error); + g_assert (g_bytes_get_size (random) == SDCP_RANDOM_SIZE); + + fp_dbg ("random:"); + fp_dbg_hex_dump_gbytes (random); +} + +static void +test_generate_host_key (void) +{ + g_autoptr(GBytes) private_key = NULL; + g_autoptr(GBytes) public_key = NULL; + g_autoptr(GError) error = NULL; + gsize len = 0; + + fpi_sdcp_generate_host_key (&private_key, &public_key, &error); + + g_assert_null (error); + + g_bytes_get_data (private_key, &len); + g_assert (len == 32); + + fp_dbg ("private_key:"); + fp_dbg_hex_dump_gbytes (private_key); + + g_bytes_get_data (public_key, &len); + g_assert (len == SDCP_PUBLIC_KEY_SIZE); + + fp_dbg ("public_key:"); + fp_dbg_hex_dump_gbytes (public_key); +} + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/sdcp/generate_host_key", test_generate_host_key); + g_test_add_func ("/sdcp/generate_random", test_generate_random); + g_test_add_func ("/sdcp/verify_connect", test_verify_connect); + g_test_add_func ("/sdcp/verify_reconnect", test_verify_reconnect); + g_test_add_func ("/sdcp/verify_identify", test_verify_identify); + g_test_add_func ("/sdcp/generate_enrollment_id", test_generate_enrollment_id); + + return g_test_run (); +} diff --git a/tests/test-sdcp-device-fake.c b/tests/test-sdcp-device-fake.c new file mode 100644 index 00000000..408650aa --- /dev/null +++ b/tests/test-sdcp-device-fake.c @@ -0,0 +1,196 @@ +/* + * Virtual driver for SDCP unit testing + * Completes actions using example values from Microsoft's SDCP documentation + * Copyright (C) 2025 Joshua Grisham + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#define FP_COMPONENT "fake_test_sdcp_dev" +#include "fpi-log.h" + +#include "test-sdcp-device-fake.h" + +G_DEFINE_TYPE (FpiSdcpDeviceFake, fpi_sdcp_device_fake, FP_TYPE_SDCP_DEVICE) + +static const FpIdEntry driver_ids[] = { + { .virtual_envvar = "FP_VIRTUAL_FAKE_SDCP_DEVICE" }, + { .virtual_envvar = NULL } +}; + +static void +fpi_sdcp_device_fake_identify (FpSdcpDevice *sdcp_device) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GBytes) nonce = NULL; + g_autoptr(GBytes) identify_enrollment_id = g_bytes_from_hex (identify_enrollment_id_hex); + g_autoptr(GBytes) identify_mac = g_bytes_from_hex (identify_mac_hex); + + /* + * Normally, a driver would fetch the identify data and then send it to the + * device's Identify command. In this fake device, we will just fetch and + * verify the nonce was generated but do nothing with it + */ + + fpi_sdcp_device_get_identify_data (sdcp_device, &nonce); + + g_assert (nonce); + g_assert (g_bytes_get_size (nonce) == SDCP_NONCE_SIZE); + + /* + * In emulation mode (FP_DEVICE_EMULATION=1), a different hard-coded nonce is + * set in fpi-sdcp-device, which was the same nonce used to generate both the + * identify_enrollment_id and identify_mac values provided here + */ + + fpi_sdcp_device_identify_complete (sdcp_device, identify_enrollment_id, identify_mac, error); +} + +static void +fpi_sdcp_device_fake_enroll_commit (FpSdcpDevice *sdcp_device, GBytes *id) +{ + fpi_sdcp_device_enroll_commit_complete (sdcp_device, NULL); +} + +static void +fpi_sdcp_device_fake_enroll (FpSdcpDevice *sdcp_device) +{ + g_autoptr(GError) error = NULL; + GBytes *enrollment_nonce = g_bytes_from_hex (enrollment_nonce_hex); + + fpi_sdcp_device_enroll_commit (sdcp_device, enrollment_nonce, error); + + g_bytes_unref (enrollment_nonce); +} + +static void +fpi_sdcp_device_fake_list (FpSdcpDevice *sdcp_device) +{ + g_autoptr(GBytes) identify_enrollment_id = g_bytes_from_hex (identify_enrollment_id_hex); + GPtrArray *enrollment_ids = g_ptr_array_new_with_free_func ((GDestroyNotify) g_bytes_unref); + + g_ptr_array_add (enrollment_ids, g_steal_pointer (&identify_enrollment_id)); + + for (gint i = 0; i < enrollment_ids->len; i++) + { + fp_dbg ("print %d:", i); + fp_dbg_hex_dump_gbytes (g_ptr_array_index (enrollment_ids, i)); + } + + fpi_sdcp_device_list_complete (sdcp_device, g_steal_pointer (&enrollment_ids), NULL); +} + +static void +fpi_sdcp_device_fake_reconnect (FpSdcpDevice *sdcp_device) +{ + FpiSdcpDeviceFake *fake_device = FPI_SDCP_DEVICE_FAKE (sdcp_device); + g_autoptr(GError) error = NULL; + g_autoptr(GBytes) random = NULL; + g_autoptr(GBytes) reconnect_mac = g_bytes_from_hex (reconnect_mac_hex); + + /* + * Normally, a driver would fetch the reconnect data and then send it to the + * device's Reconnect command. In this fake device, we will just fetch and + * verify the random was generated but do nothing with it + */ + + fpi_sdcp_device_get_reconnect_data (sdcp_device, &random); + + g_assert (random); + g_assert (g_bytes_get_size (random) == SDCP_RANDOM_SIZE); + + /* + * In emulation mode (FP_DEVICE_EMULATION=1), a different hard-coded random is + * set in fpi-sdcp-device, which was the same random used to generate the + * reconnect_mac value provided here + */ + + fpi_sdcp_device_reconnect_complete (sdcp_device, reconnect_mac, error); + + fake_device->reconnect_called = TRUE; +} + +static void +fpi_sdcp_device_fake_connect (FpSdcpDevice *sdcp_device) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GBytes) host_random = NULL; + g_autoptr(GBytes) host_public_key = NULL; + g_autoptr(GBytes) device_random = g_bytes_from_hex (device_random_hex); + g_autoptr(GBytes) connect_mac = g_bytes_from_hex (connect_mac_hex); + FpiSdcpClaim *claim = sdcp_test_claim (); + + fp_device_open_sync (FP_DEVICE (sdcp_device), NULL, NULL); + + fpi_sdcp_device_get_connect_data (sdcp_device, &host_random, &host_public_key); + + g_assert (host_random); + g_assert (g_bytes_get_size (host_random) == SDCP_RANDOM_SIZE); + + g_assert (host_public_key); + g_assert (g_bytes_get_size (host_public_key) == SDCP_PUBLIC_KEY_SIZE); + + fpi_sdcp_device_connect_complete (sdcp_device, device_random, claim, connect_mac, error); + + fpi_sdcp_claim_free (claim); +} + +static void +fpi_sdcp_device_fake_close (FpDevice *device) +{ + fpi_device_close_complete (device, NULL); +} + +static void +fpi_sdcp_device_fake_open (FpSdcpDevice *sdcp_device) +{ + fpi_sdcp_device_open_complete (sdcp_device, NULL); +} + +static void +fpi_sdcp_device_fake_init (FpiSdcpDeviceFake *self) +{ + G_DEBUG_HERE (); +} + +static void +fpi_sdcp_device_fake_class_init (FpiSdcpDeviceFakeClass *klass) +{ + FpDeviceClass *dev_class = FP_DEVICE_CLASS (klass); + FpSdcpDeviceClass *sdcp_dev_class = FP_SDCP_DEVICE_CLASS (klass); + + dev_class->id = FP_COMPONENT; + dev_class->full_name = "Virtual SDCP test device"; + dev_class->type = FP_DEVICE_TYPE_VIRTUAL; + dev_class->id_table = driver_ids; + dev_class->nr_enroll_stages = 5; + dev_class->scan_type = FP_SCAN_TYPE_PRESS; + + sdcp_dev_class->ignore_device_certificate = FALSE; + sdcp_dev_class->ignore_device_signatures = FALSE; + + sdcp_dev_class->open = fpi_sdcp_device_fake_open; + sdcp_dev_class->connect = fpi_sdcp_device_fake_connect; + sdcp_dev_class->reconnect = fpi_sdcp_device_fake_reconnect; + sdcp_dev_class->list = fpi_sdcp_device_fake_list; + sdcp_dev_class->enroll = fpi_sdcp_device_fake_enroll; + sdcp_dev_class->enroll_commit = fpi_sdcp_device_fake_enroll_commit; + sdcp_dev_class->identify = fpi_sdcp_device_fake_identify; + + dev_class->close = fpi_sdcp_device_fake_close; + + fpi_device_class_auto_initialize_features (dev_class); + dev_class->features |= FP_DEVICE_FEATURE_STORAGE; +} diff --git a/tests/test-sdcp-device-fake.h b/tests/test-sdcp-device-fake.h new file mode 100644 index 00000000..5ad8f0eb --- /dev/null +++ b/tests/test-sdcp-device-fake.h @@ -0,0 +1,33 @@ +/* + * Virtual driver for SDCP unit testing + * Copyright (C) 2025 Joshua Grisham + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include "fpi-sdcp-device.h" + +#include "test-sdcp-utils.h" + +#define FPI_TYPE_SDCP_DEVICE_FAKE (fpi_sdcp_device_fake_get_type ()) +G_DECLARE_FINAL_TYPE (FpiSdcpDeviceFake, fpi_sdcp_device_fake, FPI, SDCP_DEVICE_FAKE, FpSdcpDevice) + +struct _FpiSdcpDeviceFake +{ + FpDevice parent; + gboolean reconnect_called; +}; diff --git a/tests/test-sdcp-utils.c b/tests/test-sdcp-utils.c new file mode 100644 index 00000000..5b2682c0 --- /dev/null +++ b/tests/test-sdcp-utils.c @@ -0,0 +1,60 @@ +/* + * Secure Device Connection Protocol (SDCP) support test utils + * Copyright (C) 2025 Joshua Grisham + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "test-sdcp-utils.h" + +static const guint8 from_hex_map[] = + { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // 01234567 + 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 89:;<=>? + 0x00, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, // @abcdef + }; + +GBytes * +g_bytes_from_hex (const gchar *hex) +{ + g_autoptr(GBytes) res = NULL; + guint8 b0, b1; + gsize bytes_len = strlen (hex) / 2; + guint8 *bytes = g_malloc0 (bytes_len); + + for (int i = 0; i < strlen (hex) - 1; i += 2) + { + b0 = ((guint8)hex[i+0] & 0x1F) ^ 0x10; + b1 = ((guint8)hex[i+1] & 0x1F) ^ 0x10; + bytes[i/2] = (guint8)(from_hex_map[b0] << 4) | from_hex_map[b1]; + } + + res = g_bytes_new_take (bytes, bytes_len); + + return g_steal_pointer (&res); +} + +FpiSdcpClaim * +sdcp_test_claim (void) +{ + FpiSdcpClaim *claim = g_new0 (FpiSdcpClaim, 1); + claim->model_certificate = g_bytes_from_hex (model_certificate_hex); + claim->device_public_key = g_bytes_from_hex (device_public_key_hex); + claim->firmware_public_key = g_bytes_from_hex (firmware_public_key_hex); + claim->firmware_hash = g_bytes_from_hex (firmware_hash_hex); + claim->model_signature = g_bytes_from_hex (model_signature_hex); + claim->device_signature = g_bytes_from_hex (device_signature_hex); + return g_steal_pointer (&claim); +} diff --git a/tests/test-sdcp-utils.h b/tests/test-sdcp-utils.h new file mode 100644 index 00000000..f9b2a3c9 --- /dev/null +++ b/tests/test-sdcp-utils.h @@ -0,0 +1,102 @@ +/* + * Secure Device Connection Protocol (SDCP) support test utils + * Copyright (C) 2025 Joshua Grisham + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include "fpi-log.h" +#include "fpi-sdcp.h" +#include "fpi-sdcp-device.h" + +/******************************************************************************/ + +/* host keys */ + +static const gchar host_private_key_hex[] = "8400ed14579cdf11586477e836e8cb52708441c1c2a447c218c5bbc2d118fbc7"; +static const gchar host_public_key_hex[] = "0452f056ffb9c6728654771a3629b770767b19a2106a4916fb81ba06ef6797c4" + "a3df672ade0e9116d1abe278a8223abde4958d62d4ff6882159f0617c6f8ce10" + "bf"; +static const gchar host_random_hex[] = "d877403abe82f4d97e1448c5052d83a532a45e56ef049cbbf981137520e713bf"; + +/* device keys */ + +static const gchar device_random_hex[] = "6e2f6c1abef2a1973fb1315a17e209fdb0c78520f1fd6a85d294d7aeb40a04a7"; +static const gchar model_certificate_hex[] = "30820323308202caa00302010202133300000004c45d661d6eed040d00000000" + "0004300a06082a8648ce3d0403023056310b3009060355040613025553311e30" + "1c060355040a13154d6963726f736f667420436f72706f726174696f6e312730" + "250603550403131e57696e646f77732048656c6c6f2032303936414443432043" + "412032303231301e170d3232303130343231303033355a170d32333034303432" + "31303033355a301c311a3018060355040313115365637572652042494f205365" + "6e736f723059301306072a8648ce3d020106082a8648ce3d0301070342000414" + "cfc287f872a2b7d3339e0b31390e3ca688e61165eaa6687c959270e07666b1fa" + "19e3efaf1750d134a886d494424fe471970c4b06838408a18d1f5d57735dd7a3" + "8201af308201ab30750603551d11046e306ca46a30683132303006082b060104" + "82376402132436423045413344382d383339372d343444332d383043392d3930" + "324130414346344343303132303006082b060104823764011324413944423730" + "32362d433646462d344341462d423134382d393133454641333730423933301d" + "0603551d0e04160414db6d66d642b7236d5e6bb2ec2186decda98067b6301f06" + "03551d23041830168014bf3748e34a632de953a3ba890298c069472a99b9305f" + "0603551d1f045830563054a052a050864e687474703a2f2f7777772e6d696372" + "6f736f66742e636f6d2f706b696f70732f63726c2f57696e646f777325323048" + "656c6c6f25323032303936414443432532304341253230323032312e63726c30" + "6c06082b060105050701010460305e305c06082b060105050730028650687474" + "703a2f2f7777772e6d6963726f736f66742e636f6d2f706b696f70732f636572" + "74732f57696e646f777325323048656c6c6f2532303230393641444343253230" + "4341253230323032312e637274300c0603551d130101ff040230003015060355" + "1d25040e300c060a2b0601040182374c2b01300a06082a8648ce3d0403020347" + "003044022077553ef520f732e03cd740c8cf807e6366e12918bc581f75bfe0f1" + "95b7b1fd4f0220324e25b93b9da7538b797a624272b21b7cc0e96ea487924250" + "4677600450f283"; +static const gchar device_public_key_hex[] = "04e2787890a684f95b96b9a2316ca8d3d33d4d79ff4c89dc6f9e888e973990d1" + "d3154133dcc8bd33b99af9dbf0673390d404d092498a3f214cd93f9b9f28fb5f" + "66"; +static const gchar firmware_public_key_hex[] = "04f06a84ab51a3a6e8ff46868f91dd720e4cdad21f2e090d11e8f9bfc2ea19ee" + "1b5eac850b4532968a9399f76cd779e7723e8c2ca73b597c0df5f73b94a36f2b" + "6c"; +static const gchar firmware_hash_hex[] = "c3bf47ea1f4a4a605470313cacb3a44f4a461f68c6faeab07e737610cb5ac835"; +static const gchar model_signature_hex[] = "febe6ba3107813e185f05189e69ae79d9f7a40802582d94324459844c8b97ec6" + "c5daed5462276cb8a193c33e350424b0305d63d79a93a9188dcfc0cb5595f6c1"; +static const gchar device_signature_hex[] = "10cc57dd8dafb463510a7327a5fca49b698e999b36448e2023eaf0dff0b0d4a3" + "4f1caf4e872b77364a0a00d7476554d0324c4cc931937e232a0315837d696c06"; +static const gchar connect_mac_hex[] = "422bc475a78f972bae842a28e5ad721207457fcbd9a1a3aaf71587c07b84d247"; + +/* expected application_secret based on above */ + +static const gchar application_secret_hex[] = "13330ba3135ecf5dc71cede01a886540771efab35c8ba053902b2c1ee7de6efe"; + +/* test verify_reconnect values */ + +static const gchar reconnect_random_hex[] = "8a7451c1d3a8dca1c1330ca50d73454b351a49f46c8e9dcee15c964d295c31c9"; +static const gchar reconnect_mac_hex[] = "bf3f3bb3bd6ecb2784c160f526f7bc3b3ca8faf5557194c48e0024a0493903c7"; + +/* test verify_identify values */ + +static const gchar identify_nonce_hex[] = "3a1b506f5bec089059acefb9b44dfbdea7a599ee9aa267e5252664d60b798053"; +static const gchar identify_enrollment_id_hex[] = "ef2055244e49c39beabdac49fdf4ee418605d195da23b202ba219a13831ae621"; +static const gchar identify_mac_hex[] = "f0a5c5f261c2fe937d8b113857bc629cd07ca88edf991f69ca6fae5c332390f6"; + +/* test enrollment_id values */ + +static const gchar enrollment_nonce_hex[] = "c2101c44c9a667bba397e81f48b143398603e2c9335a68b409e1dbe71e005ca2"; +static const gchar enrollment_enrollment_id_hex[] = "67109dc70a216331f1580ddac601915929c1ff6c9bcba6544ba572c660c3d91e"; + +/******************************************************************************/ + +GBytes *g_bytes_from_hex (const gchar *hex); + +FpiSdcpClaim *sdcp_test_claim (void); diff --git a/tests/virtual-sdcp.py b/tests/virtual-sdcp.py deleted file mode 100644 index 6efb24d7..00000000 --- a/tests/virtual-sdcp.py +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env python3 - -import sys -try: - import gi - import os - - from gi.repository import GLib, Gio - - import unittest - import subprocess - import shutil - import tempfile -except Exception as e: - print("Missing dependencies: %s" % str(e)) - sys.exit(77) - -FPrint = None - -# Re-run the test with the passed wrapper if set -wrapper = os.getenv('LIBFPRINT_TEST_WRAPPER') -if wrapper: - wrap_cmd = wrapper.split(' ') + [sys.executable, os.path.abspath(__file__)] + \ - sys.argv[1:] - os.unsetenv('LIBFPRINT_TEST_WRAPPER') - sys.exit(subprocess.check_call(wrap_cmd)) - -# Only permit loading virtual_sdcp driver for tests in this file -os.environ['FP_DRIVERS_WHITELIST'] = 'virtual_sdcp' - -if hasattr(os.environ, 'MESON_SOURCE_ROOT'): - root = os.environ['MESON_SOURCE_ROOT'] -else: - root = os.path.join(os.path.dirname(__file__), '..') - -ctx = GLib.main_context_default() - -class VirtualSDCP(unittest.TestCase): - - @classmethod - def setUpClass(cls): - os.environ['FP_VIRTUAL_SDCP'] = os.environ['SDCP_VIRT_BINARY'] - - cls.ctx = FPrint.Context() - - cls.dev = None - for dev in cls.ctx.get_devices(): - cls.dev = dev - break - - assert cls.dev is not None, "You need to compile with virtual_sdcp for testing" - - @classmethod - def tearDownClass(cls): - del cls.dev - del cls.ctx - - def setUp(self): - pass - - def tearDown(self): - pass - - def enroll(self, progress_cb=None): - # Enroll another print - template = FPrint.Print.new(self.dev) - template.props.finger = FPrint.Finger.LEFT_THUMB - template.props.username = "testuser" - template.props.description = "test print" - datetime = GLib.DateTime.new_now_local() - date = GLib.Date() - date.set_dmy(*datetime.get_ymd()[::-1]) - template.props.enroll_date = date - return self.dev.enroll_sync(template, None, progress_cb, None) - - def test_connect(self): - self.dev.open_sync() - self.dev.close_sync() - - def test_reconnect(self): - # Ensure device was opened once before, this may be a reconnect if - # it is the same process as another test. - self.dev.open_sync() - self.dev.close_sync() - - # Check that a reconnect happens on next open. To know about this, we - # need to parse check log messages for that. - success = [False] - def log_func(domain, level, msg): - print("log: '%s', '%s', '%s'" % (str(domain), str(level), msg)) - if msg == 'Reconnect succeeded': - success[0] = True - - # Call default handler - GLib.log_default_handler(domain, level, msg) - - handler_id = GLib.log_set_handler('libfprint-sdcp_device', GLib.LogLevelFlags.LEVEL_DEBUG, log_func) - self.dev.open_sync() - self.dev.close_sync() - GLib.log_remove_handler('libfprint-sdcp_device', handler_id) - assert success[0] - - def test_enroll(self): - self.dev.open_sync() - - # Must return a print - assert isinstance(self.enroll(), FPrint.Print) - - self.dev.close_sync() - - - def test_verify(self): - self.dev.open_sync() - - # Enroll a new print (will be the last), check that it verifies - p = self.enroll() - match, dev_print = self.dev.verify_sync(p) - assert match - - # The returned "device" print is identical - assert p.equal(dev_print) - - # We can do the same with it - match, dev_print2 = self.dev.verify_sync(dev_print) - assert match - - # Now, enroll a new print, causing the old one to not match anymore - # (the device always claims to see the last enrolled print). - self.enroll() - match, dev_print = self.dev.verify_sync(p) - assert match is False - - self.dev.close_sync() - -if __name__ == '__main__': - try: - gi.require_version('FPrint', '2.0') - from gi.repository import FPrint - except Exception as e: - print("Missing dependencies: %s" % str(e)) - sys.exit(77) - - # avoid writing to stderr - unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2))