fwupd/plugins/solokey/fu-solokey-device.c
Richard Hughes 56ac823af9 Set the protocol per-device not per-plugin
Some plugins have devices with more than one protocol. Logically the protocol
belongs to the device, not the plugin, and in the future we could use this to
further check firmware that's about to be deployed.

This is also not exported into libfwupd (yet?) as it's remains a debug-feature
only -- protocols are not actually required for devices to be added.
2019-11-25 18:01:54 +00:00

505 lines
15 KiB
C

/*
* Copyright (C) 2019 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include <string.h>
#include "fu-chunk.h"
#include "fu-solokey-device.h"
#include "fu-solokey-firmware.h"
struct _FuSolokeyDevice {
FuUsbDevice parent_instance;
guint32 cid;
};
G_DEFINE_TYPE (FuSolokeyDevice, fu_solokey_device, FU_TYPE_USB_DEVICE)
#define SOLO_EXTENSION_VERSION 0x14
#define SOLO_BOOTLOADER_WRITE 0x40
#define SOLO_BOOTLOADER_DONE 0x41
#define SOLO_BOOTLOADER_VERSION 0x44
#define SOLO_BOOTLOADER_HID_CMD_BOOT 0x50
#define SOLO_USB_TIMEOUT 5000 /* ms */
#define SOLO_USB_HID_EP 0x0001
#define SOLO_USB_HID_EP_IN (SOLO_USB_HID_EP | 0x80)
#define SOLO_USB_HID_EP_OUT (SOLO_USB_HID_EP | 0x00)
#define SOLO_USB_HID_EP_SIZE 64
static void
fu_solokey_device_exchange (GByteArray *req, guint8 cmd, guint32 addr, GByteArray *ibuf)
{
guint8 buf_addr[4] = { 0x00 };
guint8 buf_len[2] = { 0x00 };
/* command */
fu_byte_array_append_uint8 (req, cmd);
/* first *3* bytes of the LE address */
fu_common_write_uint32 (buf_addr, addr, G_LITTLE_ENDIAN);
g_byte_array_append (req, buf_addr, 3);
/* "random" number :/ */
g_byte_array_append (req, (const guint8 *) "\x8C\x27\x90\xf6", 4);
/* uint16 length then optional (or dummy) data */
if (ibuf != NULL) {
fu_common_write_uint16 (buf_len, ibuf->len, G_BIG_ENDIAN);
g_byte_array_append (req, buf_len, sizeof(buf_len));
g_byte_array_append (req, ibuf->data, ibuf->len);
return;
}
/* dummy data */
fu_common_write_uint16 (buf_len, 16, G_BIG_ENDIAN);
g_byte_array_append (req, buf_len, sizeof(buf_len));
for (guint i = 0; i < 16; i++)
fu_byte_array_append_uint8 (req, 'A');
}
static gboolean
fu_solokey_device_probe (FuUsbDevice *device, GError **error)
{
/* always disregard the bcdVersion */
fu_device_set_version (FU_DEVICE (device), NULL, FWUPD_VERSION_FORMAT_UNKNOWN);
return TRUE;
}
static gboolean
fu_solokey_device_open (FuUsbDevice *device, GError **error)
{
GUsbDevice *usb_device = fu_usb_device_get_dev (device);
g_autofree gchar *product = NULL;
g_auto(GStrv) split = NULL;
/* got the version using the HID API */
if (!g_usb_device_set_configuration (usb_device, 0x0001, error))
return FALSE;
if (!g_usb_device_claim_interface (usb_device, 0x0000,
G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
error)) {
return FALSE;
}
/* parse product ID */
product = g_usb_device_get_string_descriptor (usb_device,
g_usb_device_get_product_index (usb_device),
error);
if (product == NULL)
return FALSE;
split = g_strsplit (product, " ", -1);
if (g_strv_length (split) < 2) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"product not parsable, got '%s'",
product);
return FALSE;
}
if (g_strcmp0 (split[0], "Solo") != 0) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"product not expected format, got '%s'",
product);
return FALSE;
}
if (g_strcmp0 (split[1], "Hacker") == 0) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"Only Solo Secure supported");
return FALSE;
}
if (g_strcmp0 (split[1], "Bootloader") == 0) {
fu_device_set_version_bootloader (FU_DEVICE (device), split[2]);
fu_device_add_flag (FU_DEVICE (device), FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
fu_device_remove_flag (FU_DEVICE (device), FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER);
} else {
fu_device_set_version (FU_DEVICE (device), split[1], FWUPD_VERSION_FORMAT_TRIPLET);
fu_device_remove_flag (FU_DEVICE (device), FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
fu_device_add_flag (FU_DEVICE (device), FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER);
}
/* success */
return TRUE;
}
static gboolean
fu_solokey_device_close (FuUsbDevice *device, GError **error)
{
GUsbDevice *usb_device = fu_usb_device_get_dev (device);
/* rebind kernel driver so it works as a security key again... */
if (!g_usb_device_release_interface (usb_device, 0x0000,
G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
error)) {
g_prefix_error (error, "failed to release interface: ");
return FALSE;
}
/* success */
return TRUE;
}
static gboolean
fu_solokey_device_packet_tx (FuSolokeyDevice *self, GByteArray *req, GError **error)
{
GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (self));
gsize actual_length = 0;
/* round up to the endpoint size */
for (guint i = req->len; i < SOLO_USB_HID_EP_SIZE; i++)
fu_byte_array_append_uint8 (req, 0x0);
/* request */
if (g_getenv ("FWUPD_SOLOKEY_VERBOSE") != NULL) {
fu_common_dump_full (G_LOG_DOMAIN, "REQ", req->data, req->len,
16, FU_DUMP_FLAGS_SHOW_ADDRESSES);
}
/* do not hit hardware */
if (g_getenv ("FWUPD_SOLOKEY_EMULATE") != NULL)
return TRUE;
if (!g_usb_device_interrupt_transfer (usb_device,
SOLO_USB_HID_EP_OUT,
req->data,
req->len,
&actual_length,
SOLO_USB_TIMEOUT,
NULL, /* cancellable */
error)) {
g_prefix_error (error, "failed to send request: ");
return FALSE;
}
if (actual_length != req->len) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"request not all sent, got %" G_GSIZE_FORMAT,
actual_length);
return FALSE;
}
/* success */
return TRUE;
}
static GByteArray *
fu_solokey_device_packet_rx (FuSolokeyDevice *self, GError **error)
{
GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (self));
gsize actual_length = 0;
g_autoptr(GByteArray) res = g_byte_array_new ();
guint8 buf[SOLO_USB_HID_EP_SIZE] = { 0x00 };
/* return anything */
if (g_getenv ("FWUPD_SOLOKEY_EMULATE") != NULL)
return g_steal_pointer (&res);
/* read reply */
if (!g_usb_device_interrupt_transfer (usb_device,
SOLO_USB_HID_EP_IN,
buf, sizeof(buf),
&actual_length,
SOLO_USB_TIMEOUT,
NULL, /* cancellable */
error)) {
g_prefix_error (error, "failed to get reply: ");
return NULL;
}
if (g_getenv ("FWUPD_SOLOKEY_VERBOSE") != NULL)
fu_common_dump_raw (G_LOG_DOMAIN, "RES", buf, actual_length);
/* copy back optional buf */
g_byte_array_append (res, buf, actual_length);
return g_steal_pointer (&res);
}
static GByteArray *
fu_solokey_device_packet (FuSolokeyDevice *self, guint8 cmd,
GByteArray *payload, GError **error)
{
guint8 buf_cid[4] = { 0x00 };
guint8 buf_len[2] = { 0x00 };
guint8 cmd_id = cmd | 0x80;
guint16 first_chunk_size;
g_autoptr(GByteArray) req = g_byte_array_new ();
g_autoptr(GByteArray) res = NULL;
/* U2F header */
fu_common_write_uint32 (buf_cid, self->cid, G_LITTLE_ENDIAN);
g_byte_array_append (req, buf_cid, sizeof(buf_cid));
g_byte_array_append (req, &cmd_id, sizeof(cmd_id));
/* no payload */
if (payload == NULL) {
if (!fu_solokey_device_packet_tx (self, req, error))
return NULL;
return fu_solokey_device_packet_rx (self, error);
}
/* sanity check */
if (payload->len > 7609) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"payload impossible size, got %x",
payload->len);
return NULL;
}
/* initialization packet */
first_chunk_size = payload->len;
if (payload->len > SOLO_USB_HID_EP_SIZE - 7)
first_chunk_size = SOLO_USB_HID_EP_SIZE - 7;
fu_common_write_uint16 (buf_len, payload->len, G_BIG_ENDIAN);
g_byte_array_append (req, buf_len, sizeof(buf_len));
g_byte_array_append (req, payload->data, first_chunk_size);
if (!fu_solokey_device_packet_tx (self, req, error))
return NULL;
/* continuation packets */
if (payload->len > first_chunk_size) {
g_autoptr(GPtrArray) chunks = NULL;
chunks = fu_chunk_array_new (payload->data + first_chunk_size,
payload->len - first_chunk_size,
0x00, /* addr start */
0x00, /* page_sz */
SOLO_USB_HID_EP_SIZE - 5);
for (guint i = 0; i < chunks->len; i++) {
FuChunk *chk = g_ptr_array_index (chunks, i);
guint8 seq = chk->idx;
g_autoptr(GByteArray) req2 = g_byte_array_new ();
g_byte_array_append (req2, buf_cid, sizeof(buf_cid));
g_byte_array_append (req2, &seq, sizeof(seq));
g_byte_array_append (req2, chk->data, chk->data_sz);
if (!fu_solokey_device_packet_tx (self, req2, error))
return NULL;
}
}
/* do not hit hardware */
if (g_getenv ("FWUPD_SOLOKEY_EMULATE") != NULL)
return g_byte_array_new ();
/* success */
res = fu_solokey_device_packet_rx (self, error);
if (res == NULL)
return NULL;
if (res->len != SOLO_USB_HID_EP_SIZE) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"result invalid size, got %x", res->len);
return NULL;
}
if (memcmp (res->data + 0, buf_cid, sizeof(buf_cid)) != 0) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"CID invalid, got %x",
fu_common_read_uint32 (res->data + 0, G_BIG_ENDIAN));
return NULL;
}
if (res->data[4] != cmd_id) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"command ID invalid, got %x", res->data[4]);
return NULL;
}
return g_steal_pointer (&res);
}
static gboolean
fu_solokey_device_setup_cid (FuSolokeyDevice *self, GError **error)
{
g_autoptr(GByteArray) nonce = g_byte_array_new ();
g_autoptr(GByteArray) res = NULL;
/* do not hit hardware */
if (g_getenv ("FWUPD_SOLOKEY_EMULATE") != NULL)
return TRUE;
/* get a channel ID */
for (guint i = 0; i < 8; i++)
fu_byte_array_append_uint8 (nonce, g_random_int_range (0x00, 0xff));
res = fu_solokey_device_packet (self, 0x06, nonce, error);
if (res == NULL)
return FALSE;
if (fu_common_read_uint16 (res->data + 5, G_LITTLE_ENDIAN) < 0x11) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"INIT length invalid");
return FALSE;
}
if (memcmp (res->data + 7, nonce->data, 8) != 0) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"nonce invalid");
return FALSE;
}
self->cid = fu_common_read_uint32 (res->data + 7 + 8, G_LITTLE_ENDIAN);
g_debug ("CID to use for device: %04x", self->cid);
return TRUE;
}
static gboolean
fu_solokey_device_get_version_bl (FuSolokeyDevice *self, GError **error)
{
g_autofree gchar *version = NULL;
g_autoptr(GByteArray) req = g_byte_array_new ();
g_autoptr(GByteArray) res = NULL;
/* pass through data */
fu_solokey_device_exchange (req, SOLO_BOOTLOADER_VERSION, 0x00, NULL);
res = fu_solokey_device_packet (self, SOLO_BOOTLOADER_HID_CMD_BOOT, req, error);
if (res == NULL)
return FALSE;
version = g_strdup_printf ("%u.%u.%u", res->data[8], res->data[9], res->data[10]);
fu_device_set_version_bootloader (FU_DEVICE (self), version);
return TRUE;
}
static gboolean
fu_solokey_device_setup (FuDevice *device, GError **error)
{
FuSolokeyDevice *self = FU_SOLOKEY_DEVICE (device);
/* get channel ID */
if (!fu_solokey_device_setup_cid (self, error))
return FALSE;
/* verify version */
if (fu_device_has_flag (FU_DEVICE (device), FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) {
if (!fu_solokey_device_get_version_bl (self, error))
return FALSE;
}
/* success */
return TRUE;
}
static gboolean
fu_solokey_device_verify (FuSolokeyDevice *self, GBytes *fw_sig, GError **error)
{
g_autoptr(GByteArray) req = g_byte_array_new ();
g_autoptr(GByteArray) res = NULL;
g_autoptr(GByteArray) sig = g_byte_array_new ();
fu_device_set_status (FU_DEVICE (self), FWUPD_STATUS_DEVICE_VERIFY);
g_byte_array_append (sig, g_bytes_get_data (fw_sig, NULL), g_bytes_get_size (fw_sig));
fu_solokey_device_exchange (req, SOLO_BOOTLOADER_DONE, 0x00, sig);
res = fu_solokey_device_packet (self, SOLO_BOOTLOADER_HID_CMD_BOOT, req, error);
if (res == NULL)
return FALSE;
return TRUE;
}
static FuFirmware *
fu_solokey_device_prepare_firmware (FuDevice *device,
GBytes *fw,
FwupdInstallFlags flags,
GError **error)
{
g_autoptr(FuFirmware) firmware = fu_solokey_firmware_new ();
fu_device_set_status (device, FWUPD_STATUS_DECOMPRESSING);
if (!fu_firmware_parse (firmware, fw, flags, error))
return NULL;
return g_steal_pointer (&firmware);
}
static gboolean
fu_solokey_device_write_firmware (FuDevice *device,
FuFirmware *firmware,
FwupdInstallFlags flags,
GError **error)
{
FuSolokeyDevice *self = FU_SOLOKEY_DEVICE (device);
g_autoptr(FuFirmwareImage) img = NULL;
g_autoptr(GBytes) fw = NULL;
g_autoptr(GBytes) fw_sig = NULL;
g_autoptr(GPtrArray) chunks = NULL;
/* get main image */
img = fu_firmware_get_image_by_id (firmware, NULL, error);
if (img == NULL)
return FALSE;
/* build packets */
fw = fu_firmware_image_write (img, error);
if (fw == NULL)
return FALSE;
chunks = fu_chunk_array_new_from_bytes (fw,
fu_firmware_image_get_addr (img),
0x00, /* page_sz */
2048);
/* write each block */
fu_device_set_status (device, FWUPD_STATUS_DEVICE_WRITE);
for (guint i = 0; i < chunks->len; i++) {
FuChunk *chk = g_ptr_array_index (chunks, i);
g_autoptr(GByteArray) buf = g_byte_array_new ();
g_autoptr(GByteArray) req = g_byte_array_new ();
g_autoptr(GByteArray) res = NULL;
g_autoptr(GError) error_local = NULL;
g_byte_array_append (buf, chk->data, chk->data_sz);
fu_solokey_device_exchange (req, SOLO_BOOTLOADER_WRITE, chk->address, buf);
res = fu_solokey_device_packet (self, SOLO_BOOTLOADER_HID_CMD_BOOT, req, &error_local);
if (res == NULL) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_WRITE,
"failed to write: %s",
error_local->message);
return FALSE;
}
/* update progress */
fu_device_set_progress_full (device, (gsize) i, (gsize) chunks->len);
}
/* verify the signature and reboot back to runtime */
fw_sig = fu_firmware_get_image_by_id_bytes (firmware,
FU_FIRMWARE_IMAGE_ID_SIGNATURE,
error);
if (fw_sig == NULL)
return FALSE;
return fu_solokey_device_verify (self, fw_sig, error);
}
static void
fu_solokey_device_init (FuSolokeyDevice *self)
{
self->cid = 0xffffffff;
fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_UPDATABLE);
fu_device_set_remove_delay (FU_DEVICE (self), FU_DEVICE_REMOVE_DELAY_USER_REPLUG);
fu_device_set_protocol (FU_DEVICE (self), "com.solokeys");
fu_device_set_name (FU_DEVICE (self), "Solo Secure");
fu_device_set_summary (FU_DEVICE (self), "An open source FIDO2 security key");
fu_device_add_icon (FU_DEVICE (self), "applications-internet");
}
static void
fu_solokey_device_class_init (FuSolokeyDeviceClass *klass)
{
FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass);
FuUsbDeviceClass *klass_usb_device = FU_USB_DEVICE_CLASS (klass);
klass_device->write_firmware = fu_solokey_device_write_firmware;
klass_device->prepare_firmware = fu_solokey_device_prepare_firmware;
klass_device->setup = fu_solokey_device_setup;
klass_usb_device->open = fu_solokey_device_open;
klass_usb_device->close = fu_solokey_device_close;
klass_usb_device->probe = fu_solokey_device_probe;
}