From 6086c9fb27df308d135cbbb0416974d8f015f808 Mon Sep 17 00:00:00 2001 From: Joshua Grisham Date: Tue, 9 Sep 2025 21:30:38 +0200 Subject: [PATCH] sdcp: Refactor test-sdcp-device-fake to virtual-sdcp to support testing with fprintd --- libfprint/drivers/virtual-sdcp.c | 331 ++++++++++++++++++ .../drivers/virtual-sdcp.h | 26 +- libfprint/meson.build | 2 + meson.build | 2 + tests/meson.build | 7 +- tests/test-fp-sdcp-device.c | 143 -------- tests/test-fpi-sdcp.c | 56 ++- tests/test-sdcp-device-fake.c | 197 ----------- tests/test-sdcp-device-fake.h | 33 -- tests/test-sdcp-utils.c | 60 ---- tests/virtual-sdcp.py | 169 +++++++++ 11 files changed, 566 insertions(+), 460 deletions(-) create mode 100644 libfprint/drivers/virtual-sdcp.c rename tests/test-sdcp-utils.h => libfprint/drivers/virtual-sdcp.h (86%) delete mode 100644 tests/test-fp-sdcp-device.c delete mode 100644 tests/test-sdcp-device-fake.c delete mode 100644 tests/test-sdcp-device-fake.h delete mode 100644 tests/test-sdcp-utils.c create mode 100644 tests/virtual-sdcp.py diff --git a/libfprint/drivers/virtual-sdcp.c b/libfprint/drivers/virtual-sdcp.c new file mode 100644 index 00000000..83c37a67 --- /dev/null +++ b/libfprint/drivers/virtual-sdcp.c @@ -0,0 +1,331 @@ +/* + * Virtual driver for SDCP device debugging + * + * 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 + */ + +/* + * This is a virtual driver to debug the SDCP-based drivers. + * This virtual driver does not use a socket listener but instead + * embeds the simulated logic of the fake "device" directly within + * the logic of each function. This driver also allows prints to be + * registered programmatically, making it possible to test libfprint + * and fprintd. + * + * This virtual driver will override FpSdcpDevice's dynamically + * generated cryptography values and instead replace them with + * pre-generated values taken from from Microsoft's sample client + * implementation. See: + * https://github.com/Microsoft/SecureDeviceConnectionProtocol + */ + +#define FP_COMPONENT "virtual_sdcp" +#include "fpi-log.h" + +#include "../fpi-sdcp.h" +#include "virtual-sdcp.h" + +struct _FpDeviceVirtualSdcp +{ + FpSdcpDevice parent; + + GPtrArray *print_ids; +}; + +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) + +/******************************************************************************/ + +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 +}; + +static 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); +} + +static FpiSdcpClaim * +get_fake_sdcp_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); +} + +/******************************************************************************/ + +static void +dev_identify (FpSdcpDevice *sdcp_device) +{ + fp_dbg ("Virtual SDCP device: %s()", G_STRFUNC); + FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (sdcp_device); + g_autoptr(GBytes) enrollment_id = NULL; + g_autoptr(GBytes) identify_mac = NULL; + GBytes *identify_nonce = NULL; + + if (self->print_ids->len > 0) + { + /* + * Pretend that the virtual device identified the first print. + * Since we used a pre-generated enrollment_id for it, we can also use the + * matching pre-generated test identify data for its identification. + */ + identify_nonce = g_bytes_from_hex (identify_nonce_hex); + enrollment_id = g_bytes_from_hex (enrollment_id_hex); + fpi_sdcp_device_set_identify_data (sdcp_device, identify_nonce); + identify_mac = g_bytes_from_hex (identify_mac_hex); + + fpi_sdcp_device_identify_complete (sdcp_device, enrollment_id, identify_mac, NULL); + } + else + { + fpi_sdcp_device_identify_complete (sdcp_device, NULL, NULL, + fpi_device_error_new (FP_DEVICE_ERROR_DATA_NOT_FOUND)); + } +} + +static void +dev_enroll_commit (FpSdcpDevice *sdcp_device, GBytes *id) +{ + fp_dbg ("Virtual SDCP device: %s()", G_STRFUNC); + FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (sdcp_device); + g_autoptr(GBytes) expected_first_print_id = NULL; + GBytes *print_id; + + print_id = g_bytes_new (g_bytes_get_data (id, NULL), g_bytes_get_size (id)); + + /* + * If this is the first print, it is probably good if we make sure the + * internal API assigned it the expected pre-known ID + */ + if (self->print_ids->len == 0) + { + expected_first_print_id = g_bytes_from_hex (enrollment_id_hex); + if (!g_bytes_equal (print_id, expected_first_print_id)) + { + fpi_sdcp_device_enroll_commit_complete (sdcp_device, + fpi_device_error_new_msg (FP_DEVICE_ERROR_UNTRUSTED, + "First enrolled print ID does not match expected value")); + return; + } + } + + g_ptr_array_add (self->print_ids, g_steal_pointer (&print_id)); + + fpi_sdcp_device_enroll_commit_complete (sdcp_device, NULL); +} + +static void +dev_enroll (FpSdcpDevice *sdcp_device) +{ + fp_dbg ("Virtual SDCP device: %s()", G_STRFUNC); + FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (sdcp_device); + FpDevice *device = FP_DEVICE (sdcp_device); + g_autoptr(GError) error = NULL; + g_autoptr(GBytes) nonce = NULL; + FpPrint *print; + + fpi_device_get_enroll_data (device, &print); + + if (self->print_ids->len == 0) + { + /* Use the hard-coded nonce for the first enrollment */ + nonce = g_bytes_from_hex (enrollment_nonce_hex); + } + else + { + /* Generate a new nonce for all other enrollments */ + nonce = fpi_sdcp_generate_random (&error); + if (error) + fpi_device_enroll_progress (device, 0, print, error); + } + + fpi_device_enroll_progress (device, 1, print, NULL); + fpi_sdcp_device_enroll_commit (sdcp_device, nonce, error); +} + +static void +dev_list (FpSdcpDevice *sdcp_device) +{ + fp_dbg ("Virtual SDCP device: %s()", G_STRFUNC); + FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (sdcp_device); + GPtrArray *print_ids = g_ptr_array_new_with_free_func ((GDestroyNotify) g_bytes_unref); + + for (gint i = 0; i < self->print_ids->len; i++) + { + GBytes *print_id = g_ptr_array_index (self->print_ids, i); + fp_dbg ("print %d:", i); + fp_dbg_hex_dump_gbytes (print_id); + g_ptr_array_add (print_ids, g_bytes_new (g_bytes_get_data (print_id, NULL), + g_bytes_get_size (print_id))); + } + + fpi_sdcp_device_list_complete (sdcp_device, g_steal_pointer (&print_ids), NULL); +} + +static void +dev_reconnect (FpSdcpDevice *sdcp_device) +{ + fp_dbg ("Virtual SDCP device: %s()", G_STRFUNC); + 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); +} + +static void +dev_connect (FpSdcpDevice *sdcp_device) +{ + fp_dbg ("Virtual SDCP device: %s()", G_STRFUNC); + 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); + g_autoptr(FpiSdcpClaim) claim = get_fake_sdcp_claim (); + + 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); +} + +static void +dev_close (FpDevice *device) +{ + fp_dbg ("Virtual SDCP device: %s()", G_STRFUNC); + fpi_device_close_complete (device, NULL); +} + +static void +dev_open (FpSdcpDevice *sdcp_device) +{ + fp_dbg ("Virtual SDCP device: %s()", G_STRFUNC); + fpi_sdcp_device_open_complete (sdcp_device, NULL); +} + +static void +fpi_device_virtual_sdcp_finalize (GObject *object) +{ + fp_dbg ("Virtual SDCP device: %s()", G_STRFUNC); + FpDeviceVirtualSdcp *self = FPI_DEVICE_VIRTUAL_SDCP (object); + + g_clear_pointer (&self->print_ids, g_ptr_array_unref); + + G_OBJECT_CLASS (fpi_device_virtual_sdcp_parent_class)->finalize (object); +} + +static void +fpi_device_virtual_sdcp_init (FpDeviceVirtualSdcp *self) +{ + fp_dbg ("Virtual SDCP device: %s()", G_STRFUNC); + + /* Force FP_DEVICE_EMULATION=1 when using FpDeviceVirtualSdcp */ + g_setenv ("FP_DEVICE_EMULATION", "1", TRUE); + + self->print_ids = g_ptr_array_new_with_free_func ((GDestroyNotify) g_bytes_unref); +} + +static const FpIdEntry driver_ids[] = { + { .virtual_envvar = "FP_VIRTUAL_SDCP" }, + { .virtual_envvar = NULL } +}; + +static void +fpi_device_virtual_sdcp_class_init (FpDeviceVirtualSdcpClass *klass) +{ + fp_dbg ("Virtual SDCP device: %s()", G_STRFUNC); + FpDeviceClass *dev_class = FP_DEVICE_CLASS (klass); + FpSdcpDeviceClass *sdcp_dev_class = FP_SDCP_DEVICE_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = fpi_device_virtual_sdcp_finalize; + + dev_class->id = FP_COMPONENT; + dev_class->full_name = "Virtual SDCP device for debugging"; + dev_class->type = FP_DEVICE_TYPE_VIRTUAL; + dev_class->id_table = driver_ids; + dev_class->nr_enroll_stages = 1; + 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 = dev_open; + sdcp_dev_class->connect = dev_connect; + + if (!g_getenv ("FP_VIRTUAL_SDCP_NO_RECONNECT")) + sdcp_dev_class->reconnect = dev_reconnect; + + sdcp_dev_class->list = dev_list; + sdcp_dev_class->enroll = dev_enroll; + sdcp_dev_class->enroll_commit = dev_enroll_commit; + sdcp_dev_class->identify = dev_identify; + + dev_class->close = dev_close; + + fpi_device_class_auto_initialize_features (dev_class); + dev_class->features |= FP_DEVICE_FEATURE_STORAGE; +} diff --git a/tests/test-sdcp-utils.h b/libfprint/drivers/virtual-sdcp.h similarity index 86% rename from tests/test-sdcp-utils.h rename to libfprint/drivers/virtual-sdcp.h index f9b2a3c9..9ed8f125 100644 --- a/tests/test-sdcp-utils.h +++ b/libfprint/drivers/virtual-sdcp.h @@ -1,5 +1,6 @@ /* - * Secure Device Connection Protocol (SDCP) support test utils + * Virtual driver test payloads for SDCP device debugging + * * Copyright (C) 2025 Joshua Grisham * * This library is free software; you can redistribute it and/or @@ -19,11 +20,7 @@ #pragma once -#include "fpi-log.h" -#include "fpi-sdcp.h" -#include "fpi-sdcp-device.h" - -/******************************************************************************/ +#include "fpi-compat.h" /* host keys */ @@ -84,19 +81,12 @@ static const gchar application_secret_hex[] = "13330ba3135ecf5dc71cede01a8865407 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"; +static const gchar enrollment_nonce_hex[] = "c2101c44c9a667bba397e81f48b143398603e2c9335a68b409e1dbe71e005ca2"; +static const gchar enrollment_id_hex[] = "67109dc70a216331f1580ddac601915929c1ff6c9bcba6544ba572c660c3d91e"; -/******************************************************************************/ +/* test verify_identify values */ -GBytes *g_bytes_from_hex (const gchar *hex); - -FpiSdcpClaim *sdcp_test_claim (void); +static const gchar identify_nonce_hex[] = "3a1b506f5bec089059acefb9b44dfbdea7a599ee9aa267e5252664d60b798053"; +static const gchar identify_mac_hex[] = "53a723eef40713094a90c5ef9996cbd6ba268e30676cd7107705a6c3e3e1eff9"; diff --git a/libfprint/meson.build b/libfprint/meson.build index f132fe60..813559a2 100644 --- a/libfprint/meson.build +++ b/libfprint/meson.build @@ -144,6 +144,8 @@ 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' : diff --git a/meson.build b/meson.build index a8846ae0..a2128e5d 100644 --- a/meson.build +++ b/meson.build @@ -112,6 +112,7 @@ virtual_drivers = [ 'virtual_image', 'virtual_device', 'virtual_device_storage', + 'virtual_sdcp', ] default_drivers = [ 'upektc_img', @@ -214,6 +215,7 @@ driver_helper_mapping = { 'virtual_image' : [ 'virtual' ], 'virtual_device' : [ 'virtual' ], 'virtual_device_storage' : [ 'virtual' ], + 'virtual_sdcp' : [ 'virtual', 'sdcp' ], } driver_helpers = [] diff --git a/tests/meson.build b/tests/meson.build index 3c3af4e8..986044d5 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -19,6 +19,7 @@ envs.set('FP_DRIVERS_ALLOWLIST', ':'.join([ 'virtual_image', 'virtual_device', 'virtual_device_storage', + 'virtual_sdcp', ])) envs.set('FP_PRINTS_PATH', meson.project_source_root() / 'examples' / 'prints') @@ -91,6 +92,7 @@ if get_option('introspection') virtual_devices_tests = [ 'virtual-image', 'virtual-device', + 'virtual-sdcp', ] unittest_inspector = find_program('unittest_inspector.py') @@ -265,12 +267,7 @@ 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 diff --git a/tests/test-fp-sdcp-device.c b/tests/test-fp-sdcp-device.c deleted file mode 100644 index baa2e323..00000000 --- a/tests/test-fp-sdcp-device.c +++ /dev/null @@ -1,143 +0,0 @@ -/* - * 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 index e7a99d7a..ffb1e1a7 100644 --- a/tests/test-fpi-sdcp.c +++ b/tests/test-fpi-sdcp.c @@ -20,7 +20,55 @@ #define FP_COMPONENT "test_fpi_sdcp" #include "fpi-log.h" -#include "test-sdcp-utils.h" +#include "fpi-sdcp.h" +#include "fpi-sdcp-device.h" + +/* We can re-use the test payloads from virtual-sdcp */ +#include "drivers/virtual-sdcp.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 +}; + +static 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); +} + +static FpiSdcpClaim * +get_fake_sdcp_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); +} + +/******************************************************************************/ static void test_generate_enrollment_id (void) @@ -29,7 +77,7 @@ test_generate_enrollment_id (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 (enrollment_nonce_hex); - g_autoptr(GBytes) expected_id = g_bytes_from_hex (enrollment_enrollment_id_hex); + g_autoptr(GBytes) expected_id = g_bytes_from_hex (enrollment_id_hex); id = fpi_sdcp_generate_enrollment_id (application_secret, nonce, &error); @@ -49,7 +97,7 @@ 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) id = g_bytes_from_hex (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)); @@ -78,7 +126,7 @@ test_verify_connect (void) 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 (); + FpiSdcpClaim *claim = get_fake_sdcp_claim (); g_autoptr(GBytes) expected_application_secret = g_bytes_from_hex (application_secret_hex); diff --git a/tests/test-sdcp-device-fake.c b/tests/test-sdcp-device-fake.c deleted file mode 100644 index 6cd84473..00000000 --- a/tests/test-sdcp-device-fake.c +++ /dev/null @@ -1,197 +0,0 @@ -/* - * 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 deleted file mode 100644 index 5ad8f0eb..00000000 --- a/tests/test-sdcp-device-fake.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 deleted file mode 100644 index 430f2462..00000000 --- a/tests/test-sdcp-utils.c +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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/virtual-sdcp.py b/tests/virtual-sdcp.py new file mode 100644 index 00000000..fc12e874 --- /dev/null +++ b/tests/virtual-sdcp.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 + +import sys +try: + import gi + import os + + from gi.repository import GLib + + import unittest +except Exception as e: + print("Missing dependencies: %s" % str(e)) + sys.exit(77) + +FPrint = None + +# 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 VirtualSDCPBase(unittest.TestCase): + + @classmethod + def setUpClass(cls): + os.environ['FP_VIRTUAL_SDCP'] = '1' + + 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): + self.ctx = FPrint.Context() + self.assertIsNotNone(self.dev) + self.assertFalse(self.dev.is_open()) + self.dev.open_sync() + self.assertTrue(self.dev.is_open()) + + def tearDown(self): + self.dev.close_sync() + self.assertFalse(self.dev.is_open()) + del self.ctx + +class VirtualSDCP(VirtualSDCPBase): + + def test_connect(self): + # Nothing to do here since setUp and tearDown will open and close the device + pass + + 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.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 == 'SDCP 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() + GLib.log_remove_handler('libfprint-sdcp_device', handler_id) + assert success[0] + + def test_list(self): + prints = self.dev.list_prints_sync() + assert len(prints) == 0 + + def test_enroll_list_verify(self): + # Set up a new print + template = FPrint.Print.new(self.dev) + template.set_finger(FPrint.Finger.LEFT_THUMB) + + # Enroll the new print + new_print = self.dev.enroll_sync(template, None, None, None) + self.assertIsInstance(new_print, FPrint.Print) + + # Get the print list again and ensure there is exactly 1 print + prints = self.dev.list_prints_sync() + self.assertTrue(len(prints) == 1) + + # Ensure the one print from list is the same as new_print + self.assertTrue(prints[0].equal(new_print)) + + # Verify new_print + verify_res, verify_print = self.dev.verify_sync(prints[0]) + self.assertTrue(verify_res) + self.assertTrue(verify_print.equal(prints[0])) + + # Set up a second new print + template = FPrint.Print.new(self.dev) + template.set_finger(FPrint.Finger.LEFT_INDEX) + + # Enroll the second print + new_print2 = self.dev.enroll_sync(template, None, None, None) + self.assertIsInstance(new_print2, FPrint.Print) + + # Get the print list again and ensure there is exactly 2 prints + prints = self.dev.list_prints_sync() + self.assertTrue(len(prints) == 2) + + # Ensure the second print from list is the same as new_print2 + self.assertTrue(prints[1].equal(new_print2)) + +class VirtualSDCPNoReconnect(VirtualSDCPBase): + + @classmethod + def setUpClass(cls): + os.environ['FP_VIRTUAL_SDCP_NO_RECONNECT'] = '1' + super().setUpClass() + + def test_connect(self): + # Nothing to do here since setUp and tearDown will open and close the device + pass + + 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.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 == 'SDCP 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() + GLib.log_remove_handler('libfprint-sdcp_device', handler_id) + + # Ensure that we did NOT see "SDCP Reconnect succeeded" in the log + assert success[0] == False + + +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) + + unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2))