mirror of
https://git.proxmox.com/git/fwupd
synced 2025-05-02 18:30:40 +00:00
526 lines
15 KiB
C
526 lines
15 KiB
C
/*
|
|
* Copyright (C) 2019 Richard Hughes <richard@hughsie.com>
|
|
*
|
|
* SPDX-License-Identifier: LGPL-2.1+
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <fwupdplugin.h>
|
|
|
|
#include <string.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_open(FuDevice *device, GError **error)
|
|
{
|
|
GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device));
|
|
g_autofree gchar *product = NULL;
|
|
g_auto(GStrv) split = NULL;
|
|
|
|
/* FuUsbDevice->open */
|
|
if (!FU_DEVICE_CLASS(fu_solokey_device_parent_class)->open(device, error))
|
|
return FALSE;
|
|
|
|
/* 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(device, split[2]);
|
|
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
|
|
fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER);
|
|
} else if (g_strcmp0(split[1], "Keys") == 0 && g_strcmp0(split[2], "Solo") == 0) {
|
|
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
|
|
fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER);
|
|
} else {
|
|
fu_device_set_version(device, split[1]);
|
|
fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
|
|
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER);
|
|
}
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_solokey_device_close(FuDevice *device, GError **error)
|
|
{
|
|
GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(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;
|
|
}
|
|
|
|
/* FuUsbDevice->close */
|
|
return FU_DEVICE_CLASS(fu_solokey_device_parent_class)->close(device, error);
|
|
}
|
|
|
|
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;
|
|
guint8 cmd_id_tmp = 0;
|
|
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 = fu_chunk_get_idx(chk);
|
|
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,
|
|
fu_chunk_get_data(chk),
|
|
fu_chunk_get_data_sz(chk));
|
|
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) {
|
|
guint32 cid = 0;
|
|
if (!fu_common_read_uint32_safe(res->data,
|
|
res->len,
|
|
0x0,
|
|
&cid,
|
|
G_BIG_ENDIAN,
|
|
error))
|
|
return NULL;
|
|
g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "CID invalid, got %x", cid);
|
|
return NULL;
|
|
}
|
|
if (!fu_common_read_uint8_safe(res->data, res->len, 0x4, &cmd_id_tmp, error))
|
|
return NULL;
|
|
if (cmd_id_tmp != cmd_id) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INTERNAL,
|
|
"command ID invalid, got %x",
|
|
cmd_id_tmp);
|
|
return NULL;
|
|
}
|
|
return g_steal_pointer(&res);
|
|
}
|
|
|
|
static gboolean
|
|
fu_solokey_device_setup_cid(FuSolokeyDevice *self, GError **error)
|
|
{
|
|
guint16 init_len = 0;
|
|
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_safe(res->data, res->len, 5, &init_len, G_LITTLE_ENDIAN, error))
|
|
return FALSE;
|
|
if (init_len < 0x11) {
|
|
g_set_error_literal(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INTERNAL,
|
|
"INIT length invalid");
|
|
return FALSE;
|
|
}
|
|
if (res->len < 7 + 8 || memcmp(res->data + 7, nonce->data, 8) != 0) {
|
|
g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL, "nonce invalid");
|
|
return FALSE;
|
|
}
|
|
if (!fu_common_read_uint32_safe(res->data,
|
|
res->len,
|
|
7 + 8,
|
|
&self->cid,
|
|
G_LITTLE_ENDIAN,
|
|
error))
|
|
return FALSE;
|
|
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);
|
|
|
|
/* FuUsbDevice->setup */
|
|
if (!FU_DEVICE_CLASS(fu_solokey_device_parent_class)->setup(device, error))
|
|
return FALSE;
|
|
|
|
/* get channel ID */
|
|
if (!fu_solokey_device_setup_cid(self, error))
|
|
return FALSE;
|
|
|
|
/* verify version */
|
|
if (fu_device_has_flag(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);
|
|
fu_byte_array_append_bytes(sig, 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();
|
|
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(GBytes) fw = NULL;
|
|
g_autoptr(GBytes) fw_sig = NULL;
|
|
g_autoptr(GPtrArray) chunks = NULL;
|
|
|
|
/* build packets */
|
|
fw = fu_firmware_get_bytes(firmware, error);
|
|
if (fw == NULL)
|
|
return FALSE;
|
|
chunks = fu_chunk_array_new_from_bytes(fw,
|
|
fu_firmware_get_addr(firmware),
|
|
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, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk));
|
|
fu_solokey_device_exchange(req,
|
|
SOLO_BOOTLOADER_WRITE,
|
|
fu_chunk_get_address(chk),
|
|
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_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_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET);
|
|
fu_device_add_protocol(FU_DEVICE(self), "com.solokeys");
|
|
fu_device_set_name(FU_DEVICE(self), "Solo Secure");
|
|
fu_device_set_summary(FU_DEVICE(self), "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);
|
|
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_device->open = fu_solokey_device_open;
|
|
klass_device->close = fu_solokey_device_close;
|
|
}
|