fwupd/plugins/solokey/fu-solokey-device.c
Richard Hughes 7afd7cba0d Use FuFirmware as a container for firmware images
In many plugins we've wanted to use ->prepare_firmware() to parse the firmware
ahead of ->detach() and ->write_firmware() but this has the limitation that it
can only return a single blob of data.

For many devices, multiple binary blobs are required from one parsed image,
for instance providing signatures, config and data blobs that have to be pushed
to the device in different way.

This also means we parse the firmware *before* we ask the user to detach.

Break the internal FuDevice API to support these firmware types as they become
more popular.

This also allows us to move the Intel HEX and SREC parsing out of the dfu plugin
as they are used by a few plugins now, and resolving symbols between plugins
isn't exactly awesome.
2019-08-08 13:10:57 +01:00

517 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 */
g_byte_array_append (req, &cmd, sizeof(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++) {
guint8 tmp = 'A';
g_byte_array_append (req, &tmp, sizeof(tmp));
}
}
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++) {
const guint8 tmp = 0x0;
g_byte_array_append (req, &tmp, 1);
}
/* 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 FALSE;
}
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,
"commmand 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++) {
guint8 tmp = g_random_int_range (0x00, 0xff);
g_byte_array_append (nonce, &tmp, 1);
}
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 thru 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_get_bytes (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, "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_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;
}
FuSolokeyDevice *
fu_solokey_device_new (FuUsbDevice *device)
{
FuSolokeyDevice *self = NULL;
self = g_object_new (FU_TYPE_SOLOKEY_DEVICE, NULL);
fu_device_incorporate (FU_DEVICE (self), FU_DEVICE (device));
return self;
}