Merge branch 'feature/sdcp' into 'master'

sdcp: Implement SDCP data storage and resetting

See merge request libfprint/fprintd!232
This commit is contained in:
Joshua Grisham 2026-04-06 14:28:21 +00:00
commit 558c7c22aa
7 changed files with 369 additions and 2 deletions

View file

@ -55,7 +55,10 @@ stages:
before_script:
# Make sure we don't build or link against the system libfprint
- dnf remove -y libfprint-devel
- git clone https://gitlab.freedesktop.org/libfprint/libfprint.git
- git clone https://gitlab.freedesktop.org/joshuagrisham/libfprint.git # TODO: Changed temporarily for testing
- cd libfprint # TODO: Changed temporarily for testing
- git switch feature/sdcp-v2 # TODO: Changed temporarily for testing
- cd .. # TODO: Changed temporarily for testing
- meson setup libfprint/_build libfprint --prefix=/usr -Ddrivers=virtual_image,virtual_device,virtual_device_storage -Ddoc=false
- meson compile -C libfprint/_build
- meson install -C libfprint/_build

View file

@ -1024,6 +1024,21 @@ _fprint_device_add_client (FprintDevice *rdev, const char *sender)
}
}
static void
reset_sdcp_if_device_untrusted (FpDevice *dev,
GError *error)
{
if (g_error_matches (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_UNTRUSTED))
{
g_debug ("resetting SDCP connection");
if (g_object_class_find_property (G_OBJECT_GET_CLASS (dev), "sdcp-data"))
g_object_set (G_OBJECT (dev), "sdcp-data", NULL, NULL);
store.sdcp_data_delete (dev);
}
}
static void
dev_open_cb (FpDevice *dev, GAsyncResult *res, void *user_data)
{
@ -1048,11 +1063,14 @@ dev_open_cb (FpDevice *dev, GAsyncResult *res, void *user_data)
"Open failed with error: %s", error->message);
g_dbus_method_invocation_return_gerror (invocation, dbus_error);
session_data_set_new (priv, NULL, NULL);
reset_sdcp_if_device_untrusted (dev, error);
return;
}
g_debug ("claimed device %d", priv->id);
store.sdcp_data_save (priv->dev);
fprint_dbus_device_complete_claim (FPRINT_DBUS_DEVICE (rdev),
invocation);
}
@ -1120,6 +1138,8 @@ fprint_device_claim (FprintDBusDevice *dbus_dev,
g_debug ("user '%s' claiming the device: %d", session->username, priv->id);
store.sdcp_data_load (priv->dev);
priv->current_action = ACTION_OPEN;
fp_device_open (priv->dev, NULL, (GAsyncReadyCallback) dev_open_cb, rdev);
@ -1571,6 +1591,8 @@ verify_cb (FpDevice *dev, GAsyncResult *res, void *user_data)
g_debug ("verify_cb: result %s", name);
reset_sdcp_if_device_untrusted (dev, error);
/* Automatically restart the operation for retry failures */
if (error && error->domain == FP_DEVICE_RETRY)
{
@ -1616,6 +1638,8 @@ identify_cb (FpDevice *dev, GAsyncResult *res, void *user_data)
g_debug ("identify_cb: result %s", name);
reset_sdcp_if_device_untrusted (dev, error);
/* Automatically restart the operation for retry failures */
if (error && error->domain == FP_DEVICE_RETRY)
{
@ -2052,6 +2076,8 @@ enroll_cb (FpDevice *dev, GAsyncResult *res, void *user_data)
g_signal_emit (rdev, signals[SIGNAL_ENROLL_STATUS], 0, name, TRUE);
reset_sdcp_if_device_untrusted (dev, error);
if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
g_warning ("Device reported an error during enroll: %s", error->message);
@ -2091,6 +2117,8 @@ enroll_identify_cb (FpDevice *dev, GAsyncResult *res, void *user_data)
g_clear_error (&error);
}
reset_sdcp_if_device_untrusted (dev, error);
/* We may need to retry or error out. */
if (error)
{

View file

@ -366,3 +366,191 @@ file_storage_deinit (void)
g_clear_pointer (&storage_path, g_free);
return 0;
}
static char *current_boot_id = NULL;
static const char *
get_boot_id (void)
{
g_autofree gchar *kernel_boot_id = NULL;
/* read the boot_id exposed by the kernel */
if (current_boot_id == NULL)
{
if (g_file_get_contents ("/proc/sys/kernel/random/boot_id", &kernel_boot_id, NULL, NULL))
{
current_boot_id = g_strdup (kernel_boot_id);
g_strchomp (current_boot_id);
}
else
{
g_warning ("get_boot_id(): could not read '/proc/sys/kernel/random/boot_id'");
}
}
/*
* TODO: is it better to read this boot_id sysfs file or to get BootID from
* org.freedesktop.hostname1 using D-Bus ? Or maybe we should try one and use
* the other as a fallback ?
*/
return current_boot_id;
}
static char *
get_sdcp_data_path (FpDevice *dev)
{
char *path = g_build_filename (get_storage_path (),
".sdcp",
fp_device_get_driver (dev),
fp_device_get_device_id (dev),
NULL);
return g_steal_pointer (&path);
}
static int
delete_sdcp_data (FpDevice *dev,
const char *boot_id)
{
g_autofree gchar *dirpath = NULL;
g_autofree gchar *path = NULL;
int r;
dirpath = get_sdcp_data_path (dev);
path = g_build_filename (dirpath, boot_id, NULL);
if (!g_file_test (path, G_FILE_TEST_EXISTS))
return 0;
r = g_unlink (path);
g_debug ("delete_sdcp_data(): unlink(\"%s\") %s",
path, g_strerror (r));
return r;
}
int
file_storage_sdcp_data_save (FpDevice *dev)
{
g_autoptr(GError) err = NULL;
g_autoptr(GBytes) sdcp_data = NULL;
g_autofree gchar *dirpath = NULL;
g_autofree gchar *path = NULL;
int r;
dirpath = get_sdcp_data_path (dev);
path = g_build_filename (dirpath, get_boot_id (), NULL);
if (g_object_class_find_property (G_OBJECT_GET_CLASS (dev), "sdcp-data") == NULL)
{
g_debug ("file_storage_sdcp_data_save(): device does not have 'scdp-data'");
return -ENOENT;
}
g_object_get (G_OBJECT (dev), "sdcp-data", &sdcp_data, NULL);
if (!sdcp_data)
{
g_debug ("file_storage_sdcp_data_save(): device does not have 'scdp-data'");
return -ENOENT;
}
r = g_mkdir_with_parents (dirpath, DIR_PERMS);
if (r < 0)
{
g_debug ("file_storage_sdcp_data_save(): could not mkdir(\"%s\"): %s",
dirpath, g_strerror (r));
return r;
}
g_file_set_contents (path,
g_bytes_get_data (sdcp_data, NULL),
g_bytes_get_size (sdcp_data),
&err);
if (err)
{
g_debug ("file_storage_sdcp_data_save(): could not save '%s': %s",
path, err->message);
/* FIXME interpret error codes */
return err->code;
}
g_debug ("file_storage_sdcp_data_save(): SDCP data saved to %s", path);
return 0;
}
int
file_storage_sdcp_data_load (FpDevice *dev)
{
g_autoptr(GError) err = NULL;
g_autofree gchar *dirpath = NULL;
GDir *dir = NULL;
const gchar *entry;
g_autofree gchar *path = NULL;
gboolean found = FALSE;
gchar *buf = NULL;
gsize len;
dirpath = get_sdcp_data_path (dev);
if (!g_file_test (dirpath, G_FILE_TEST_EXISTS))
return 0;
dir = g_dir_open (dirpath, 0, &err);
if (!dir)
{
g_debug ("file_storage_sdcp_data_load(): failed to open directory '%s': %s",
dirpath, err->message);
g_clear_error (&err);
return -ENOENT;
}
while ((entry = g_dir_read_name (dir)) != NULL)
{
/*
* Each dir entry should be a boot_id including the "sdcp-data". If
* an entry does not match the current boot_id, we should delete it.
* Otherwise, we load the sdcp-data from that file.
*/
if (g_strcmp0 (entry, get_boot_id ()) != 0)
{
g_debug ("file_storage_sdcp_data_load(): deleting SDCP data from prior system boot '%s'",
entry);
delete_sdcp_data (dev, entry);
continue;
}
else
{
path = g_build_filename (dirpath, entry, NULL);
g_file_get_contents (path, &buf, &len, &err);
if (err)
{
g_debug ("file_storage_sdcp_data_load(): could not read SDCP data file '%s': %s",
path, err->message);
continue;
}
g_object_set (G_OBJECT (dev), "sdcp-data", g_bytes_new_take (buf, len), NULL);
g_debug ("file_storage_sdcp_data_load(): loaded SDCP data from file '%s'", path);
found = TRUE;
}
}
g_dir_close (dir);
if (found)
return 0;
else
return -ENOENT;
}
int
file_storage_sdcp_data_delete (FpDevice *dev)
{
return delete_sdcp_data (dev, get_boot_id ());
}

View file

@ -38,3 +38,9 @@ int file_storage_deinit (void);
GSList *file_storage_discover_prints (FpDevice *dev,
const char *username);
GSList *file_storage_discover_users (void);
int file_storage_sdcp_data_save (FpDevice *dev);
int file_storage_sdcp_data_load (FpDevice *dev);
int file_storage_sdcp_data_delete (FpDevice *dev);

View file

@ -51,6 +51,9 @@ set_storage_file (void)
store.print_data_delete = &file_storage_print_data_delete;
store.discover_prints = &file_storage_discover_prints;
store.discover_users = &file_storage_discover_users;
store.sdcp_data_save = &file_storage_sdcp_data_save;
store.sdcp_data_load = &file_storage_sdcp_data_load;
store.sdcp_data_delete = &file_storage_sdcp_data_delete;
}
static gboolean
@ -75,7 +78,10 @@ load_storage_module (const char *module_name)
!g_module_symbol (module, "print_data_load", (gpointer *) &store.print_data_load) ||
!g_module_symbol (module, "print_data_delete", (gpointer *) &store.print_data_delete) ||
!g_module_symbol (module, "discover_prints", (gpointer *) &store.discover_prints) ||
!g_module_symbol (module, "discover_users", (gpointer *) &store.discover_users))
!g_module_symbol (module, "discover_users", (gpointer *) &store.discover_users) ||
!g_module_symbol (module, "sdcp_data_save", (gpointer *) &store.sdcp_data_save) ||
!g_module_symbol (module, "sdcp_data_load", (gpointer *) &store.sdcp_data_load) ||
!g_module_symbol (module, "sdcp_data_delete", (gpointer *) &store.sdcp_data_delete))
{
g_module_close (module);
g_debug ("Failed to load module. Please update your code.");

View file

@ -31,6 +31,9 @@ typedef int (*storage_print_data_delete)(FpDevice *dev,
typedef GSList *(*storage_discover_prints)(FpDevice *dev,
const char *username);
typedef GSList *(*storage_discover_users)(void);
typedef int (*storage_sdcp_data_load)(FpDevice *dev);
typedef int (*storage_sdcp_data_save)(FpDevice *dev);
typedef int (*storage_sdcp_data_delete)(FpDevice *dev);
typedef int (*storage_init)(void);
typedef int (*storage_deinit)(void);
@ -43,6 +46,9 @@ struct storage
storage_print_data_delete print_data_delete;
storage_discover_prints discover_prints;
storage_discover_users discover_users;
storage_sdcp_data_save sdcp_data_save;
storage_sdcp_data_load sdcp_data_load;
storage_sdcp_data_delete sdcp_data_delete;
};
typedef struct storage fp_storage;

View file

@ -3448,6 +3448,136 @@ class FPrintdUtilsTest(FPrintdVirtualStorageDeviceBaseTest):
self.run_verify(finger=FPrint.Finger.UNKNOWN, match=False,
error=FPrint.DeviceError.PROTO)
class FPrintdVirtualSdcpDeviceBaseTests(FPrintdVirtualDeviceBaseTest):
socket_env = 'FP_VIRTUAL_SDCP'
device_driver = 'virtual_sdcp'
driver_name = 'Virtual SDCP device for debugging'
has_identification = True
def get_sdcp_data_path(self):
with open("/proc/sys/kernel/random/boot_id", "r") as f:
boot_id = f.read().strip()
return os.path.join(self.state_dir, ".sdcp", self.device_driver, str(self.device_id),
boot_id)
def test_connect(self):
# To test a Connect, we just need to open and close the device
self.device.Claim('(s)', 'testuser')
self.device.Release()
# Check that the SDCP data file was created
self.assertTrue(os.path.isfile(self.get_sdcp_data_path()))
def test_reconnect(self):
# To test a Reconnect, we should connect, disconnect, and connect again
self.device.Claim('(s)', 'testuser')
self.device.Release()
self.device.Claim('(s)', 'testuser')
self.daemon_log.check_line('SDCP Reconnect succeeded', timeout=3)
self.device.Release()
def test_reconnect_from_sdcp_data(self):
# Connecting will create the SDCP data file
self.device.Claim('(s)', 'testuser')
self.device.Release()
# Now stop and restart the daemon so that sdcp-data will be unloaded from memory and force
# reading data from disk instead
self.daemon_stop()
self.daemon_start(self.driver_name)
self.device.Claim('(s)', 'testuser')
self.daemon_log.check_line('loaded SDCP data from file', timeout=3)
self.daemon_log.check_line('SDCP Reconnect succeeded', timeout=3)
self.device.Release()
def test_reconnect_with_invalid_data(self):
self.device.Claim('(s)', 'testuser')
self.device.Release()
self.daemon_stop()
# virtual_sdcp uses a hard-coded application secret; if we just write something else then it should stop working
bad_sdcp_data = bytes.fromhex("0123456789abcdef1032547698badcfeaabbccddeeff00112233445566778899")
with open(self.get_sdcp_data_path(), "wb") as f:
f.write(bad_sdcp_data)
# Now when we try to connect, the Reconnect will fail and it should reset SDCP data and perform a normal Connect again
self.daemon_start(self.driver_name)
self.device.Claim('(s)', 'testuser')
self.daemon_log.check_line('SDCP Reconnect failed', timeout=3)
self.daemon_log.check_line('SDCP ConnectResponse claim validated successfully', timeout=3)
self.device.Release()
# And check that the SDCP data file has been changed from the above bogus value
with open(self.get_sdcp_data_path(), "rb") as f:
new_sdcp_data = f.read()
self.assertNotEqual(bad_sdcp_data, new_sdcp_data)
def test_list(self):
self.device.Claim('(s)', 'testuser')
with self.assertFprintError('NoEnrolledPrints'):
self.device.ListEnrolledFingers('(s)', 'testuser')
self.device.Release()
def test_enroll_list_verify(self):
self.device.Claim('(s)', 'testuser')
self.device.EnrollStart('(s)', 'right-thumb')
self.device.EnrollStop()
enrolled = self.device.ListEnrolledFingers('(s)', 'testuser')
self.assertEqual(enrolled, ['right-thumb'])
self.device.VerifyStart('(s)', 'any')
self.wait_for_result('verify-match')
self.device.VerifyStop()
self.device.Release()
def test_enroll_with_invalid_data(self):
# To test this we need to ensure that a Reconnect will not be attempted
# (fprintd will just use the sdcp-data file as-is instead of correcting is as part of Reconnect)
os.environ['FP_VIRTUAL_SDCP_NO_RECONNECT'] = '1'
self.device.Claim('(s)', 'testuser')
self.device.Release()
self.daemon_stop()
# Now we set the bad sdcp-data which will be used with the next operation
bad_sdcp_data = bytes.fromhex("0123456789abcdef1032547698badcfeaabbccddeeff00112233445566778899")
with open(self.get_sdcp_data_path(), "wb") as f:
f.write(bad_sdcp_data)
self.daemon_start(self.driver_name)
self.device.Claim('(s)', 'testuser')
self.device.EnrollStart('(s)', 'right-thumb')
self.device.EnrollStop()
self.wait_for_result('enroll-unknown-error')
self.daemon_log.check_line('resetting SDCP connection', timeout=3)
# When exception occurs fprintd should release the device and then
# claim again for the next operation, so we will mimic that here
self.device.Release()
self.device.Claim('(s)', 'testuser')
# Check that a new Connect was performed
self.daemon_log.check_line('SDCP ConnectResponse claim validated successfully', timeout=3)
with self.assertFprintError('NoEnrolledPrints'):
self.device.ListEnrolledFingers('(s)', 'testuser')
self.device.EnrollStart('(s)', 'right-thumb')
self.device.EnrollStop()
enrolled = self.device.ListEnrolledFingers('(s)', 'testuser')
self.assertEqual(enrolled, ['right-thumb'])
self.device.Release()
def list_tests():
import unittest_inspector