From aa1981e28046b2791a7bee122dec2f14dec70f62 Mon Sep 17 00:00:00 2001 From: Benjamin Berg Date: Mon, 11 May 2020 16:17:03 +0200 Subject: [PATCH] sdcp: Add SDCP base class This adds a base class for SDCP devices. Not all functionality has been fully tested, in particular the code to verify the model certificate is most likely broken or incomplete. One problem there is that there is no code to find the root CA to trust. See: #257 --- doc/libfprint-2-sections.txt | 26 + doc/libfprint-docs.xml | 2 + libfprint/fp-sdcp-device-private.h | 58 ++ libfprint/fp-sdcp-device.c | 141 +++ libfprint/fp-sdcp-device.h | 29 + libfprint/fpi-sdcp-device.c | 1369 ++++++++++++++++++++++++++++ libfprint/fpi-sdcp-device.h | 142 +++ libfprint/meson.build | 2 + meson.build | 7 + 9 files changed, 1776 insertions(+) create mode 100644 libfprint/fp-sdcp-device-private.h create mode 100644 libfprint/fp-sdcp-device.c create mode 100644 libfprint/fp-sdcp-device.h create mode 100644 libfprint/fpi-sdcp-device.c create mode 100644 libfprint/fpi-sdcp-device.h diff --git a/doc/libfprint-2-sections.txt b/doc/libfprint-2-sections.txt index eba3d81..3b6f4b6 100644 --- a/doc/libfprint-2-sections.txt +++ b/doc/libfprint-2-sections.txt @@ -94,6 +94,12 @@ FP_TYPE_IMAGE_DEVICE FpImageDevice +
+fp-sdcp-device +FP_TYPE_SDCP_DEVICE +FpSdcpDevice +
+
fp-print FP_TYPE_PRINT @@ -204,6 +210,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 b866aab..1330917 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 0000000..249c33c --- /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 0000000..4cdac4e --- /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 0000000..cebe5a9 --- /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 0000000..999b474 --- /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 0000000..d5a4fcf --- /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 6855f1e..c736f28 100644 --- a/libfprint/meson.build +++ b/libfprint/meson.build @@ -138,6 +138,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 d2bc8e8..8a77d7e 100644 --- a/meson.build +++ b/meson.build @@ -215,6 +215,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