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 },