diff --git a/doc/libfprint-2-sections.txt b/doc/libfprint-2-sections.txt index 0fb0cfab..b582f2c2 100644 --- a/doc/libfprint-2-sections.txt +++ b/doc/libfprint-2-sections.txt @@ -100,6 +100,12 @@ FP_TYPE_IMAGE_DEVICE FpImageDevice +
+fp-sdcp-device +FP_TYPE_SDCP_DEVICE +FpSdcpDevice +
+
fp-print FP_TYPE_PRINT @@ -215,6 +221,26 @@ fpi_image_device_retry_scan fpi_image_device_set_bz3_threshold
+
+fpi-sdcp-device +Internal FpSdcpDevice +FpiSdcpClaim +FpSdcpDeviceClass +fpi_sdcp_claim_copy +fpi_sdcp_claim_free +fpi_sdcp_claim_get_type +fpi_sdcp_claim_new +fpi_sdcp_device_connect_complete +fpi_sdcp_device_get_connect_data +fpi_sdcp_device_get_reconnect_data +fpi_sdcp_device_reconnect_complete +fpi_sdcp_device_enroll_commit_complete +fpi_sdcp_device_enroll_ready +fpi_sdcp_device_enroll_set_nonce +fpi_sdcp_device_identify_retry +fpi_sdcp_device_identify_complete +
+
fpi-log fp_dbg diff --git a/doc/libfprint-docs.xml b/doc/libfprint-docs.xml index 0a57efb4..03eedb39 100644 --- a/doc/libfprint-docs.xml +++ b/doc/libfprint-docs.xml @@ -28,6 +28,7 @@ + @@ -38,6 +39,7 @@ Device methods for drivers + diff --git a/libfprint/fp-sdcp-device-private.h b/libfprint/fp-sdcp-device-private.h new file mode 100644 index 00000000..249c33c9 --- /dev/null +++ b/libfprint/fp-sdcp-device-private.h @@ -0,0 +1,58 @@ +/* + * FpSdcpDevice - A base class for SDCP enabled devices + * Copyright (C) 2020 Benjamin Berg + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include "fpi-sdcp-device.h" + +#include +#include +#include +#include + +typedef struct +{ + GError *enroll_pre_commit_error; + + /* XXX: Do we want a separate SDCP session object? + */ + + GPtrArray *intermediate_cas; + + /* Host random for the connection */ + guint8 host_random[32]; + + NSSInitContext *nss_init_context; + PK11SlotInfo *slot; + SECKEYPrivateKey *host_key_private; + SECKEYPublicKey *host_key_public; + + /* Master secret is required for reconnects. + * TODO: We probably want to serialize this to disk so it can survive + * fprintd idle shutdown. */ + PK11SymKey *master_secret; + PK11SymKey *mac_secret; + +} FpSdcpDevicePrivate; + +void fpi_sdcp_device_connect (FpSdcpDevice *self); +void fpi_sdcp_device_reconnect (FpSdcpDevice *self); + +void fpi_sdcp_device_enroll (FpSdcpDevice *self); +void fpi_sdcp_device_identify (FpSdcpDevice *self); diff --git a/libfprint/fp-sdcp-device.c b/libfprint/fp-sdcp-device.c new file mode 100644 index 00000000..4cdac4e5 --- /dev/null +++ b/libfprint/fp-sdcp-device.c @@ -0,0 +1,141 @@ +/* + * FpSdcpDevice - A base class for SDCP enabled devices + * Copyright (C) 2020 Benjamin Berg + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#define FP_COMPONENT "sdcp_device" +#include "fpi-log.h" + +#include "fp-sdcp-device-private.h" + +/** + * SECTION: fp-sdcp-device + * @title: FpSdcpDevice + * @short_description: SDCP device subclass + * + * This is a base class for devices implementing the SDCP security protocol. + */ + +G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (FpSdcpDevice, fp_sdcp_device, FP_TYPE_DEVICE) + +#if 0 +/* XXX: We'll very likely want/need some properties on this class. */ +enum { + PROP_0, + N_PROPS +}; + +static GParamSpec *properties[N_PROPS]; +#endif + +/*******************************************************/ + +/* Callbacks/vfuncs */ +static void +fp_sdcp_device_open (FpDevice *device) +{ + FpSdcpDevice *self = FP_SDCP_DEVICE (device); + FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + + /* Try a reconnect if we still have the mac secret. */ + if (priv->mac_secret) + fpi_sdcp_device_reconnect (self); + else + fpi_sdcp_device_connect (self); +} + +static void +fp_sdcp_device_enroll (FpDevice *device) +{ + FpSdcpDevice *self = FP_SDCP_DEVICE (device); + + fpi_sdcp_device_enroll (self); +} + +static void +fp_sdcp_device_identify (FpDevice *device) +{ + FpSdcpDevice *self = FP_SDCP_DEVICE (device); + + fpi_sdcp_device_identify (self); +} + +/*********************************************************/ + +static void +fp_sdcp_device_finalize (GObject *object) +{ + FpSdcpDevice *self = (FpSdcpDevice *) object; + FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + + g_clear_pointer (&priv->intermediate_cas, g_ptr_array_unref); + g_clear_pointer (&priv->slot, PK11_FreeSlot); + g_clear_pointer (&priv->host_key_private, SECKEY_DestroyPrivateKey); + g_clear_pointer (&priv->host_key_public, SECKEY_DestroyPublicKey); + g_clear_pointer (&priv->master_secret, PK11_FreeSymKey); + g_clear_pointer (&priv->mac_secret, PK11_FreeSymKey); + g_clear_pointer (&priv->nss_init_context, NSS_ShutdownContext); + + G_OBJECT_CLASS (fp_sdcp_device_parent_class)->finalize (object); +} + +static void +fp_sdcp_device_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + switch (prop_id) + { + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +fp_sdcp_device_constructed (GObject *obj) +{ + G_OBJECT_CLASS (fp_sdcp_device_parent_class)->constructed (obj); +} + +static void +fp_sdcp_device_class_init (FpSdcpDeviceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + FpDeviceClass *fp_device_class = FP_DEVICE_CLASS (klass); + + object_class->finalize = fp_sdcp_device_finalize; + object_class->get_property = fp_sdcp_device_get_property; + object_class->constructed = fp_sdcp_device_constructed; + + fp_device_class->open = fp_sdcp_device_open; + fp_device_class->enroll = fp_sdcp_device_enroll; + fp_device_class->verify = fp_sdcp_device_identify; + fp_device_class->identify = fp_sdcp_device_identify; + +#if 0 + g_object_class_install_properties (object_class, N_PROPS, properties); +#endif +} + +static void +fp_sdcp_device_init (FpSdcpDevice *self) +{ + FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + + priv->intermediate_cas = g_ptr_array_new_with_free_func ((GDestroyNotify) g_bytes_unref); +} diff --git a/libfprint/fp-sdcp-device.h b/libfprint/fp-sdcp-device.h new file mode 100644 index 00000000..cebe5a94 --- /dev/null +++ b/libfprint/fp-sdcp-device.h @@ -0,0 +1,29 @@ +/* + * FpSdcpDevice - A base class for SDCP enabled devices + * Copyright (C) 2020 Benjamin Berg + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define FP_TYPE_SDCP_DEVICE (fp_sdcp_device_get_type ()) +G_DECLARE_DERIVABLE_TYPE (FpSdcpDevice, fp_sdcp_device, FP, SDCP_DEVICE, FpDevice) + +G_END_DECLS diff --git a/libfprint/fpi-sdcp-device.c b/libfprint/fpi-sdcp-device.c new file mode 100644 index 00000000..999b474c --- /dev/null +++ b/libfprint/fpi-sdcp-device.c @@ -0,0 +1,1369 @@ +/* + * FpSdcpDevice - A base class for SDCP enabled devices + * Copyright (C) 2020 Benjamin Berg + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#define FP_COMPONENT "sdcp_device" +#include "fpi-log.h" + +#include +#include + +#include "fp-sdcp-device-private.h" +#include "fpi-sdcp-device.h" +#include "fpi-print.h" + +/** + * SECTION: fpi-sdcp-device + * @title: Internal FpSdcpDevice + * @short_description: Internal SDCP Device routines + * + * Internal SDCP handling routines. See #FpSdcpDevice for public routines. + */ + + +G_DEFINE_BOXED_TYPE (FpiSdcpClaim, fpi_sdcp_claim, fpi_sdcp_claim_copy, fpi_sdcp_claim_free) + +/** + * fpi_sdcp_claim_new: + * + * Create an empty #FpiSdcpClaim to provide to the base class. + * + * Returns: (transfer full): A newly created #FpiSdcpClaim + */ +FpiSdcpClaim * +fpi_sdcp_claim_new (void) +{ + FpiSdcpClaim *res = NULL; + + res = g_new0 (FpiSdcpClaim, 1); + + return res; +} + +/** + * fpi_sdcp_claim_free: + * @claim: a #FpiSdcpClaim + * + * Release the memory used by an #FpiSdcpClaim. + */ +void +fpi_sdcp_claim_free (FpiSdcpClaim * claim) +{ + g_return_if_fail (claim); + + g_clear_pointer (&claim->cert_m, g_bytes_unref); + g_clear_pointer (&claim->pk_d, g_bytes_unref); + g_clear_pointer (&claim->pk_f, g_bytes_unref); + g_clear_pointer (&claim->h_f, g_bytes_unref); + g_clear_pointer (&claim->s_m, g_bytes_unref); + g_clear_pointer (&claim->s_d, g_bytes_unref); + + g_free (claim); +} + +/** + * fpi_sdcp_claim_copy: + * @other: The #FpiSdcpClaim to copy + * + * Create a (shallow) copy of a #FpiSdcpClaim. + * + * Returns: (transfer full): A newly created #FpiSdcpClaim + */ +FpiSdcpClaim * +fpi_sdcp_claim_copy (FpiSdcpClaim *other) +{ + FpiSdcpClaim *res = NULL; + + res = fpi_sdcp_claim_new (); + + if (other->cert_m) + res->cert_m = g_bytes_ref (other->cert_m); + if (other->pk_d) + res->pk_d = g_bytes_ref (other->pk_d); + if (other->pk_f) + res->pk_f = g_bytes_ref (other->pk_f); + if (other->h_f) + res->h_f = g_bytes_ref (other->h_f); + if (other->s_m) + res->s_m = g_bytes_ref (other->s_m); + if (other->s_d) + res->s_d = g_bytes_ref (other->s_d); + + return res; +} + +static void +dump_bytes (GBytes *d) +{ + g_autoptr(GString) line = NULL; + const guint8 *dump_data; + gsize dump_len; + + dump_data = g_bytes_get_data (d, &dump_len); + + line = g_string_new (""); + /* Dump the buffer. */ + for (gint i = 0; i < dump_len; i++) + { + g_string_append_printf (line, "%02x ", dump_data[i]); + if ((i + 1) % 16 == 0) + { + g_debug ("%s", line->str); + g_string_set_size (line, 0); + } + } + + if (line->len) + g_debug ("%s", line->str); + +} + +/* Manually redefine what G_DEFINE_* macro does */ +static inline gpointer +fp_sdcp_device_get_instance_private (FpSdcpDevice *self) +{ + FpSdcpDeviceClass *sdcp_class = g_type_class_peek_static (FP_TYPE_SDCP_DEVICE); + + return G_STRUCT_MEMBER_P (self, + g_type_class_get_instance_private_offset (sdcp_class)); +} + +/** + * fpi_sdcp_kdf: + * @self: The #FpSdcpDevice + * @baseKey: The key to base it on + * @data_a: (nullable): First data segment to concatenate + * @data_b: (nullable): Second data segment to concatenate + * @out_key_2: (nullable) (out): Second output key or %NULL. + * @error: (out): #GError in case the return value is %NULL + * + * Convenience function to calculate a KDF with a specific label + * and up to two data segments that are concatinated. The returned + * keys will be 32bytes (256bit) in length. If @out_key_2 is set + * then two keys will be generated. + * + * Returns: A new #PK11SymKey of length @bitlength + **/ +static PK11SymKey * +fpi_sdcp_kdf (FpSdcpDevice *self, + PK11SymKey *baseKey, + const gchar *label, + GBytes *data_a, + GBytes *data_b, + PK11SymKey **out_key_2, + GError **error) +{ + PK11SymKey * res = NULL; + CK_SP800_108_KDF_PARAMS kdf_params; + CK_SP800_108_COUNTER_FORMAT counter_format; + CK_SP800_108_DKM_LENGTH_FORMAT length_format; + CK_DERIVED_KEY additional_key; + CK_ATTRIBUTE additional_key_attrs[2]; + CK_ULONG attr_type, attr_len; + CK_OBJECT_HANDLE out_key_handle = 0; + CK_PRF_DATA_PARAM data_param[5]; + SECItem params; + + kdf_params.prfType = CKM_SHA256_HMAC; + kdf_params.pDataParams = data_param; + + /* First item is the counter */ + counter_format.bLittleEndian = FALSE; + counter_format.ulWidthInBits = 32; + data_param[0].type = CK_SP800_108_ITERATION_VARIABLE; + data_param[0].pValue = &counter_format; + data_param[0].ulValueLen = sizeof (counter_format); + kdf_params.ulNumberOfDataParams = 1; + + /* Then the label */ + data_param[kdf_params.ulNumberOfDataParams].type = CK_SP800_108_BYTE_ARRAY; + data_param[kdf_params.ulNumberOfDataParams].pValue = (guint8 *) label; + data_param[kdf_params.ulNumberOfDataParams].ulValueLen = strlen (label) + 1; + kdf_params.ulNumberOfDataParams += 1; + + /* Then the context a */ + if (data_a) + { + data_param[kdf_params.ulNumberOfDataParams].type = CK_SP800_108_BYTE_ARRAY; + data_param[kdf_params.ulNumberOfDataParams].pValue = (guint8 *) g_bytes_get_data (data_a, NULL); + data_param[kdf_params.ulNumberOfDataParams].ulValueLen = g_bytes_get_size (data_a); + kdf_params.ulNumberOfDataParams += 1; + } + + /* Then the context b */ + if (data_b) + { + data_param[kdf_params.ulNumberOfDataParams].type = CK_SP800_108_BYTE_ARRAY; + data_param[kdf_params.ulNumberOfDataParams].pValue = (guint8 *) g_bytes_get_data (data_b, NULL); + data_param[kdf_params.ulNumberOfDataParams].ulValueLen = g_bytes_get_size (data_b); + kdf_params.ulNumberOfDataParams += 1; + } + + /* And the output length */ + length_format.dkmLengthMethod = CK_SP800_108_DKM_LENGTH_SUM_OF_KEYS; + length_format.bLittleEndian = FALSE; + length_format.ulWidthInBits = 32; + data_param[kdf_params.ulNumberOfDataParams].type = CK_SP800_108_DKM_LENGTH; + data_param[kdf_params.ulNumberOfDataParams].pValue = &length_format; + data_param[kdf_params.ulNumberOfDataParams].ulValueLen = sizeof (length_format); + kdf_params.ulNumberOfDataParams += 1; + + /* TODO: support a second key out (may be discarded) */ + kdf_params.ulAdditionalDerivedKeys = 0; + kdf_params.pAdditionalDerivedKeys = NULL; + if (out_key_2) + { + attr_type = CKK_SHA256_HMAC; + attr_len = 256 / 8; + additional_key_attrs[0].type = CKA_KEY_TYPE; + additional_key_attrs[0].pValue = &attr_type; + additional_key_attrs[0].ulValueLen = sizeof (attr_type); + additional_key_attrs[1].type = CKA_VALUE_LEN; + additional_key_attrs[1].pValue = &attr_len; + additional_key_attrs[1].ulValueLen = sizeof (attr_len); + + additional_key.pTemplate = additional_key_attrs; + additional_key.ulAttributeCount = 2; + additional_key.phKey = &out_key_handle; + + kdf_params.ulAdditionalDerivedKeys = 1; + kdf_params.pAdditionalDerivedKeys = &additional_key; + } + + params.len = sizeof (kdf_params); + params.data = (guint8 *) &kdf_params; + res = PK11_Derive (baseKey, + CKM_SP800_108_COUNTER_KDF, + ¶ms, + CKM_SHA256_HMAC, + CKA_SIGN, + 256 / 8); + if (!res) + { + g_propagate_error (error, + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Error deriving secret (label: %s): %d", + label, + PORT_GetError ())); + } + + if (out_key_2) + *out_key_2 = PK11_SymKeyFromHandle (PK11_GetSlotFromKey (baseKey), res, CKO_DATA, CKM_NULL, out_key_handle, FALSE, NULL); + + return res; +} + +/** + * fpi_sdcp_mac: + * @self: The #FpSdcpDevice + * @baseKey: The key to base it on + * @data_a: (nullable): Data segment a to concatenate + * @data_b: (nullable): Data segment b to concatenate + * @error: (out): #GError in case the return value is %NULL + * + * Convenience function to calculate a MAC with a specific label + * and a generic data segments that is concatenated. + * + * Returns: A new #PK11SymKey + **/ +static GBytes * +fpi_sdcp_mac (FpSdcpDevice *self, + const gchar *label, + GBytes *data_a, + GBytes *data_b, + GError **error) +{ + g_autoptr(GBytes) res = NULL; + FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + SECStatus r; + SECItem input, output; + g_autofree guint8 *data = NULL; + gsize label_len; + gsize data_a_len = 0; + gsize data_b_len = 0; + gsize length; + + label_len = strlen (label) + 1; + length = label_len; + if (data_a) + data_a_len = g_bytes_get_size (data_a); + if (data_b) + data_b_len = g_bytes_get_size (data_b); + + length += data_a_len + data_b_len; + + data = g_malloc (length); + + memcpy (data, label, label_len); + if (data_a) + memcpy (data + label_len, g_bytes_get_data (data_a, NULL), data_a_len); + if (data_b) + memcpy (data + label_len + data_a_len, g_bytes_get_data (data_b, NULL), data_b_len); + + input.len = length; + input.data = data; + output.len = 32; + output.data = g_malloc0 (32); + res = g_bytes_new_take (output.data, output.len); + + r = PK11_SignWithSymKey (priv->mac_secret, + CKM_SHA256_HMAC, + NULL, + &output, + &input); + if (r != SECSuccess) + { + g_propagate_error (error, + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Error calculating MAC (label: %s): %d", + label, + r)); + } + + return g_steal_pointer (&res); +} + +/* FpiSdcpDevice */ + +/* Internal functions of FpSdcpDevice */ +void +fpi_sdcp_device_connect (FpSdcpDevice *self) +{ + G_GNUC_UNUSED g_autofree void * ec_params_data = NULL; + FpSdcpDeviceClass *cls = FP_SDCP_DEVICE_GET_CLASS (self); + FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + SECOidData * oid_data; + SECKEYECParams ec_parameters; + SECStatus r = SECSuccess; + + /* Disable loading p11-kit's user configuration */ + g_setenv ("P11_KIT_NO_USER_CONFIG", "1", TRUE); + + /* Initialise NSS; Same as NSS_NoDB_Init but using a context. */ + if (!priv->nss_init_context) + { + priv->nss_init_context = NSS_InitContext ("", "", "", "", NULL, + NSS_INIT_READONLY | + NSS_INIT_NOCERTDB | + NSS_INIT_NOMODDB | + NSS_INIT_FORCEOPEN | + NSS_INIT_NOROOTINIT | + NSS_INIT_OPTIMIZESPACE); + } + if (!priv->nss_init_context) + goto nss_error; + + g_clear_pointer (&priv->slot, PK11_FreeSlot); + g_clear_pointer (&priv->host_key_private, SECKEY_DestroyPrivateKey); + g_clear_pointer (&priv->host_key_public, SECKEY_DestroyPublicKey); + + /* Create a slot for PK11 operation */ + priv->slot = PK11_GetBestSlot (CKM_EC_KEY_PAIR_GEN, NULL); + if (priv->slot == NULL) + goto nss_error; + + /* SDCP Connect: 3.i. Generate an ephemeral ECDH key pair */ + /* Look up the OID data for our curve. */ + oid_data = SECOID_FindOIDByTag (SEC_OID_SECG_EC_SECP256R1); + if (!oid_data) + goto nss_error; + + /* Copy into EC parameters */ + ec_parameters.len = oid_data->oid.len + 2; + ec_parameters.data = ec_params_data = g_malloc0 (oid_data->oid.len + 2); + ec_parameters.data[0] = SEC_ASN1_OBJECT_ID; + ec_parameters.data[1] = oid_data->oid.len; + memcpy (ec_parameters.data + 2, oid_data->oid.data, oid_data->oid.len); + + priv->host_key_private = PK11_GenerateKeyPair (priv->slot, CKM_EC_KEY_PAIR_GEN, + &ec_parameters, + &priv->host_key_public, + FALSE, TRUE, + NULL); + + if (!priv->host_key_private || !priv->host_key_public) + goto nss_error; + + /* SDCP Connect: 3.ii. Generate host random */ + r = PK11_GenerateRandom (priv->host_random, sizeof (priv->host_random)); + if (r != SECSuccess) + goto nss_error; + + /* SDCP Connect: 3.iii. Send the Connect message */ + cls->connect (self); + + return; + +nss_error: + if (r == SECSuccess) + r = PORT_GetError (); + fpi_sdcp_device_connect_complete (self, + NULL, NULL, NULL, + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Error calling NSS crypto routine: %d", r)); +} + +void +fpi_sdcp_device_reconnect (FpSdcpDevice *self) +{ + FpSdcpDeviceClass *cls = FP_SDCP_DEVICE_GET_CLASS (self); + FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + SECStatus r; + + /* SDCP Reconnect: 2.i. Generate host random */ + r = PK11_GenerateRandom (priv->host_random, sizeof (priv->host_random)); + if (r != SECSuccess) + { + fpi_sdcp_device_reconnect_complete (self, + NULL, + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Error calling NSS crypto routine: %d", r)); + } + + /* SDCP Reconnect: 2.ii. Send the Reconnect message */ + if (cls->reconnect) + cls->reconnect (self); + else + fpi_sdcp_device_connect (self); +} + +void +fpi_sdcp_device_enroll (FpSdcpDevice *self) +{ + FpSdcpDeviceClass *cls = FP_SDCP_DEVICE_GET_CLASS (self); + FpPrint *print; + + g_return_if_fail (FP_IS_SDCP_DEVICE (self)); + g_return_if_fail (fpi_device_get_current_action (FP_DEVICE (self)) == FPI_DEVICE_ACTION_ENROLL); + + fpi_device_get_enroll_data (FP_DEVICE (self), &print); + + fpi_print_set_device_stored (print, FALSE); + g_object_set (print, "fpi-data", NULL, NULL); + + /* For enrollment, all we need to do is start the process. But just to be sure, + * clear a bit of internal state. + */ + cls->enroll_begin (self); +} + +void +fpi_sdcp_device_identify (FpSdcpDevice *self) +{ + FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + FpSdcpDeviceClass *cls = FP_SDCP_DEVICE_GET_CLASS (self); + FpiDeviceAction action; + SECStatus r; + + g_return_if_fail (FP_IS_SDCP_DEVICE (self)); + action = fpi_device_get_current_action (FP_DEVICE (self)); + + g_return_if_fail (action == FPI_DEVICE_ACTION_IDENTIFY || action == FPI_DEVICE_ACTION_VERIFY); + + /* Generate a new nonce. */ + r = PK11_GenerateRandom (priv->host_random, sizeof (priv->host_random)); + if (r != SECSuccess) + { + fpi_device_action_error (FP_DEVICE (self), + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Error calling NSS crypto routine: %d", r)); + return; + } + + cls->identify (self); +} + +/*********************************************************/ +/* Private API */ + +/** + * fp_sdcp_device_set_intermediate_cas: + * @self: The #FpSdcpDevice + * @ca_1: (transfer none): DER encoded intermediate CA certificate #1 + * @ca_2: (transfer none): DER encoded intermediate CA certificate #2 + * + * Set the intermediate CAs used by the device. + */ +void +fpi_sdcp_device_set_intermediat_cas (FpSdcpDevice *self, + GBytes *ca_1, + GBytes *ca_2) +{ + FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + + g_ptr_array_set_size (priv->intermediate_cas, 0); + if (ca_1) + g_ptr_array_add (priv->intermediate_cas, g_bytes_ref (ca_1)); + if (ca_2) + g_ptr_array_add (priv->intermediate_cas, g_bytes_ref (ca_2)); +} + +/* FIXME: This is (transfer full), but other getters have (transfer none) +* for drivers. Kind of inconsistent, but it is convenient here. */ +/** + * fp_sdcp_device_get_connect_data: + * @r_h: (out) (transfer full): The host random + * @pk_h: (out) (transfer full): The host public key + * + * Get data required to connect to (i.e. open) the device securely. + */ +void +fpi_sdcp_device_get_connect_data (FpSdcpDevice *self, + GBytes **r_h, + GBytes **pk_h) +{ + FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + + g_return_if_fail (r_h != NULL); + g_return_if_fail (pk_h != NULL); + + *r_h = g_bytes_new (priv->host_random, sizeof (priv->host_random)); + + g_assert (priv->host_key_public->u.ec.publicValue.len == 65); + *pk_h = g_bytes_new (priv->host_key_public->u.ec.publicValue.data, priv->host_key_public->u.ec.publicValue.len); +} + +/** + * fp_sdcp_device_get_reconnect_data: + * @r_h: (out) (transfer full): The host random + * + * Get data required to reconnect to (i.e. open) to the device securely. + */ +void +fpi_sdcp_device_get_reconnect_data (FpSdcpDevice *self, + GBytes **r_h) +{ + FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + + g_return_if_fail (r_h != NULL); + + *r_h = g_bytes_new (priv->host_random, sizeof (priv->host_random)); +} + +/** + * fp_sdcp_device_get_identify_data: + * @r_h: (out) (transfer full): The host random + * + * Get data required to identify a new print. + */ +void +fpi_sdcp_device_get_identify_data (FpSdcpDevice *self, + GBytes **nonce) +{ + FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + + g_return_if_fail (nonce != NULL); + + *nonce = g_bytes_new (priv->host_random, sizeof (priv->host_random)); +} + +/* Returns the certificates public key after validation. */ +static SECKEYPublicKey * +fpi_sdcp_validate_cert (FpSdcpDevice *self, + FpiSdcpClaim *claim, + GError **error) +{ + FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + CERTValInParam in_params[1] = { 0, }; + CERTValOutParam out_params[2] = { 0, }; + const void *cert_m_data; + gsize cert_m_length; + CERTCertDBHandle *cert_db = NULL; + CERTCertificate *cert_m = NULL; + + g_autoptr(GPtrArray) intermediate_cas = NULL; + PLArenaPool *res_arena = NULL; + SECKEYPublicKey *res = NULL; + SECStatus r; + gint i; + + g_debug ("cert_m:"); + dump_bytes (claim->cert_m); + cert_m_data = g_bytes_get_data (claim->cert_m, &cert_m_length); + cert_m = CERT_DecodeCertFromPackage ((char *) cert_m_data, cert_m_length); + if (!cert_m) + { + /* So, the MS test client we use for the virtual-sdcp driver does not return + * a certificate (yeah ... why?). This special case is purely for testing + * purposes and should be removed by fixing the test client! + */ + if (g_str_equal (fp_device_get_driver (FP_DEVICE (self)), "virtual_sdcp") && cert_m_length == 65) + { + /* Create a new public key directly from the buffer rather than from the certificate. */ + res_arena = PORT_NewArena (DER_DEFAULT_CHUNKSIZE); + g_assert (res_arena); + + res = (SECKEYPublicKey *) PORT_ArenaZAlloc (res_arena, sizeof (SECKEYPublicKey)); + g_assert (res); + + res->arena = res_arena; + res->pkcs11Slot = 0; + res->pkcs11ID = CK_INVALID_HANDLE; + + res->keyType = priv->host_key_public->keyType; + res->u.ec.DEREncodedParams = priv->host_key_public->u.ec.DEREncodedParams; + res->u.ec.size = priv->host_key_public->u.ec.size; + res->u.ec.publicValue.len = 65; + res->u.ec.publicValue.type = priv->host_key_public->u.ec.publicValue.type; + res->u.ec.publicValue.data = (guint8 *) cert_m_data; + + goto out; + } + + g_propagate_error (error, + fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, + "Failed to read cert_m: %d", PORT_GetError ())); + goto out; + } + +#if 0 + /* The following code would be a better way of specifying the intermediate CAs + * (instead of inserting them into the certificate store), but it does not + * work because the feature has simply not been implemented in PKIX. + * The code here is left purely as a reference and warning. + */ + CERTCertList *intermediate_cas = NULL; + + /* Setup list for the intermediate CAs. */ + intermediate_cas = CERT_NewCertList (); + for (i = 0; i < priv->intermediate_cas->len; i++) + { + CERTCertificate *cert = NULL; + const void *data; + gsize length; + + data = g_bytes_get_data ((GBytes *) g_ptr_array_index (priv->intermediate_cas, i), + &length); + cert = CERT_DecodeCertFromPackage ((char *) data, length); + if (!cert) + { + g_propagate_error (error, + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Failed to read intermediate cert: %d", PORT_GetError ())); + goto out; + } + /* Adding takes the reference. */ + r = CERT_AddCertToListTail (intermediate_cas, cert); + + if (r != SECSuccess) + { + g_propagate_error (error, + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Failed to add cert to cert list: %d", r)); + goto out; + } + } +#endif + + /* Import intermediate certificates into cert DB. */ + cert_db = CERT_GetDefaultCertDB (); + if (!cert_db) + { + g_propagate_error (error, + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "No default certificate DB!")); + goto out; + } + + intermediate_cas = g_ptr_array_new_full (priv->intermediate_cas->len, g_free); + for (i = 0; i < priv->intermediate_cas->len; i++) + { + gsize length; + SECItem *item = NULL; + + item = g_new0 (SECItem, 1); + item->type = siDERCertBuffer; + item->data = (guint8 *) g_bytes_get_data ((GBytes *) g_ptr_array_index (priv->intermediate_cas, i), + &length); + item->len = length; + g_ptr_array_add (intermediate_cas, item); + } + r = CERT_ImportCerts (cert_db, certUsageVerifyCA, + intermediate_cas->len, (SECItem **) intermediate_cas->pdata, + NULL, + FALSE, FALSE, NULL); + if (r != SECSuccess) + { + g_propagate_error (error, + fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, + "Failed to import intermediate CAs: %d", PORT_GetError ())); + goto out; + } + + /* We assume we have the root CA in the system store already. */ + in_params[0].type = cert_pi_end; + + out_params[0].type = cert_po_end; // cert_po_errorLog; + out_params[0].value.pointer.log = NULL; + out_params[1].type = cert_po_end; + + r = CERT_PKIXVerifyCert (cert_m, + certUsageAnyCA, /* XXX: is this correct? */ + in_params, + out_params, + NULL); + if (r != SECSuccess) + { + g_propagate_error (error, + fpi_device_error_new_msg (FP_DEVICE_ERROR_UNTRUSTED, + "Failed to verify device certificate: %d", PORT_GetError ())); + goto out; + } + + /* All seems good, extract the public key in order to return it. */ + res = CERT_ExtractPublicKey (cert_m); + if (!res) + { + g_propagate_error (error, + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Failed to extract public key from certificate: %d", PORT_GetError ())); + goto out; + } + +out: + g_clear_pointer (&cert_m, CERT_DestroyCertificate); + if (out_params[0].value.pointer.log) + PORT_FreeArena (out_params[0].value.pointer.log->arena, FALSE); + return res; +} + +/* FIXME: How to provide intermediate CAs provided? Same call or separate channel? */ +/** + * fpi_sdcp_device_connect_complete: + * @self: a #FpSdcpDevice fingerprint device + * @r_d: The device random nonce + * @claim: The device #FpiSdcpClaim + * @mac: The MAC authenticating @claim + * @error: A #GError or %NULL on success + * + * Reports completion of connect (i.e. open) operation. + */ +void +fpi_sdcp_device_connect_complete (FpSdcpDevice *self, + GBytes *r_d, + FpiSdcpClaim *claim, + GBytes *mac, + GError *error) +{ + g_autoptr(GBytes) r_h = NULL; + g_autoptr(GBytes) claim_hash_bytes = NULL; + g_autoptr(GBytes) claim_mac = NULL; + FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + SECKEYPublicKey firmware_key_public = { 0, }; + SECKEYPublicKey device_key_public = { 0, }; + SECKEYPublicKey *model_key_public = NULL; + HASHContext *hash_ctx; + guint8 hash_out[SHA256_LENGTH]; + guint hash_len = 0; + FpiDeviceAction action; + PK11SymKey *a = NULL; + PK11SymKey *enc_secret = NULL; + gsize length; + SECItem sig, hash; + SECStatus r; + + action = fpi_device_get_current_action (FP_DEVICE (self)); + + g_return_if_fail (action == FPI_DEVICE_ACTION_OPEN); + + if (error) + { + if (r_d || claim || mac) + { + g_warning ("Driver provided connect information but also reported error."); + g_clear_pointer (&r_d, g_bytes_unref); + g_clear_pointer (&claim, fpi_sdcp_claim_free); + g_clear_pointer (&mac, g_bytes_unref); + } + + fpi_device_open_complete (FP_DEVICE (self), error); + return; + } + + if (!r_d || !claim || !mac || + (!claim->cert_m || !claim->pk_d || !claim->pk_f || !claim->h_f || !claim->s_m || !claim->s_d)) + { + g_warning ("Driver did not provide all required information to callback, returning error instead."); + g_clear_pointer (&r_d, g_bytes_unref); + g_clear_pointer (&claim, fpi_sdcp_claim_free); + g_clear_pointer (&mac, g_bytes_unref); + + fpi_device_open_complete (FP_DEVICE (self), + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Driver called connect complete with incomplete arguments.")); + return; + } + + /* Device key is of same type as host key */ + g_assert (g_bytes_get_size (claim->pk_f) == 65); + firmware_key_public.keyType = priv->host_key_public->keyType; + firmware_key_public.u.ec.DEREncodedParams = priv->host_key_public->u.ec.DEREncodedParams; + firmware_key_public.u.ec.size = priv->host_key_public->u.ec.size; + firmware_key_public.u.ec.publicValue.len = 65; + firmware_key_public.u.ec.publicValue.type = priv->host_key_public->u.ec.publicValue.type; + firmware_key_public.u.ec.publicValue.data = (guint8 *) g_bytes_get_data (claim->pk_f, NULL); + + /* SDCP Connect: 5.i. Perform key agreement */ + a = PK11_PubDeriveWithKDF (priv->host_key_private, + &firmware_key_public, + TRUE, + NULL, + NULL, + CKM_ECDH1_DERIVE, + CKM_SP800_108_COUNTER_KDF, + CKA_DERIVE, + 32, /* 256 bit (HMAC) secret */ + CKD_NULL, + NULL, + NULL); + + if (!a) + { + error = fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Error doing key agreement: %d", PORT_GetError ()); + goto out; + } + + /* SDCP Connect: 5.ii. Derive master secret */ + g_clear_pointer (&priv->master_secret, PK11_FreeSymKey); + + r_h = g_bytes_new (priv->host_random, sizeof (priv->host_random)); + + priv->master_secret = fpi_sdcp_kdf (self, + a, + "master secret", + r_h, + r_d, + NULL, + &error); + if (!priv->master_secret) + goto out; + + /* SDCP Connect: 5.iii. Derive MAC secret and symetric key */ + + /* NOTE: symetric key is never used, as such we just don't derive it! */ + g_clear_pointer (&priv->mac_secret, PK11_FreeSymKey); + priv->mac_secret = fpi_sdcp_kdf (self, + priv->master_secret, + "application keys", + NULL, + NULL, + &enc_secret, + &error); + if (!priv->mac_secret) + goto out; + + /* SDCP Connect: 5.iv. Validate the MAC over H(claim) */ + hash_ctx = HASH_Create (HASH_AlgSHA256); + if (!hash_ctx) + { + error = fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Could not create hash context"); + goto out; + } + HASH_Begin (hash_ctx); + HASH_Update (hash_ctx, g_bytes_get_data (claim->cert_m, NULL), g_bytes_get_size (claim->cert_m)); + HASH_Update (hash_ctx, g_bytes_get_data (claim->pk_d, NULL), g_bytes_get_size (claim->pk_d)); + HASH_Update (hash_ctx, g_bytes_get_data (claim->pk_f, NULL), g_bytes_get_size (claim->pk_f)); + HASH_Update (hash_ctx, g_bytes_get_data (claim->h_f, NULL), g_bytes_get_size (claim->h_f)); + HASH_Update (hash_ctx, g_bytes_get_data (claim->s_m, NULL), g_bytes_get_size (claim->s_m)); + HASH_Update (hash_ctx, g_bytes_get_data (claim->s_d, NULL), g_bytes_get_size (claim->s_d)); + HASH_End (hash_ctx, hash_out, &hash_len, sizeof (hash_out)); + g_clear_pointer (&hash_ctx, HASH_Destroy); + g_assert (hash_len == sizeof (hash_out)); + + claim_hash_bytes = g_bytes_new (hash_out, sizeof (hash_out)); + g_debug ("H(c):"); + dump_bytes (claim_hash_bytes); + + claim_mac = fpi_sdcp_mac (self, "connect", claim_hash_bytes, NULL, &error); + if (!claim_mac) + goto out; + + g_debug ("MAC(s, \"connect\"||H(c)):"); + dump_bytes (claim_mac); + + if (!g_bytes_equal (mac, claim_mac)) + { + error = fpi_device_error_new_msg (FP_DEVICE_ERROR_UNTRUSTED, + "Device MAC over H(c) is incorrect."); + goto out; + } + + /* SDCP Connect: 5.v. Unpack the claim (SKIP, already done) */ + /* SDCP Connect: 5.vi. Verify claim */ + + /* First, validate the certificate (and return its public key). */ + model_key_public = fpi_sdcp_validate_cert (self, claim, &error); + if (!model_key_public) + goto out; + + /* Verify(pk_m, H(pk_d), s_m) */ + sig.data = (guint8 *) g_bytes_get_data (claim->s_m, &length); + sig.len = length; + memset (hash_out, 0, sizeof (hash_out)); + r = PK11_HashBuf (HASH_GetHashOidTagByHashType (HASH_AlgSHA256), + hash_out, + g_bytes_get_data (claim->pk_d, NULL), + g_bytes_get_size (claim->pk_d)); + if (r != SECSuccess) + { + error = fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Failed to hash device public key!"); + goto out; + } + + hash.data = hash_out; + hash.len = sizeof (hash_out); + r = PK11_Verify (model_key_public, &sig, &hash, NULL); + if (r != SECSuccess) + { + error = fpi_device_error_new_msg (FP_DEVICE_ERROR_UNTRUSTED, + "Verification of device public key failed: %d", PORT_GetError ()); + goto out; + } + + device_key_public.keyType = priv->host_key_public->keyType; + device_key_public.u.ec.DEREncodedParams = priv->host_key_public->u.ec.DEREncodedParams; + device_key_public.u.ec.size = priv->host_key_public->u.ec.size; + device_key_public.u.ec.publicValue.len = 65; + device_key_public.u.ec.publicValue.type = priv->host_key_public->u.ec.publicValue.type; + device_key_public.u.ec.publicValue.data = (guint8 *) g_bytes_get_data (claim->pk_d, NULL); + + /* Verify(pk_d, H(C001||h_f||pk_f), s_d) */ + sig.data = (guint8 *) g_bytes_get_data (claim->s_d, &length); + sig.len = length; + + hash_ctx = HASH_Create (HASH_AlgSHA256); + if (!hash_ctx) + { + error = fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Could not create hash context"); + goto out; + } + HASH_Begin (hash_ctx); + HASH_Update (hash_ctx, (guint8 *) "\xC0\x01", 2); + HASH_Update (hash_ctx, g_bytes_get_data (claim->h_f, NULL), g_bytes_get_size (claim->h_f)); + HASH_Update (hash_ctx, g_bytes_get_data (claim->pk_f, NULL), g_bytes_get_size (claim->pk_f)); + HASH_End (hash_ctx, hash_out, &hash_len, sizeof (hash_out)); + g_clear_pointer (&hash_ctx, HASH_Destroy); + g_assert (hash_len == sizeof (hash_out)); + + hash.data = hash_out; + hash.len = sizeof (hash_out); + r = PK11_Verify (&device_key_public, &sig, &hash, NULL); + if (r != SECSuccess) + { + error = fpi_device_error_new_msg (FP_DEVICE_ERROR_UNTRUSTED, + "Verification of boot process failed: %d", PORT_GetError ()); + goto out; + } + + /* XXX/FIXME: We should be checking H(f) against a list of compromised firmwares. + * We would need a way to distribute and load it though. + */ + +out: + g_clear_pointer (&a, PK11_FreeSymKey); + g_clear_pointer (&enc_secret, PK11_FreeSymKey); + g_clear_pointer (&model_key_public, SECKEY_DestroyPublicKey); + + if (error) + g_clear_pointer (&priv->mac_secret, PK11_FreeSymKey); + + fpi_device_open_complete (FP_DEVICE (self), error); +} + +/** + * fpi_sdcp_device_reconnect_complete: + * @self: a #FpSdcpDevice fingerprint device + * @mac: The MAC authenticating @claim + * @error: A #GError or %NULL on success + * + * Reports completion of a reconnect (i.e. open) operation. + */ +void +fpi_sdcp_device_reconnect_complete (FpSdcpDevice *self, + GBytes *mac, + GError *error) +{ + g_autoptr(GError) err = NULL; + FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + FpiDeviceAction action; + + action = fpi_device_get_current_action (FP_DEVICE (self)); + + g_return_if_fail (action == FPI_DEVICE_ACTION_OPEN); + + if (error) + { + if (mac) + { + g_warning ("Driver provided a MAC but also reported an error."); + g_bytes_unref (mac); + } + + /* Silently try a normal connect instead. */ + fpi_sdcp_device_connect (self); + } + else if (mac) + { + g_autoptr(GBytes) mac_verify = NULL; + g_autoptr(GBytes) host_random = NULL; + + /* We got a MAC, so we can check whether the device + * still agrees with us on the shared secret. */ + host_random = g_bytes_new (priv->host_random, sizeof (priv->host_random)); + mac_verify = fpi_sdcp_mac (self, "reconnect", host_random, NULL, &err); + if (!mac_verify) + { + fpi_device_open_complete (FP_DEVICE (self), g_steal_pointer (&err)); + return; + } + + if (g_bytes_equal (mac, mac_verify)) + { + g_debug ("Reconnect succeeded"); + fpi_device_open_complete (FP_DEVICE (self), NULL); + } + else + { + g_message ("Fast reconnect with SDCP device failed, doing a full connect."); + fpi_sdcp_device_connect (self); + } + } + else + { + fpi_device_open_complete (FP_DEVICE (self), + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Driver called reconnect complete with wrong arguments.")); + } +} + +/** + * fpi_sdcp_device_enroll_set_nonce: + * @self: a #FpSdcpDevice fingerprint device + * @nonce: The device generated nonce + * + * Called during enroll to inform the SDCP base class about the nonce + * that the device chose. This can be called at any point, but must be + * called before calling fpi_sdcp_device_enroll_ready(). + */ +void +fpi_sdcp_device_enroll_set_nonce (FpSdcpDevice *self, + GBytes *nonce) +{ + g_autoptr(GBytes) id = NULL; + GVariant *id_var; + FpPrint *print; + GVariant *data; + + g_return_if_fail (FP_IS_SDCP_DEVICE (self)); + g_return_if_fail (fpi_device_get_current_action (FP_DEVICE (self)) == FPI_DEVICE_ACTION_ENROLL); + + g_return_if_fail (nonce || g_bytes_get_size (nonce) != 32); + + fpi_device_get_enroll_data (FP_DEVICE (self), &print); + + id = fpi_sdcp_mac (self, "enroll", nonce, NULL, NULL); + if (!id) + { + g_warning ("Could not generate enroll MAC"); + return; + } + + id_var = g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE, + g_bytes_get_data (id, NULL), + g_bytes_get_size (id), + 1); + data = g_variant_new ("(@ay)", id_var); + + /* Set to true once committed */ + fpi_print_set_device_stored (print, FALSE); + + /* Attach the ID to the print */ + g_object_set (print, "fpi-data", data, NULL); +} + +/** + * fpi_sdcp_device_enroll_ready: + * @self: a #FpSdcpDevice fingerprint device + * @error: a #GError or %NULL on success + * + * Called when the print is ready to be committed to device memory. + * For each enroll step, fpi_device_enroll_progress() must first + * be called until the enroll is ready to be committed. + */ +void +fpi_sdcp_device_enroll_ready (FpSdcpDevice *self, + GError *error) +{ + g_autoptr(GVariant) data = NULL; + g_autoptr(GVariant) id_var = NULL; + g_autoptr(GBytes) id = NULL; + FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + FpSdcpDeviceClass *cls = FP_SDCP_DEVICE_GET_CLASS (self); + FpPrint *print; + + g_return_if_fail (FP_IS_SDCP_DEVICE (self)); + g_return_if_fail (fpi_device_get_current_action (FP_DEVICE (self)) == FPI_DEVICE_ACTION_ENROLL); + + fpi_device_get_enroll_data (FP_DEVICE (self), &print); + + if (error) + { + fpi_device_enroll_complete (FP_DEVICE (self), NULL, error); + g_object_set (print, "fpi-data", NULL, NULL); + return; + } + + /* TODO: The following will need to ensure that the ID has been generated */ + + g_object_get (G_OBJECT (print), "fpi-data", &data, NULL); + + if (data) + { + const guint8 *id_data; + gsize id_len; + + g_variant_get (data, + "(@ay)", + &id_var); + + id_data = g_variant_get_fixed_array (id_var, &id_len, 1); + id = g_bytes_new (id_data, id_len); + } + + g_debug ("ID/enroll mac:"); + dump_bytes (id); + + if (!id) + { + g_warning ("Driver failed to call fpi_sdcp_device_enroll_set_nonce, aborting enroll."); + + /* NOTE: Cancel the enrollment, i.e. don't commit */ + priv->enroll_pre_commit_error = fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, + "Device/driver did not provide a nonce as required by protocol, aborting enroll!"); + cls->enroll_commit (self, NULL); + } + else + { + cls->enroll_commit (self, g_steal_pointer (&id)); + } +} + +/** + * fpi_sdcp_device_enroll_commit_complete: + * @self: a #FpSdcpDevice fingerprint device + * + * Called when device has committed the given print to memory. + * This finalizes the enroll operation. + */ +void +fpi_sdcp_device_enroll_commit_complete (FpSdcpDevice *self, + GError *error) +{ + g_autoptr(GVariant) data = NULL; + FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + FpPrint *print; + + g_return_if_fail (FP_IS_SDCP_DEVICE (self)); + g_return_if_fail (fpi_device_get_current_action (FP_DEVICE (self)) == FPI_DEVICE_ACTION_ENROLL); + + if (priv->enroll_pre_commit_error) + { + if (error) + { + g_warning ("Cancelling enroll after error failed with: %s", error->message); + g_error_free (error); + } + + fpi_device_enroll_complete (FP_DEVICE (self), + NULL, + g_steal_pointer (&priv->enroll_pre_commit_error)); + return; + } + + if (error) + { + fpi_device_enroll_complete (FP_DEVICE (self), NULL, error); + return; + } + + fpi_device_get_enroll_data (FP_DEVICE (self), &print); + g_object_get (G_OBJECT (print), "fpi-data", &data, NULL); + if (!data) + { + g_error ("Inconsistent state, the print must have the enrolled ID attached at this point"); + return; + } + + fpi_print_set_type (print, FPI_PRINT_SDCP); + fpi_print_set_device_stored (print, TRUE); + + fpi_device_enroll_complete (FP_DEVICE (self), g_object_ref (print), NULL); +} + +/** + * fpi_sdcp_device_identify_retry: + * @self: a #FpSdcpDevice fingerprint device + * @error: a #GError containing the retry condition + * + * Called when the device requires the finger to be presented again. + * This should not be called for a verified no-match, it should only + * be called if e.g. the finger was not centered properly or similar. + * + * Effectively this simply raises the error up. This function exists + * to bridge the difference in semantics that SDPC has from how + * libfprint works internally. + */ +void +fpi_sdcp_device_identify_retry (FpSdcpDevice *self, + GError *error) +{ + FpiDeviceAction action; + + g_return_if_fail (FP_IS_SDCP_DEVICE (self)); + action = fpi_device_get_current_action (FP_DEVICE (self)); + + g_return_if_fail (action == FPI_DEVICE_ACTION_IDENTIFY || action == FPI_DEVICE_ACTION_VERIFY); + + if (action == FPI_DEVICE_ACTION_VERIFY) + fpi_device_verify_report (FP_DEVICE (self), FPI_MATCH_ERROR, NULL, error); + else if (action == FPI_DEVICE_ACTION_IDENTIFY) + fpi_device_identify_report (FP_DEVICE (self), NULL, NULL, error); +} + +/** + * fpi_sdcp_device_identify_complete: + * @self: a #FpSdcpDevice fingerprint device + * @id: (transfer none): the ID as reported by the device + * @mac: (transfer none): MAC authenticating the message + * @error: (transfer full): #GError if an error occured + * + * Called when device is done with the identification routine. The + * returned ID may be %NULL if none of the in-device templates matched. + */ +void +fpi_sdcp_device_identify_complete (FpSdcpDevice *self, + GBytes *id, + GBytes *mac, + GError *error) +{ + g_autoptr(GBytes) mac_verify = NULL; + g_autoptr(GBytes) host_random = NULL; + FpSdcpDevicePrivate *priv = fp_sdcp_device_get_instance_private (self); + GError *err = NULL; + FpPrint *identified_print; + GVariant *id_var; + GVariant *data; + FpiDeviceAction action; + + g_return_if_fail (FP_IS_SDCP_DEVICE (self)); + action = fpi_device_get_current_action (FP_DEVICE (self)); + + g_return_if_fail (action == FPI_DEVICE_ACTION_IDENTIFY || action == FPI_DEVICE_ACTION_VERIFY); + + if (error) + { + fpi_device_action_error (FP_DEVICE (self), error); + return; + } + + if (!id || !mac || g_bytes_get_size (id) != 32 || g_bytes_get_size (mac) != 32) + { + fpi_device_action_error (FP_DEVICE (self), + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Driver returned incorrect ID/MAC for identify operation")); + return; + } + + host_random = g_bytes_new (priv->host_random, sizeof (priv->host_random)); + mac_verify = fpi_sdcp_mac (self, "identify", host_random, id, &err); + if (!mac_verify) + { + fpi_device_action_error (FP_DEVICE (self), + err); + return; + } + + if (!g_bytes_equal (mac, mac_verify)) + { + fpi_device_action_error (FP_DEVICE (self), + fpi_device_error_new_msg (FP_DEVICE_ERROR_UNTRUSTED, + "Reported match from the device cannot be trusted!")); + return; + } + + /* Create a new print */ + identified_print = fp_print_new (FP_DEVICE (self)); + + fpi_print_set_type (identified_print, FPI_PRINT_SDCP); + fpi_print_set_device_stored (identified_print, TRUE); + + id_var = g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE, + g_bytes_get_data (id, NULL), + g_bytes_get_size (id), + 1); + data = g_variant_new ("(@ay)", id_var); + + /* Set to true once committed */ + fpi_print_set_device_stored (identified_print, FALSE); + + /* Attach the ID to the print */ + g_object_set (identified_print, "fpi-data", data, NULL); + + + /* The surrounding API expects a match/no-match against a given set. */ + if (action == FPI_DEVICE_ACTION_VERIFY) + { + FpPrint *print; + + fpi_device_get_verify_data (FP_DEVICE (self), &print); + + if (fp_print_equal (print, identified_print)) + fpi_device_verify_report (FP_DEVICE (self), FPI_MATCH_SUCCESS, identified_print, NULL); + else + fpi_device_verify_report (FP_DEVICE (self), FPI_MATCH_FAIL, identified_print, NULL); + + fpi_device_verify_complete (FP_DEVICE (self), NULL); + } + else + { + GPtrArray *prints; + gint i; + + fpi_device_get_identify_data (FP_DEVICE (self), &prints); + + for (i = 0; i < prints->len; i++) + { + FpPrint *print = g_ptr_array_index (prints, i); + + if (fp_print_equal (print, identified_print)) + { + fpi_device_identify_report (FP_DEVICE (self), print, identified_print, NULL); + fpi_device_identify_complete (FP_DEVICE (self), NULL); + return; + } + } + + /* Print wasn't in database. */ + fpi_device_identify_report (FP_DEVICE (self), NULL, identified_print, NULL); + fpi_device_identify_complete (FP_DEVICE (self), NULL); + } +} diff --git a/libfprint/fpi-sdcp-device.h b/libfprint/fpi-sdcp-device.h new file mode 100644 index 00000000..d5a4fcf7 --- /dev/null +++ b/libfprint/fpi-sdcp-device.h @@ -0,0 +1,142 @@ +/* + * FpSdcpDevice - A base class for SDCP enabled devices + * Copyright (C) 2020 Benjamin Berg + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include +#include "fpi-device.h" +#include "fp-sdcp-device.h" + +/** + * FpiSdcpClaim: + * @cert_m: The per-model ECDSA certificate (x509 ASN.1 DER encoded) + * @pk_d: The device public key (65 bytes) + * @pk_f: The firmware public key (65 bytes) + * @h_f: The firmware hash + * @s_m: Signature over @pk_d using the per-model private key (64 bytes) + * @s_d: Signature over h_f and pk_f using the device private key (64 bytes) + * + * Structure to hold the claim as produced by the device during a secure + * connect. See the SDCP specification for more details. + * + * Note all of these may simply be memory views into a larger #GBytes created + * using g_bytes_new_from_bytes(). + */ +struct _FpiSdcpClaim +{ + /*< public >*/ + GBytes *cert_m; + GBytes *pk_d; + GBytes *pk_f; + GBytes *h_f; + GBytes *s_m; + GBytes *s_d; +}; +typedef struct _FpiSdcpClaim FpiSdcpClaim; + +GType fpi_sdcp_claim_get_type (void) G_GNUC_CONST; +FpiSdcpClaim *fpi_sdcp_claim_new (void); +FpiSdcpClaim *fpi_sdcp_claim_copy (FpiSdcpClaim *other); +void fpi_sdcp_claim_free (FpiSdcpClaim *claim); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (FpiSdcpClaim, fpi_sdcp_claim_free) + + +/** + * FpSdcpDeviceClass: + * @connect: Establish SDCP connection. Similar to open in #FpDeviceClass + * but called connect to mirror the SDCP specification. + * @reconnect: Perform a faster reconnect. Drivers do not need to provide this + * function. If reconnect fails, then a normal connect will be tried. + * @enroll_begin: Start the enrollment procedure. In the absence of an error, + * the driver must call fpi_sdcp_device_enroll_set_nonce() at any point. It + * also must report enrollment progress using fpi_device_enroll_progress(). + * It should also store available metadata about the print in device memory. + * The operation is completed with fpi_sdcp_device_enroll_ready(). + * @enroll_commit: Complete the enrollment procedure. This commits the newly + * enrolled print to the device memory. Will only be called if enroll_begin + * succeeded. The passed id may be %NULL, in that case the driver must + * abort the enrollment process. id is owned by the base class and remains + * valid throughout the operation. + * @identify: Start identification process. On completion, the driver must call + * fpi_sdcp_device_identify_complete(). To request the user to retry the + * fpi_sdcp_device_identify_retry() function is used. + * + * + * These are the main entry points for drivers implementing SDCP. + * + * Drivers *must* eventually call the corresponding function to finish the + * operation. + * + * XXX: Is the use of fpi_device_action_error() acceptable? + * + * Drivers *must* also handle cancellation properly for any long running + * operation (i.e. any operation that requires capturing). It is entirely fine + * to ignore cancellation requests for short operations (e.g. open/close). + * + * This API is solely intended for drivers. It is purely internal and neither + * API nor ABI stable. + */ +struct _FpSdcpDeviceClass +{ + FpDeviceClass parent_class; + + void (*connect) (FpSdcpDevice *dev); + void (*reconnect) (FpSdcpDevice *dev); + void (*close) (FpSdcpDevice *dev); + void (*enroll_begin) (FpSdcpDevice *dev); + void (*enroll_commit) (FpSdcpDevice *dev, + GBytes *id); + void (*identify) (FpSdcpDevice *dev); +}; + +void fpi_sdcp_device_set_intermediat_cas (FpSdcpDevice *self, + GBytes *ca_1, + GBytes *ca_2); + +void fpi_sdcp_device_get_connect_data (FpSdcpDevice *self, + GBytes **r_h, + GBytes **pk_h); +void fpi_sdcp_device_connect_complete (FpSdcpDevice *self, + GBytes *r_d, + FpiSdcpClaim *claim, + GBytes *mac, + GError *error); + +void fpi_sdcp_device_get_reconnect_data (FpSdcpDevice *self, + GBytes **r_h); +void fpi_sdcp_device_reconnect_complete (FpSdcpDevice *self, + GBytes *mac, + GError *error); + +void fpi_sdcp_device_enroll_set_nonce (FpSdcpDevice *self, + GBytes *nonce); +void fpi_sdcp_device_enroll_ready (FpSdcpDevice *self, + GError *error); +void fpi_sdcp_device_enroll_commit_complete (FpSdcpDevice *self, + GError *error); + +void fpi_sdcp_device_get_identify_data (FpSdcpDevice *self, + GBytes **nonce); +void fpi_sdcp_device_identify_retry (FpSdcpDevice *self, + GError *error); +void fpi_sdcp_device_identify_complete (FpSdcpDevice *self, + GBytes *id, + GBytes *mac, + GError *error); diff --git a/libfprint/meson.build b/libfprint/meson.build index 25ed10f3..348dd247 100644 --- a/libfprint/meson.build +++ b/libfprint/meson.build @@ -142,6 +142,8 @@ driver_sources = { } helper_sources = { + 'sdcp' : + [ 'fp-sdcp-device.c', 'fpi-sdcp-device.c' ], 'aeslib' : [ 'drivers/aeslib.c' ], 'aesx660' : diff --git a/meson.build b/meson.build index c937d281..0d7a8fae 100644 --- a/meson.build +++ b/meson.build @@ -217,6 +217,13 @@ foreach i : driver_helpers error('nss is required for @0@ and possibly others'.format(driver)) endif + optional_deps += nss_dep + elif i == 'sdcp' + nss_dep = dependency('nss', version: '>=3.55', required: false) + if not nss_dep.found() + error('nss >=3.55 is required for SDCP support (@0@ and possibly others)'.format(driver)) + endif + optional_deps += nss_dep elif i == 'udev' install_udev_rules = true