Files
omarchy-pkgs/pkgbuilds/libfprint-git/aurora-apple-sep.patch
T
6be6e98870 Build libfprint-git for aarch64 with the Apple Touch ID driver
Omarchy's fingerprint setup installs libfprint-git, which built for x86_64
only, so an Apple Silicon Mac had no Omarchy libfprint at all. It now builds
for aarch64 too, and there it carries Chris Kearney's aurora driver for
Touch ID, which the aurora kernel's Secure Enclave driver exposes as
/dev/sep-bio. The x86_64 build applies nothing new.

The patch is his libfprint-1.94.100-apple-sep.patch from
iconidentify/aurora-linux sep-7.1.12.aurora2-11.35, less its
tests/meson.build hunk: libfprint 3f1e2817, already in this pin, made the
same fix. The libfprint provide is now versioned, so aurora-touchid's
libfprint>=1.94.100-1.1 still resolves when this replaces his build.

Built on an M2 Max: 131 libfprint tests pass, none fail.

Co-authored-by: Chris Kearney <303316+iconidentify@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 00:51:34 -04:00

1239 lines
42 KiB
Diff

Apple Touch ID, through the aurora kernel's Secure Enclave driver (/dev/sep-bio).
By Chris Kearney, from iconidentify/aurora-linux tag sep-7.1.12.aurora2-11.35,
tools/aurora-sep/patches/libfprint-1.94.100-apple-sep.patch. Its
tests/meson.build hunk is left out: libfprint 3f1e2817 made the same fix.
diff --git a/libfprint/drivers/aurora/aurora-bio.h b/libfprint/drivers/aurora/aurora-bio.h
new file mode 100644
--- /dev/null
+++ b/libfprint/drivers/aurora/aurora-bio.h
@@ -0,0 +1,132 @@
+/* SPDX-License-Identifier: GPL-2.0-only OR MIT */
+/* Copyright 2026 Dj */
+/*
+ * Userspace interface for the SEP/Mesa biometric device.
+ *
+ * The enclave does the matching. Nothing biometric crosses this interface: only
+ * an operation, a stage, a status, an opaque identity UUID, and an opaque host
+ * label userspace chooses.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include <linux/ioctl.h>
+#include <linux/types.h>
+
+#define AURORA_BIO_IFACE_VERSION 4
+
+#define AURORA_BIO_UUID_LEN 16
+#define AURORA_BIO_LABEL_LEN 128
+#define AURORA_BIO_NONCE_LEN 32
+#define AURORA_BIO_TOKEN_LEN 32
+#define AURORA_BIO_MAX_IDENTITIES 32
+#define AURORA_BIO_CHALLENGE_LEN 32
+#define AURORA_BIO_ATTEST_PUB_LEN 65
+#define AURORA_BIO_ATTEST_SIG_MAX 72
+
+enum {
+ AURORA_BIO_STATE_IDLE = 0,
+ AURORA_BIO_STATE_PENDING = 1,
+ AURORA_BIO_STATE_PROGRESS = 2,
+ AURORA_BIO_STATE_DONE = 3,
+ AURORA_BIO_STATE_FAILED = 4,
+};
+
+
+enum {
+ AURORA_BIO_NO_MATCH = 0,
+ AURORA_BIO_MATCH = 1,
+ AURORA_BIO_NOT_COMPARED = 2,
+};
+
+struct aurora_bio_identity {
+ __u8 uuid[AURORA_BIO_UUID_LEN];
+ __u8 label[AURORA_BIO_LABEL_LEN];
+};
+
+struct aurora_bio_info {
+ __u32 version;
+ __u32 sensor_present;
+ __u32 enrolled;
+ __u32 capacity;
+ __u32 enroll_stages;
+ __u32 reserved[3];
+};
+
+/* reserved[0] flags; reserved[1] is a live SEP identity count only when valid. */
+#define AURORA_BIO_INFO_LIVE_COUNT_VALID 1u
+
+struct aurora_bio_list {
+ __u32 count;
+ __u32 reserved;
+ struct aurora_bio_identity id[AURORA_BIO_MAX_IDENTITIES];
+};
+
+struct aurora_bio_enrol_start {
+ __u32 flags;
+ __u32 reserved;
+ __u8 label[AURORA_BIO_LABEL_LEN];
+};
+
+#define AURORA_BIO_GUIDANCE_NONE 0
+#define AURORA_BIO_GUIDANCE_PLACE 1
+#define AURORA_BIO_GUIDANCE_LIFT_AND_MOVE 2
+#define AURORA_BIO_GUIDANCE_HOLD_STILL 3
+
+struct aurora_bio_enrol_poll {
+ __u32 state;
+ __u32 stage;
+ __u32 stages_total;
+ __u32 status;
+ __u8 uuid[AURORA_BIO_UUID_LEN];
+ __u32 guidance;
+ __u32 progress_percent;
+};
+struct aurora_bio_verify_start {
+ __u32 flags;
+ __u32 reserved;
+ __u8 nonce[AURORA_BIO_NONCE_LEN];
+};
+
+struct aurora_bio_verify_poll {
+ __u32 state;
+ __u32 result;
+ __u32 status;
+ __u32 guidance; /* AURORA_BIO_GUIDANCE_*; status only */
+ __u8 uuid[AURORA_BIO_UUID_LEN];
+ __u8 token[AURORA_BIO_TOKEN_LEN]; /* single use, bound to the nonce */
+ __u64 deadline_ns; /* CLOCK_MONOTONIC; past this the token is void */
+};
+
+struct aurora_bio_delete {
+ __u8 uuid[AURORA_BIO_UUID_LEN];
+};
+
+/*
+ * Device attestation of key possession: the enclave signs 'challenge' with the
+ * machine ref-key (ECDSA-P256 over the challenge as the pre-computed digest) and
+ * returns the DER signature and public point. The private key never leaves the
+ * enclave.
+ */
+struct aurora_bio_attest {
+ __u32 sig_len; /* out: DER signature length */
+ __u8 challenge[AURORA_BIO_CHALLENGE_LEN]; /* in */
+ __u8 public[AURORA_BIO_ATTEST_PUB_LEN]; /* out: P-256 point, 04||X||Y */
+ __u8 signature[AURORA_BIO_ATTEST_SIG_MAX]; /* out: DER SEQUENCE{r,s} */
+ __u8 reserved[3];
+};
+
+#define AURORA_BIO_IOC_MAGIC 0xB1
+
+#define AURORA_BIO_GET_INFO _IOR (AURORA_BIO_IOC_MAGIC, 0x01, struct aurora_bio_info)
+#define AURORA_BIO_LIST _IOR (AURORA_BIO_IOC_MAGIC, 0x02, struct aurora_bio_list)
+#define AURORA_BIO_ENROL_START _IOW (AURORA_BIO_IOC_MAGIC, 0x03, struct aurora_bio_enrol_start)
+#define AURORA_BIO_ENROL_POLL _IOR (AURORA_BIO_IOC_MAGIC, 0x04, struct aurora_bio_enrol_poll)
+#define AURORA_BIO_VERIFY_START _IOW (AURORA_BIO_IOC_MAGIC, 0x05, struct aurora_bio_verify_start)
+#define AURORA_BIO_VERIFY_POLL _IOR (AURORA_BIO_IOC_MAGIC, 0x06, struct aurora_bio_verify_poll)
+#define AURORA_BIO_CANCEL _IO (AURORA_BIO_IOC_MAGIC, 0x07)
+#define AURORA_BIO_DELETE _IOW (AURORA_BIO_IOC_MAGIC, 0x08, struct aurora_bio_delete)
+#define AURORA_BIO_DELETE_ALL _IO (AURORA_BIO_IOC_MAGIC, 0x09)
+#define AURORA_BIO_ATTEST _IOWR(AURORA_BIO_IOC_MAGIC, 0x0a, struct aurora_bio_attest)
diff --git a/libfprint/drivers/aurora/aurora.c b/libfprint/drivers/aurora/aurora.c
new file mode 100644
--- /dev/null
+++ b/libfprint/drivers/aurora/aurora.c
@@ -0,0 +1,881 @@
+/*
+ * Aurora SEP fingerprint driver
+ *
+ * The sensor is matched inside Apple's secure enclave. This driver never sees
+ * an image, a template, or key material: it starts operations on the kernel
+ * device and reports the enclave's verdict. That is why it can be short.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#define FP_COMPONENT "apple-sep"
+
+#include "drivers_api.h"
+#include "aurora-bio.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <pwd.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/random.h>
+#include <time.h>
+#include <unistd.h>
+#include <glib-unix.h>
+
+struct _FpiDeviceAurora
+{
+ FpDevice parent;
+
+ gint fd;
+ guint watch_id;
+ guint32 enroll_last_reported_stage;
+
+ guint8 nonce[AURORA_BIO_NONCE_LEN];
+ gboolean nonce_valid;
+};
+
+G_DECLARE_FINAL_TYPE (FpiDeviceAurora, fpi_device_aurora, FPI, DEVICE_AURORA, FpDevice)
+G_DEFINE_TYPE (FpiDeviceAurora, fpi_device_aurora, FP_TYPE_DEVICE)
+
+/* ------------------------------------------------------------------ */
+/* helpers */
+/* ------------------------------------------------------------------ */
+
+static gboolean
+aurora_ioctl (FpiDeviceAurora *self, unsigned long req, void *arg, GError **error)
+{
+ if (self->fd < 0)
+ {
+ g_set_error_literal (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_NOT_OPEN,
+ "device is not open");
+ return FALSE;
+ }
+
+ if (ioctl (self->fd, req, arg) < 0)
+ {
+ gint err = errno;
+
+ switch (err)
+ {
+ case ENODEV:
+ g_set_error_literal (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_REMOVED,
+ "the enclave reports no fingerprint sensor");
+ break;
+
+ case EPERM:
+ case EACCES:
+ g_set_error_literal (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_NOT_SUPPORTED,
+ "not permitted");
+ break;
+
+ case ENOSPC:
+ g_set_error_literal (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_FULL,
+ "three SEP fingerprint slots are full; delete one before enrolling");
+ break;
+
+ case ENOENT:
+ g_set_error_literal (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_NOT_FOUND,
+ "no such identity");
+ break;
+
+ default:
+ g_set_error (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_PROTO,
+ "ioctl failed: %s", g_strerror (err));
+ break;
+ }
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/* The label is how the host recognises its own records in the device's list.
+ * libfprint already has an encoding for exactly this -- it carries the enroll
+ * date, the finger and the username -- so use it rather than inventing one. */
+static gboolean
+aurora_label_from_print (FpPrint *print, guint8 *label_out, GError **error)
+{
+ g_autofree gchar *user_id = NULL;
+
+ if (print == NULL)
+ {
+ g_set_error_literal (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_INVALID,
+ "no print to label");
+ return FALSE;
+ }
+
+ user_id = fpi_print_generate_user_id (print);
+
+ if (user_id == NULL || strlen (user_id) >= AURORA_BIO_LABEL_LEN)
+ {
+ g_set_error_literal (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_INVALID,
+ "print label does not fit the device record");
+ return FALSE;
+ }
+
+ memset (label_out, 0, AURORA_BIO_LABEL_LEN);
+ memcpy (label_out, user_id, strlen (user_id));
+ return TRUE;
+}
+
+static void
+aurora_stop_watch (FpiDeviceAurora *self)
+{
+ if (self->watch_id != 0)
+ {
+ g_source_remove (self->watch_id);
+ self->watch_id = 0;
+ }
+}
+
+/* Build the FpPrint that represents one enclave-stored identity. The identity
+ * UUID is all we hold; there is no template on this side to store. */
+static FpPrint *
+aurora_print_from_uuid (FpiDeviceAurora *self,
+ const guint8 *uuid,
+ const guint8 *label)
+{
+ FpPrint *print = fp_print_new (FP_DEVICE (self));
+ GVariant *data;
+
+ data = g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE, uuid,
+ AURORA_BIO_UUID_LEN, 1);
+
+ fpi_print_set_type (print, FPI_PRINT_RAW);
+ fpi_print_set_device_stored (print, TRUE);
+ g_object_set (print, "fpi-data", data, NULL);
+
+ /* The label came from the device. Treat it as untrusted bytes: bound it to
+ * the buffer before anything reads it as a string. */
+ if (label != NULL)
+ {
+ gchar safe[AURORA_BIO_LABEL_LEN];
+
+ memcpy (safe, label, AURORA_BIO_LABEL_LEN);
+ safe[AURORA_BIO_LABEL_LEN - 1] = '\0';
+
+ if (safe[0] != '\0')
+ fpi_print_fill_from_user_id (print, safe);
+ }
+
+ return print;
+}
+
+static gboolean
+aurora_uuid_from_print (FpPrint *print, guint8 *uuid_out, GError **error)
+{
+ g_autoptr(GVariant) data = NULL;
+ const guint8 *raw;
+ gsize len = 0;
+
+ g_object_get (print, "fpi-data", &data, NULL);
+
+ if (data == NULL || !g_variant_is_of_type (data, G_VARIANT_TYPE ("ay")))
+ {
+ g_set_error_literal (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_INVALID,
+ "print does not carry an enclave identity");
+ return FALSE;
+ }
+
+ raw = g_variant_get_fixed_array (data, &len, 1);
+ if (raw == NULL || len != AURORA_BIO_UUID_LEN)
+ {
+ g_set_error_literal (error, FP_DEVICE_ERROR, FP_DEVICE_ERROR_DATA_INVALID,
+ "enclave identity is the wrong size");
+ return FALSE;
+ }
+
+ memcpy (uuid_out, raw, AURORA_BIO_UUID_LEN);
+ return TRUE;
+}
+
+/* Compare by enclave identity rather than by whole print.
+ *
+ * fp_print_equal() compares host-side labels too -- the username and finger a
+ * print was filed under. Those are ours, not the enclave's: the device knows
+ * only the identity it stored. Comparing whole prints made a correct match
+ * from fprintd, which files prints under a username, read as a non-match
+ * against the bare print the device hands back. The UUID is the ground truth,
+ * so compare that. */
+static gboolean
+aurora_print_has_uuid (FpPrint *print, const guint8 *uuid)
+{
+ guint8 stored[AURORA_BIO_UUID_LEN];
+
+ if (print == NULL)
+ return FALSE;
+
+ if (!aurora_uuid_from_print (print, stored, NULL))
+ return FALSE;
+
+ return memcmp (stored, uuid, AURORA_BIO_UUID_LEN) == 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* probe / open / close */
+/* ------------------------------------------------------------------ */
+
+static void
+aurora_probe (FpDevice *device)
+{
+ struct aurora_bio_info info = { 0 };
+ const gchar *path;
+ g_autofree gchar *serial = NULL;
+ gint fd;
+
+ path = fpi_device_get_udev_data (device, FPI_DEVICE_UDEV_SUBTYPE_MISC);
+ if (path == NULL)
+ {
+ fpi_device_probe_complete (device, NULL, NULL,
+ fpi_device_error_new_msg (FP_DEVICE_ERROR_NOT_SUPPORTED,
+ "no sep-bio node"));
+ return;
+ }
+
+ fd = open (path, O_RDWR | O_CLOEXEC);
+ if (fd < 0)
+ {
+ fpi_device_probe_complete (device, NULL, NULL,
+ fpi_device_error_new_msg (FP_DEVICE_ERROR_NOT_SUPPORTED,
+ "cannot open %s: %s",
+ path, g_strerror (errno)));
+ return;
+ }
+
+ if (ioctl (fd, AURORA_BIO_GET_INFO, &info) < 0)
+ {
+ gint err = errno;
+
+ close (fd);
+ fpi_device_probe_complete (device, NULL, NULL,
+ fpi_device_error_new_msg (FP_DEVICE_ERROR_NOT_SUPPORTED,
+ "cannot query device: %s",
+ g_strerror (err)));
+ return;
+ }
+
+ close (fd);
+
+ if (info.version != AURORA_BIO_IFACE_VERSION)
+ {
+ fpi_device_probe_complete (device, NULL, NULL,
+ fpi_device_error_new_msg (FP_DEVICE_ERROR_NOT_SUPPORTED,
+ "interface version %u, expected %u",
+ info.version,
+ AURORA_BIO_IFACE_VERSION));
+ return;
+ }
+
+ /* A driver that claims a device it cannot use makes the whole stack look
+ * broken to the user. If the enclave has no sensor, say so plainly and let
+ * fprintd report "no devices" rather than a device that fails every touch. */
+ if (!info.sensor_present)
+ {
+ fp_dbg ("enclave reports no fingerprint sensor attached");
+ fpi_device_probe_complete (device, NULL, NULL,
+ fpi_device_error_new_msg (FP_DEVICE_ERROR_NOT_SUPPORTED,
+ "the secure enclave reports no "
+ "fingerprint sensor attached"));
+ return;
+ }
+
+ if (info.enroll_stages > 0)
+ fpi_device_set_nr_enroll_stages (device, info.enroll_stages);
+
+ serial = g_strdup ("apple-sep");
+ fpi_device_probe_complete (device, serial, NULL, NULL);
+}
+
+static void
+aurora_open (FpDevice *device)
+{
+ FpiDeviceAurora *self = FPI_DEVICE_AURORA (device);
+ const gchar *path;
+
+ path = fpi_device_get_udev_data (device, FPI_DEVICE_UDEV_SUBTYPE_MISC);
+ if (path == NULL)
+ {
+ fpi_device_open_complete (device,
+ fpi_device_error_new_msg (FP_DEVICE_ERROR_NOT_SUPPORTED,
+ "no sep-bio node"));
+ return;
+ }
+
+ /* The kernel admits one opener at a time; EBUSY means something else holds
+ * the sensor, which is a real condition and not a driver fault. */
+ self->fd = open (path, O_RDWR | O_CLOEXEC);
+ if (self->fd < 0)
+ {
+ gint err = errno;
+
+ fpi_device_open_complete (device,
+ fpi_device_error_new_msg (err == EBUSY
+ ? FP_DEVICE_ERROR_BUSY
+ : FP_DEVICE_ERROR_NOT_SUPPORTED,
+ "cannot open %s: %s",
+ path, g_strerror (err)));
+ return;
+ }
+
+ fpi_device_open_complete (device, NULL);
+}
+
+static void
+aurora_close (FpDevice *device)
+{
+ FpiDeviceAurora *self = FPI_DEVICE_AURORA (device);
+
+ aurora_stop_watch (self);
+
+ if (self->fd >= 0)
+ {
+ /* Closing the description cancels anything in flight, by contract. */
+ close (self->fd);
+ self->fd = -1;
+ }
+
+ self->nonce_valid = FALSE;
+ memset (self->nonce, 0, sizeof (self->nonce));
+ fpi_device_report_finger_status (device, FP_FINGER_STATUS_NONE);
+
+ fpi_device_close_complete (device, NULL);
+}
+
+static void
+aurora_cancel (FpDevice *device)
+{
+ FpiDeviceAurora *self = FPI_DEVICE_AURORA (device);
+
+ fpi_device_report_finger_status (device, FP_FINGER_STATUS_NONE);
+ if (self->fd >= 0)
+ ioctl (self->fd, AURORA_BIO_CANCEL);
+}
+
+static void
+aurora_report_guidance (FpDevice *device, guint32 guidance, gboolean active)
+{
+ FpFingerStatusFlags status = FP_FINGER_STATUS_NONE;
+
+ if (active)
+ {
+ if (guidance == AURORA_BIO_GUIDANCE_HOLD_STILL)
+ status = FP_FINGER_STATUS_PRESENT;
+ else if (guidance == AURORA_BIO_GUIDANCE_PLACE ||
+ guidance == AURORA_BIO_GUIDANCE_LIFT_AND_MOVE)
+ status = FP_FINGER_STATUS_NEEDED;
+ }
+
+ fpi_device_report_finger_status (device, status);
+}
+
+/* ------------------------------------------------------------------ */
+/* enrolment */
+/* ------------------------------------------------------------------ */
+
+static gboolean
+aurora_enrol_ready (gint fd, GIOCondition condition, gpointer user_data)
+{
+ FpDevice *device = FP_DEVICE (user_data);
+ FpiDeviceAurora *self = FPI_DEVICE_AURORA (device);
+ struct aurora_bio_enrol_poll poll_res = { 0 };
+ g_autoptr(GError) error = NULL;
+ FpPrint *print = NULL;
+
+ if (!aurora_ioctl (self, AURORA_BIO_ENROL_POLL, &poll_res, &error))
+ {
+ self->watch_id = 0;
+ fpi_device_report_finger_status (device, FP_FINGER_STATUS_NONE);
+ fpi_device_enroll_complete (device, NULL, g_steal_pointer (&error));
+ return G_SOURCE_REMOVE;
+ }
+
+ aurora_report_guidance (device, poll_res.guidance,
+ poll_res.state == AURORA_BIO_STATE_PENDING ||
+ poll_res.state == AURORA_BIO_STATE_PROGRESS);
+
+ switch (poll_res.state)
+ {
+ case AURORA_BIO_STATE_PENDING:
+ return G_SOURCE_CONTINUE;
+
+ case AURORA_BIO_STATE_PROGRESS:
+ /* fprintd calls this "enroll-stage-passed". A guidance wakeup or an
+ * initial stage-zero event must not masquerade as a captured sample. */
+ if (poll_res.stage > self->enroll_last_reported_stage)
+ {
+ self->enroll_last_reported_stage = poll_res.stage;
+ fpi_device_enroll_progress (device, poll_res.stage, NULL, NULL);
+ }
+ return G_SOURCE_CONTINUE;
+
+ case AURORA_BIO_STATE_DONE:
+ fpi_device_get_enroll_data (device, &print);
+ if (print != NULL)
+ {
+ GVariant *data = g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE,
+ poll_res.uuid,
+ AURORA_BIO_UUID_LEN, 1);
+ fpi_print_set_type (print, FPI_PRINT_RAW);
+ fpi_print_set_device_stored (print, TRUE);
+ g_object_set (print, "fpi-data", data, NULL);
+ }
+ self->watch_id = 0;
+ fpi_device_enroll_complete (device,
+ print ? g_object_ref (print) : NULL,
+ print ? NULL
+ : fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL,
+ "enrolment produced no print"));
+ return G_SOURCE_REMOVE;
+
+ case AURORA_BIO_STATE_FAILED:
+ default:
+ self->watch_id = 0;
+ fpi_device_enroll_complete (device, NULL,
+ fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL,
+ "enrolment failed, driver status 0x%x",
+ poll_res.status));
+ return G_SOURCE_REMOVE;
+ }
+}
+
+static void
+aurora_enroll (FpDevice *device)
+{
+ FpiDeviceAurora *self = FPI_DEVICE_AURORA (device);
+ struct aurora_bio_enrol_start start = { 0 };
+ g_autoptr(GError) error = NULL;
+ FpPrint *print = NULL;
+
+ fpi_device_get_enroll_data (device, &print);
+
+ if (!aurora_label_from_print (print, start.label, &error))
+ {
+ fpi_device_enroll_complete (device, NULL, g_steal_pointer (&error));
+ return;
+ }
+
+ if (!aurora_ioctl (self, AURORA_BIO_ENROL_START, &start, &error))
+ {
+ fpi_device_enroll_complete (device, NULL, g_steal_pointer (&error));
+ return;
+ }
+
+ self->enroll_last_reported_stage = 0;
+ aurora_report_guidance (device, AURORA_BIO_GUIDANCE_PLACE, TRUE);
+ aurora_stop_watch (self);
+ self->watch_id = g_unix_fd_add (self->fd, G_IO_IN, aurora_enrol_ready, device);
+}
+
+/* ------------------------------------------------------------------ */
+/* verify and identify */
+/* ------------------------------------------------------------------ */
+
+/* The single place a match may be declared.
+ *
+ * Everything else in this driver reports failure. A result counts as a match
+ * only if the enclave said DONE and said MATCH, we require it to have bound the
+ * answer to the nonce we generated for this operation (a binding the host
+ * cannot verify cryptographically -- it is enforced by the enclave), and it
+ * arrived within the token's deadline. Any other combination -- including a
+ * state we do not recognise -- is a non-match, never an error that a caller
+ * might interpret loosely. */
+static gboolean
+aurora_result_is_match (FpiDeviceAurora *self,
+ const struct aurora_bio_verify_poll *res)
+{
+ struct timespec now;
+ guint64 now_ns;
+ gboolean token_set = FALSE;
+ gsize i;
+
+ if (res->state != AURORA_BIO_STATE_DONE)
+ return FALSE;
+
+ if (res->result != AURORA_BIO_MATCH)
+ return FALSE;
+
+ if (!self->nonce_valid)
+ return FALSE;
+
+ for (i = 0; i < AURORA_BIO_TOKEN_LEN; i++)
+ if (res->token[i] != 0)
+ {
+ token_set = TRUE;
+ break;
+ }
+
+ if (!token_set)
+ return FALSE;
+
+ if (clock_gettime (CLOCK_MONOTONIC, &now) != 0)
+ return FALSE;
+
+ now_ns = (guint64) now.tv_sec * 1000000000ull + (guint64) now.tv_nsec;
+ if (res->deadline_ns == 0 || now_ns > res->deadline_ns)
+ {
+ fp_dbg ("match result arrived after its deadline; refusing it");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+aurora_verify_ready (gint fd, GIOCondition condition, gpointer user_data)
+{
+ FpDevice *device = FP_DEVICE (user_data);
+ FpiDeviceAurora *self = FPI_DEVICE_AURORA (device);
+ struct aurora_bio_verify_poll res = { 0 };
+ FpiDeviceAction action = fpi_device_get_current_action (device);
+ g_autoptr(GError) error = NULL;
+ /* Not a g_autoptr: FpPrint is a GInitiallyUnowned and both report functions
+ * take the floating reference. Unreffing it here as well drops the last
+ * reference under libfprint's feet, and it frees the print again when the
+ * match completes. */
+ FpPrint *matched = NULL;
+
+ if (!aurora_ioctl (self, AURORA_BIO_VERIFY_POLL, &res, &error))
+ {
+ self->watch_id = 0;
+ fpi_device_report_finger_status (device, FP_FINGER_STATUS_NONE);
+ /* Terminal path: void the per-operation nonce like every other exit, so
+ * a nonce cannot be carried into a later operation. */
+ self->nonce_valid = FALSE;
+ memset (self->nonce, 0, sizeof (self->nonce));
+ if (action == FPI_DEVICE_ACTION_VERIFY)
+ fpi_device_verify_complete (device, g_steal_pointer (&error));
+ else
+ fpi_device_identify_complete (device, g_steal_pointer (&error));
+ return G_SOURCE_REMOVE;
+ }
+
+ aurora_report_guidance (device, res.guidance,
+ res.state == AURORA_BIO_STATE_PENDING ||
+ res.state == AURORA_BIO_STATE_PROGRESS);
+
+ if (res.state == AURORA_BIO_STATE_PENDING ||
+ res.state == AURORA_BIO_STATE_PROGRESS)
+ return G_SOURCE_CONTINUE;
+
+ self->watch_id = 0;
+
+ if (aurora_result_is_match (self, &res))
+ matched = aurora_print_from_uuid (self, res.uuid, NULL);
+
+
+ if (action == FPI_DEVICE_ACTION_VERIFY)
+ {
+ FpPrint *expected = NULL;
+
+ fpi_device_get_verify_data (device, &expected);
+
+ if (matched != NULL && aurora_print_has_uuid (expected, res.uuid))
+ {
+ fpi_device_verify_report (device, FPI_MATCH_SUCCESS, matched, NULL);
+ fpi_device_verify_complete (device, NULL);
+ }
+ else if (res.result == AURORA_BIO_NOT_COMPARED)
+ {
+ /* No comparison happened, so there is no verdict to report. Saying
+ * FAIL here would tell the user their finger was rejected when it
+ * was never examined. */
+ fpi_device_verify_complete (device,
+ fpi_device_error_new (FP_DEVICE_ERROR_GENERAL));
+ }
+ else
+ {
+ fpi_device_verify_report (device, FPI_MATCH_FAIL, matched, NULL);
+ fpi_device_verify_complete (device, NULL);
+ }
+ }
+ else
+ {
+ GPtrArray *gallery = NULL;
+ FpPrint *hit = NULL;
+
+ fpi_device_get_identify_data (device, &gallery);
+
+ if (matched != NULL && gallery != NULL)
+ {
+ guint i;
+
+ for (i = 0; i < gallery->len; i++)
+ {
+ FpPrint *candidate = g_ptr_array_index (gallery, i);
+
+ if (aurora_print_has_uuid (candidate, res.uuid))
+ {
+ hit = candidate;
+ break;
+ }
+ }
+ }
+
+ if (matched == NULL && res.result == AURORA_BIO_NOT_COMPARED)
+ {
+ fpi_device_identify_complete (device,
+ fpi_device_error_new (FP_DEVICE_ERROR_GENERAL));
+ }
+ else
+ {
+ fpi_device_identify_report (device, hit, matched, NULL);
+ fpi_device_identify_complete (device, NULL);
+ }
+ }
+
+ self->nonce_valid = FALSE;
+ memset (self->nonce, 0, sizeof (self->nonce));
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+aurora_match_start (FpDevice *device)
+{
+ FpiDeviceAurora *self = FPI_DEVICE_AURORA (device);
+ struct aurora_bio_verify_start start = { 0 };
+ struct aurora_bio_info info = { 0 };
+ FpiDeviceAction action = fpi_device_get_current_action (device);
+ g_autoptr(GError) error = NULL;
+ FpPrint *print = NULL;
+
+ if (action == FPI_DEVICE_ACTION_VERIFY)
+ fpi_device_get_verify_data (device, &print);
+
+ /* An identify against an EMPTY gallery is "no match" by definition, and
+ * answering it needs no hardware at all.
+ *
+ * This is not an optimisation, it is what unblocks enrolment. fprintd runs a
+ * duplicate check before each enrol by identifying the incoming finger
+ * against the prints already stored. On a device with nothing enrolled that
+ * gallery is empty, so the question has only one possible answer -- but we
+ * were forwarding it to the kernel, whose verify ioctl returns ENOSYS
+ * because matching is not implemented yet. fprintd then reported
+ * "ioctl failed: Function not implemented", which GNOME shows to the user as
+ * "Fingerprint device disconnected".
+ *
+ * So the first enrolment on a fresh device was impossible, and the message
+ * blamed the cable. Answer the empty case here and the enrol proceeds.
+ *
+ * A NON-empty gallery normally goes to the enclave for matching. A stale
+ * host index may outlive its SEP identity after a failed cold restore; only
+ * an explicit, valid zero live-identity count from SEP lets us answer that
+ * gallery without capture. An unavailable count must never be treated as
+ * zero, and VERIFY still goes through the kernel's restore proof gate.
+ */
+ if (action == FPI_DEVICE_ACTION_IDENTIFY)
+ {
+ GPtrArray *gallery = NULL;
+
+ fpi_device_get_identify_data (device, &gallery);
+ if (gallery == NULL || gallery->len == 0)
+ {
+ fp_dbg ("identify against an empty gallery: reporting no match "
+ "without touching the sensor");
+ fpi_device_identify_report (device, NULL, NULL, NULL);
+ fpi_device_identify_complete (device, NULL);
+ return;
+ }
+
+ if (ioctl (self->fd, AURORA_BIO_GET_INFO, &info) == 0 &&
+ info.version == AURORA_BIO_IFACE_VERSION &&
+ (info.reserved[0] & AURORA_BIO_INFO_LIVE_COUNT_VALID) != 0 &&
+ info.reserved[1] == 0)
+ {
+ fp_dbg ("SEP proved zero live identities despite a non-empty host "
+ "gallery: reporting no match for identify only");
+ fpi_device_identify_report (device, NULL, NULL, NULL);
+ fpi_device_identify_complete (device, NULL);
+ return;
+ }
+ }
+
+ /* A fresh nonce per operation is what makes the result non-replayable. If we
+ * cannot get one we must not fall back to anything weaker. */
+ if (getrandom (self->nonce, sizeof (self->nonce), 0) != (gssize) sizeof (self->nonce))
+ {
+ error = fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL,
+ "cannot obtain a nonce for the match");
+ if (action == FPI_DEVICE_ACTION_VERIFY)
+ fpi_device_verify_complete (device, g_steal_pointer (&error));
+ else
+ fpi_device_identify_complete (device, g_steal_pointer (&error));
+ return;
+ }
+ self->nonce_valid = TRUE;
+
+ memcpy (start.nonce, self->nonce, sizeof (start.nonce));
+
+ if (!aurora_ioctl (self, AURORA_BIO_VERIFY_START, &start, &error))
+ {
+ self->nonce_valid = FALSE;
+ if (action == FPI_DEVICE_ACTION_VERIFY)
+ fpi_device_verify_complete (device, g_steal_pointer (&error));
+ else
+ fpi_device_identify_complete (device, g_steal_pointer (&error));
+ return;
+ }
+
+ aurora_report_guidance (device, AURORA_BIO_GUIDANCE_PLACE, TRUE);
+ aurora_stop_watch (self);
+ self->watch_id = g_unix_fd_add (self->fd, G_IO_IN, aurora_verify_ready, device);
+}
+
+/* ------------------------------------------------------------------ */
+/* storage */
+/* ------------------------------------------------------------------ */
+
+static void
+aurora_list (FpDevice *device)
+{
+ FpiDeviceAurora *self = FPI_DEVICE_AURORA (device);
+ struct aurora_bio_list list = { 0 };
+ g_autoptr(GError) error = NULL;
+ GPtrArray *prints;
+ guint32 i;
+
+ if (!aurora_ioctl (self, AURORA_BIO_LIST, &list, &error))
+ {
+ fpi_device_list_complete (device, NULL, g_steal_pointer (&error));
+ return;
+ }
+
+ if (list.count > AURORA_BIO_MAX_IDENTITIES)
+ {
+ fpi_device_list_complete (device, NULL,
+ fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
+ "device reported %u identities, "
+ "more than the interface allows",
+ list.count));
+ return;
+ }
+
+ prints = g_ptr_array_new_with_free_func (g_object_unref);
+
+ for (i = 0; i < list.count; i++)
+ g_ptr_array_add (prints,
+ g_object_ref_sink (aurora_print_from_uuid (self,
+ list.id[i].uuid,
+ list.id[i].label)));
+
+ fpi_device_list_complete (device, prints, NULL);
+}
+
+static void
+aurora_delete (FpDevice *device)
+{
+ FpiDeviceAurora *self = FPI_DEVICE_AURORA (device);
+ struct aurora_bio_delete del = { 0 };
+ g_autoptr(GError) error = NULL;
+ FpPrint *print = NULL;
+
+ fpi_device_get_delete_data (device, &print);
+
+ if (print == NULL || !aurora_uuid_from_print (print, del.uuid, &error))
+ {
+ if (error == NULL)
+ error = fpi_device_error_new_msg (FP_DEVICE_ERROR_DATA_INVALID,
+ "no print to delete");
+ fpi_device_delete_complete (device, g_steal_pointer (&error));
+ return;
+ }
+
+ if (!aurora_ioctl (self, AURORA_BIO_DELETE, &del, &error))
+ {
+ fpi_device_delete_complete (device, g_steal_pointer (&error));
+ return;
+ }
+
+ fpi_device_delete_complete (device, NULL);
+}
+
+static void
+aurora_clear_storage (FpDevice *device)
+{
+ FpiDeviceAurora *self = FPI_DEVICE_AURORA (device);
+ g_autoptr(GError) error = NULL;
+
+ if (!aurora_ioctl (self, AURORA_BIO_DELETE_ALL, NULL, &error))
+ {
+ fpi_device_clear_storage_complete (device, g_steal_pointer (&error));
+ return;
+ }
+
+ fpi_device_clear_storage_complete (device, NULL);
+}
+
+/* ------------------------------------------------------------------ */
+/* class */
+/* ------------------------------------------------------------------ */
+
+static const FpIdEntry aurora_id_table[] = {
+ { .udev_types = FPI_DEVICE_UDEV_SUBTYPE_MISC, .misc_name = "sep-bio" },
+ { .udev_types = 0 }
+};
+
+static void
+fpi_device_aurora_init (FpiDeviceAurora *self)
+{
+ self->fd = -1;
+ self->watch_id = 0;
+ self->nonce_valid = FALSE;
+}
+
+static void
+fpi_device_aurora_finalize (GObject *object)
+{
+ FpiDeviceAurora *self = FPI_DEVICE_AURORA (object);
+
+ aurora_stop_watch (self);
+
+ if (self->fd >= 0)
+ {
+ close (self->fd);
+ self->fd = -1;
+ }
+
+ memset (self->nonce, 0, sizeof (self->nonce));
+
+ G_OBJECT_CLASS (fpi_device_aurora_parent_class)->finalize (object);
+}
+
+static void
+fpi_device_aurora_class_init (FpiDeviceAuroraClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ FpDeviceClass *dev_class = FP_DEVICE_CLASS (klass);
+
+ object_class->finalize = fpi_device_aurora_finalize;
+
+ dev_class->id = FP_COMPONENT;
+ dev_class->full_name = "Apple secure enclave fingerprint sensor";
+ dev_class->type = FP_DEVICE_TYPE_UDEV;
+ dev_class->id_table = aurora_id_table;
+ dev_class->scan_type = FP_SCAN_TYPE_PRESS;
+ dev_class->nr_enroll_stages = 8;
+ dev_class->temp_hot_seconds = -1;
+
+ dev_class->probe = aurora_probe;
+ dev_class->open = aurora_open;
+ dev_class->close = aurora_close;
+ dev_class->cancel = aurora_cancel;
+ dev_class->enroll = aurora_enroll;
+ dev_class->verify = aurora_match_start;
+ dev_class->identify = aurora_match_start;
+ dev_class->list = aurora_list;
+ dev_class->delete = aurora_delete;
+ dev_class->clear_storage = aurora_clear_storage;
+
+ fpi_device_class_auto_initialize_features (dev_class);
+
+ /* DUPLICATES_CHECK is deliberately NOT advertised.
+ *
+ * It tells libfprint the device can say whether a finger being enrolled is
+ * already enrolled, and libfprint implements that by running a full IDENTIFY
+ * pass immediately before every enrol. Matching is implemented, but the
+ * enclave's enrol result does not identify an already-enrolled finger in a
+ * way that satisfies libfprint's pre-enrol duplicate-check contract. Keep
+ * the feature unset until that narrower contract is implemented.
+ */
+}
diff --git a/libfprint/fp-context.c b/libfprint/fp-context.c
index 70d4062..6fdf53b 100644
--- a/libfprint/fp-context.c
+++ b/libfprint/fp-context.c
@@ -479,6 +479,7 @@ fp_context_enumerate (FpContext *context)
g_autoptr(GList) spidev_devices = g_udev_client_query_by_subsystem (udev_client, "spidev");
g_autoptr(GList) hidraw_devices = g_udev_client_query_by_subsystem (udev_client, "hidraw");
+ g_autoptr(GList) misc_devices = g_udev_client_query_by_subsystem (udev_client, "misc");
/* for each potential driver, try to match all requested resources. */
for (i = 0; i < priv->drivers->len; i++)
@@ -492,7 +493,7 @@ fp_context_enumerate (FpContext *context)
for (entry = cls->id_table; entry->udev_types; entry++)
{
- GList *matched_spidev = NULL, *matched_hidraw = NULL;
+ GList *matched_spidev = NULL, *matched_hidraw = NULL, *matched_misc = NULL;
if (entry->udev_types & FPI_DEVICE_UDEV_SUBTYPE_SPIDEV)
{
@@ -530,6 +531,20 @@ fp_context_enumerate (FpContext *context)
if (matched_hidraw == NULL)
continue;
}
+ if (entry->udev_types & FPI_DEVICE_UDEV_SUBTYPE_MISC)
+ {
+ for (matched_misc = misc_devices; matched_misc; matched_misc = matched_misc->next)
+ {
+ const gchar * name = g_udev_device_get_name (matched_misc->data);
+ if (!name || !entry->misc_name)
+ continue;
+ if (g_strcmp0 (name, entry->misc_name) == 0)
+ break;
+ }
+ /* If match was not found exit */
+ if (matched_misc == NULL)
+ continue;
+ }
priv->pending_devices++;
g_async_initable_new_async (driver,
G_PRIORITY_LOW,
@@ -539,6 +554,7 @@ fp_context_enumerate (FpContext *context)
"fpi-driver-data", entry->driver_data,
"fpi-udev-data-spidev", (matched_spidev ? g_udev_device_get_device_file (matched_spidev->data) : NULL),
"fpi-udev-data-hidraw", (matched_hidraw ? g_udev_device_get_device_file (matched_hidraw->data) : NULL),
+ "fpi-udev-data-misc", (matched_misc ? g_udev_device_get_device_file (matched_misc->data) : NULL),
NULL);
/* remove entries from list to avoid conflicts */
if (matched_spidev)
@@ -551,6 +567,11 @@ fp_context_enumerate (FpContext *context)
g_object_unref (matched_hidraw->data);
hidraw_devices = g_list_delete_link (hidraw_devices, matched_hidraw);
}
+ if (matched_misc)
+ {
+ g_object_unref (matched_misc->data);
+ misc_devices = g_list_delete_link (misc_devices, matched_misc);
+ }
}
}
diff --git a/libfprint/fp-device-private.h b/libfprint/fp-device-private.h
index 1c3702f..c8253c6 100644
--- a/libfprint/fp-device-private.h
+++ b/libfprint/fp-device-private.h
@@ -49,6 +49,7 @@ typedef struct
{
gchar *spidev_path;
gchar *hidraw_path;
+ gchar *misc_path;
} udev_data;
gboolean is_removed;
diff --git a/libfprint/fp-device.c b/libfprint/fp-device.c
index 115063d..70b10f1 100644
--- a/libfprint/fp-device.c
+++ b/libfprint/fp-device.c
@@ -53,6 +53,7 @@ enum {
PROP_FPI_USB_DEVICE,
PROP_FPI_UDEV_DATA_SPIDEV,
PROP_FPI_UDEV_DATA_HIDRAW,
+ PROP_FPI_UDEV_DATA_MISC,
PROP_FPI_DRIVER_DATA,
N_PROPS
};
@@ -237,6 +238,7 @@ fp_device_finalize (GObject *object)
g_clear_pointer (&priv->virtual_env, g_free);
g_clear_pointer (&priv->udev_data.spidev_path, g_free);
g_clear_pointer (&priv->udev_data.hidraw_path, g_free);
+ g_clear_pointer (&priv->udev_data.misc_path, g_free);
G_OBJECT_CLASS (fp_device_parent_class)->finalize (object);
}
@@ -307,6 +309,13 @@ fp_device_get_property (GObject *object,
g_value_set_string (value, NULL);
break;
+ case PROP_FPI_UDEV_DATA_MISC:
+ if (cls->type == FP_DEVICE_TYPE_UDEV)
+ g_value_set_string (value, priv->udev_data.misc_path);
+ else
+ g_value_set_string (value, NULL);
+ break;
+
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
@@ -353,6 +362,13 @@ fp_device_set_property (GObject *object,
g_assert (g_value_get_string (value) == NULL);
break;
+ case PROP_FPI_UDEV_DATA_MISC:
+ if (cls->type == FP_DEVICE_TYPE_UDEV)
+ priv->udev_data.misc_path = g_value_dup_string (value);
+ else
+ g_assert (g_value_get_string (value) == NULL);
+ break;
+
case PROP_FPI_DRIVER_DATA:
priv->driver_data = g_value_get_uint64 (value);
break;
@@ -583,6 +599,19 @@ fp_device_class_init (FpDeviceClass *klass)
"Private: The path to /dev/hidrawN",
NULL,
G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+ /**
+ * FpDevice::fpi-udev-data-misc: (skip)
+ *
+ * This property is only for internal purposes.
+ *
+ * Stability: private
+ */
+ properties[PROP_FPI_UDEV_DATA_MISC] =
+ g_param_spec_string ("fpi-udev-data-misc",
+ "Udev data: misc path",
+ "Private: The path to the misc character device",
+ NULL,
+ G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
/**
* FpDevice::fpi-driver-data: (skip)
diff --git a/libfprint/fpi-device.c b/libfprint/fpi-device.c
index 9500b3a..a70db5c 100644
--- a/libfprint/fpi-device.c
+++ b/libfprint/fpi-device.c
@@ -521,6 +521,9 @@ fpi_device_get_udev_data (FpDevice *device, FpiDeviceUdevSubtypeFlags subtype)
case FPI_DEVICE_UDEV_SUBTYPE_SPIDEV:
return priv->udev_data.spidev_path;
+ case FPI_DEVICE_UDEV_SUBTYPE_MISC:
+ return priv->udev_data.misc_path;
+
default:
g_return_val_if_reached (NULL);
return NULL;
diff --git a/libfprint/fpi-device.h b/libfprint/fpi-device.h
index b17c10d..7671ad6 100644
--- a/libfprint/fpi-device.h
+++ b/libfprint/fpi-device.h
@@ -28,12 +28,14 @@
* FpiDeviceUdevSubtypeFlags:
* @FPI_DEVICE_UDEV_SUBTYPE_SPIDEV: The device requires an spidev node
* @FPI_DEVICE_UDEV_SUBTYPE_HIDRAW: The device requires a hidraw node
+ * @FPI_DEVICE_UDEV_SUBTYPE_MISC: The device requires a misc character node
*
* Bitfield of required hardware resources for a udev-backed device.
*/
typedef enum {
FPI_DEVICE_UDEV_SUBTYPE_SPIDEV = 1 << 0,
FPI_DEVICE_UDEV_SUBTYPE_HIDRAW = 1 << 1,
+ FPI_DEVICE_UDEV_SUBTYPE_MISC = 1 << 2,
} FpiDeviceUdevSubtypeFlags;
/**
@@ -71,6 +73,7 @@ struct _FpIdEntry
guint pid;
guint vid;
} hid_id;
+ const gchar *misc_name;
};
};
guint64 driver_data;
diff --git a/libfprint/meson.build b/libfprint/meson.build
index f11533c..400c5b7 100644
--- a/libfprint/meson.build
+++ b/libfprint/meson.build
@@ -116,6 +116,7 @@ driver_sources = {
),
'etes603' : files('drivers/etes603.c'),
'egis0570' : files('drivers/egis0570.c'),
+ 'aurora' : files('drivers/aurora/aurora.c'),
'egismoc' : files('drivers/egismoc/egismoc.c'),
'egis_etu905' : files('drivers/egismoc/egis_etu905.c'),
'vfs0050' : files('drivers/vfs0050.c'),
diff --git a/meson.build b/meson.build
index ab09dc1..dfa7460 100644
--- a/meson.build
+++ b/meson.build
@@ -151,6 +151,9 @@ drivers_info = {
# SPI driver (non-optional if SPI is available)
'elanspi': { 'spi': true, 'helper': ['udev'], 'optional': not have_spi },
+ # Apple secure enclave, reached through a kernel misc device
+ 'aurora': { 'helper': ['udev'], 'optional': host_machine.system() != 'linux' },
+
# Virtual drivers (test-only, optional)
'virtual_image': { 'virtual': true, 'helper': ['virtual'], 'optional': true },
'virtual_device': { 'virtual': true, 'helper': ['virtual'], 'optional': true },