diff --git a/pkgbuilds/libfprint-git/PKGBUILD b/pkgbuilds/libfprint-git/PKGBUILD index 07fd2b9c..451912b4 100644 --- a/pkgbuilds/libfprint-git/PKGBUILD +++ b/pkgbuilds/libfprint-git/PKGBUILD @@ -6,15 +6,19 @@ # the pin here. Pinned to the commit adding the Synaptics reader USB # 06cb:010b, missing from stock libfprint 1.94.100; the FocalTech MOC support # carried by the previous pin is retained. +# +# On aarch64 it also carries the aurora driver for Apple Touch ID, which the +# aurora kernel's Secure Enclave driver exposes as /dev/sep-bio +# (aurora-apple-sep.patch, by Chris Kearney). The x86_64 build is unchanged. pkgname=libfprint-git pkgver=1.94.100.r10.g6f9479c -pkgrel=1 +pkgrel=2 # Retain the epoch used to supersede the previously published untested build. epoch=1 -pkgdesc="Library for fingerprint readers (pinned upstream snapshot with FocalTech and Synaptics 06cb:010b support)" +pkgdesc="Library for fingerprint readers (pinned upstream snapshot with FocalTech and Synaptics 06cb:010b support, and Apple Touch ID on aarch64)" url="https://fprint.freedesktop.org/" -arch=(x86_64) +arch=(x86_64 aarch64) license=(LGPL-2.1-or-later) depends=( gcc-libs @@ -39,17 +43,20 @@ checkdepends=( cairo umockdev ) -provides=(libfprint libfprint-2.so) +# Versioned, so a package that requires a libfprint version still installs. +provides=("libfprint=$pkgver" libfprint-2.so) conflicts=(libfprint) groups=(fprint) source=( "git+https://gitlab.freedesktop.org/libfprint/libfprint.git#commit=6f9479c3d55f847c1b3769f28ceb99227f9858cf" "focaltech_moc-add-pid-0xA97A.patch" ) +source_aarch64=("aurora-apple-sep.patch") b2sums=( 'SKIP' 'SKIP' ) +b2sums_aarch64=('SKIP') pkgver() { cd libfprint @@ -66,6 +73,10 @@ prepare() { if ! grep -qi 'pid = 0xa97a' libfprint/drivers/focaltech_moc/focaltech_moc.c; then patch -p1 < "$srcdir/focaltech_moc-add-pid-0xA97A.patch" fi + + if [[ $CARCH == "aarch64" && ! -f libfprint/drivers/aurora/aurora.c ]]; then + patch -p1 < "$srcdir/aurora-apple-sep.patch" + fi } build() { diff --git a/pkgbuilds/libfprint-git/aurora-apple-sep.patch b/pkgbuilds/libfprint-git/aurora-apple-sep.patch new file mode 100644 index 00000000..b1715c04 --- /dev/null +++ b/pkgbuilds/libfprint-git/aurora-apple-sep.patch @@ -0,0 +1,1238 @@ +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 ++#include ++ ++#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 ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++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 },