diff --git a/configure b/configure index 6ecaff429b..8dc981684b 100755 --- a/configure +++ b/configure @@ -495,6 +495,7 @@ trace_file="trace" spice="" rbd="" smartcard="" +u2f="auto" libusb="" usb_redir="" opengl="" @@ -1415,6 +1416,10 @@ for opt do ;; --enable-smartcard) smartcard="yes" ;; + --disable-u2f) u2f="disabled" + ;; + --enable-u2f) u2f="enabled" + ;; --disable-libusb) libusb="no" ;; --enable-libusb) libusb="yes" @@ -1945,6 +1950,7 @@ disabled with --disable-FEATURE, default is enabled if available: libiscsi iscsi support libnfs nfs support smartcard smartcard support (libcacard) + u2f U2F support (u2f-emu) libusb libusb (for usb passthrough) live-block-migration Block migration in the main migration stream usb-redir usb network redirection support @@ -8229,7 +8235,7 @@ NINJA=${ninja:-$PWD/ninjatool} $meson setup \ -Db_coverage=$(if test "$gcov" = yes; then echo true; else echo false; fi) \ -Dsdl=$sdl -Dsdl_image=$sdl_image \ -Dvnc=$vnc -Dvnc_sasl=$vnc_sasl -Dvnc_jpeg=$vnc_jpeg -Dvnc_png=$vnc_png \ - -Dgettext=$gettext -Dxkbcommon=$xkbcommon \ + -Dgettext=$gettext -Dxkbcommon=$xkbcommon -Du2f=$u2f\ $cross_arg \ "$PWD" "$source_path" diff --git a/docs/qdev-device-use.txt b/docs/qdev-device-use.txt index f8d0d2fe29..9889521e3c 100644 --- a/docs/qdev-device-use.txt +++ b/docs/qdev-device-use.txt @@ -325,6 +325,7 @@ The new way is -device DEVNAME,DEV-OPTS... Details depend on DRIVER: * mouse -device usb-mouse * tablet -device usb-tablet * wacom-tablet -device usb-wacom-tablet +* u2f -device u2f-{emulated,passthru} * braille See "Character Devices" === Watchdog Devices === diff --git a/docs/system/usb.rst b/docs/system/usb.rst index ddfa828d74..9a2f1927c4 100644 --- a/docs/system/usb.rst +++ b/docs/system/usb.rst @@ -81,6 +81,9 @@ option or the ``device_add`` monitor command. Available devices are: ``usb-audio`` USB audio device +``u2f-{emulated,passthru}`` + Universal Second Factor device + .. _host_005fusb_005fdevices: Using host USB devices on a Linux host diff --git a/docs/u2f.txt b/docs/u2f.txt new file mode 100644 index 0000000000..8f44994818 --- /dev/null +++ b/docs/u2f.txt @@ -0,0 +1,110 @@ +QEMU U2F Key Device Documentation. + +Contents +1. USB U2F key device +2. Building +3. Using u2f-emulated +4. Using u2f-passthru +5. Libu2f-emu + +1. USB U2F key device + +U2F is an open authentication standard that enables relying parties +exposed to the internet to offer a strong second factor option for end +user authentication. + +The standard brings many advantages to both parties, client and server, +allowing to reduce over-reliance on passwords, it increases authentication +security and simplifies passwords. + +The second factor is materialized by a device implementing the U2F +protocol. In case of a USB U2F security key, it is a USB HID device +that implements the U2F protocol. + +In Qemu, the USB U2F key device offers a dedicated support of U2F, allowing +guest USB FIDO/U2F security keys operating in two possible modes: +pass-through and emulated. + +The pass-through mode consists of passing all requests made from the guest +to the physical security key connected to the host machine and vice versa. +In addition, the dedicated pass-through allows to have a U2F security key +shared on several guests which is not possible with a simple host device +assignment pass-through. + +The emulated mode consists of completely emulating the behavior of an +U2F device through software part. Libu2f-emu is used for that. + + +2. Building + +To ensure the build of the u2f-emulated device variant which depends +on libu2f-emu: configuring and building: + + ./configure --enable-u2f && make + +The pass-through mode is built by default on Linux. To take advantage +of the autoscan option it provides, make sure you have a working libudev +installed on the host. + + +3. Using u2f-emulated + +To work, an emulated U2F device must have four elements: + * ec x509 certificate + * ec private key + * counter (four bytes value) + * 48 bytes of entropy (random bits) + +To use this type of device, this one has to be configured, and these +four elements must be passed one way or another. + +Assuming that you have a working libu2f-emu installed on the host. +There are three possible ways of configurations: + * ephemeral + * setup directory + * manual + +Ephemeral is the simplest way to configure, it lets the device generate +all the elements it needs for a single use of the lifetime of the device. + + qemu -usb -device u2f-emulated + +Setup directory allows to configure the device from a directory containing +four files: + * certificate.pem: ec x509 certificate + * private-key.pem: ec private key + * counter: counter value + * entropy: 48 bytes of entropy + + qemu -usb -device u2f-emulated,dir=$dir + +Manual allows to configure the device more finely by specifying each +of the elements necessary for the device: + * cert + * priv + * counter + * entropy + + qemu -usb -device u2f-emulated,cert=$DIR1/$FILE1,priv=$DIR2/$FILE2,counter=$DIR3/$FILE3,entropy=$DIR4/$FILE4 + + +4. Using u2f-passthru + +On the host specify the u2f-passthru device with a suitable hidraw: + + qemu -usb -device u2f-passthru,hidraw=/dev/hidraw0 + +Alternately, the u2f-passthru device can autoscan to take the first +U2F device it finds on the host (this requires a working libudev): + + qemu -usb -device u2f-passthru + + +5. Libu2f-emu + +The u2f-emulated device uses libu2f-emu for the U2F key emulation. Libu2f-emu +implements completely the U2F protocol device part for all specified +transport given by the FIDO Alliance. + +For more information about libu2f-emu see this page: +https://github.com/MattGorko/libu2f-emu. diff --git a/hw/usb/Kconfig b/hw/usb/Kconfig index 5e63dc75f8..3fc8fbe3c7 100644 --- a/hw/usb/Kconfig +++ b/hw/usb/Kconfig @@ -96,6 +96,11 @@ config USB_STORAGE_MTP default y depends on USB +config USB_U2F + bool + default y + depends on USB + config IMX_USBPHY bool default y diff --git a/hw/usb/core.c b/hw/usb/core.c index 5abd128b6b..5234dcc73f 100644 --- a/hw/usb/core.c +++ b/hw/usb/core.c @@ -129,6 +129,7 @@ void usb_wakeup(USBEndpoint *ep, unsigned int stream) static void do_token_setup(USBDevice *s, USBPacket *p) { int request, value, index; + unsigned int setup_len; if (p->iov.size != 8) { p->status = USB_RET_STALL; @@ -138,14 +139,15 @@ static void do_token_setup(USBDevice *s, USBPacket *p) usb_packet_copy(p, s->setup_buf, p->iov.size); s->setup_index = 0; p->actual_length = 0; - s->setup_len = (s->setup_buf[7] << 8) | s->setup_buf[6]; - if (s->setup_len > sizeof(s->data_buf)) { + setup_len = (s->setup_buf[7] << 8) | s->setup_buf[6]; + if (setup_len > sizeof(s->data_buf)) { fprintf(stderr, "usb_generic_handle_packet: ctrl buffer too small (%d > %zu)\n", - s->setup_len, sizeof(s->data_buf)); + setup_len, sizeof(s->data_buf)); p->status = USB_RET_STALL; return; } + s->setup_len = setup_len; request = (s->setup_buf[0] << 8) | s->setup_buf[1]; value = (s->setup_buf[3] << 8) | s->setup_buf[2]; @@ -259,26 +261,28 @@ static void do_token_out(USBDevice *s, USBPacket *p) static void do_parameter(USBDevice *s, USBPacket *p) { int i, request, value, index; + unsigned int setup_len; for (i = 0; i < 8; i++) { s->setup_buf[i] = p->parameter >> (i*8); } s->setup_state = SETUP_STATE_PARAM; - s->setup_len = (s->setup_buf[7] << 8) | s->setup_buf[6]; s->setup_index = 0; request = (s->setup_buf[0] << 8) | s->setup_buf[1]; value = (s->setup_buf[3] << 8) | s->setup_buf[2]; index = (s->setup_buf[5] << 8) | s->setup_buf[4]; - if (s->setup_len > sizeof(s->data_buf)) { + setup_len = (s->setup_buf[7] << 8) | s->setup_buf[6]; + if (setup_len > sizeof(s->data_buf)) { fprintf(stderr, "usb_generic_handle_packet: ctrl buffer too small (%d > %zu)\n", - s->setup_len, sizeof(s->data_buf)); + setup_len, sizeof(s->data_buf)); p->status = USB_RET_STALL; return; } + s->setup_len = setup_len; if (p->pid == USB_TOKEN_OUT) { usb_packet_copy(p, s->data_buf, s->setup_len); diff --git a/hw/usb/dev-hid.c b/hw/usb/dev-hid.c index 89f63b698b..c73f7b2fe2 100644 --- a/hw/usb/dev-hid.c +++ b/hw/usb/dev-hid.c @@ -32,21 +32,9 @@ #include "qemu/module.h" #include "qemu/timer.h" #include "hw/input/hid.h" +#include "hw/usb/hid.h" #include "hw/qdev-properties.h" -/* HID interface requests */ -#define GET_REPORT 0xa101 -#define GET_IDLE 0xa102 -#define GET_PROTOCOL 0xa103 -#define SET_REPORT 0x2109 -#define SET_IDLE 0x210a -#define SET_PROTOCOL 0x210b - -/* HID descriptor types */ -#define USB_DT_HID 0x21 -#define USB_DT_REPORT 0x22 -#define USB_DT_PHY 0x23 - typedef struct USBHIDState { USBDevice dev; USBEndpoint *intr; @@ -618,38 +606,38 @@ static void usb_hid_handle_control(USBDevice *dev, USBPacket *p, goto fail; } break; - case GET_REPORT: + case HID_GET_REPORT: if (hs->kind == HID_MOUSE || hs->kind == HID_TABLET) { p->actual_length = hid_pointer_poll(hs, data, length); } else if (hs->kind == HID_KEYBOARD) { p->actual_length = hid_keyboard_poll(hs, data, length); } break; - case SET_REPORT: + case HID_SET_REPORT: if (hs->kind == HID_KEYBOARD) { p->actual_length = hid_keyboard_write(hs, data, length); } else { goto fail; } break; - case GET_PROTOCOL: + case HID_GET_PROTOCOL: if (hs->kind != HID_KEYBOARD && hs->kind != HID_MOUSE) { goto fail; } data[0] = hs->protocol; p->actual_length = 1; break; - case SET_PROTOCOL: + case HID_SET_PROTOCOL: if (hs->kind != HID_KEYBOARD && hs->kind != HID_MOUSE) { goto fail; } hs->protocol = value; break; - case GET_IDLE: + case HID_GET_IDLE: data[0] = hs->idle; p->actual_length = 1; break; - case SET_IDLE: + case HID_SET_IDLE: hs->idle = (uint8_t) (value >> 8); hid_set_next_idle(hs); if (hs->kind == HID_MOUSE || hs->kind == HID_TABLET) { diff --git a/hw/usb/dev-wacom.c b/hw/usb/dev-wacom.c index 8aba44b8bc..76fc5a5dab 100644 --- a/hw/usb/dev-wacom.c +++ b/hw/usb/dev-wacom.c @@ -29,6 +29,7 @@ #include "qemu/osdep.h" #include "ui/console.h" #include "hw/usb.h" +#include "hw/usb/hid.h" #include "migration/vmstate.h" #include "qemu/module.h" #include "desc.h" @@ -37,13 +38,6 @@ #define WACOM_GET_REPORT 0x2101 #define WACOM_SET_REPORT 0x2109 -/* HID interface requests */ -#define HID_GET_REPORT 0xa101 -#define HID_GET_IDLE 0xa102 -#define HID_GET_PROTOCOL 0xa103 -#define HID_SET_IDLE 0x210a -#define HID_SET_PROTOCOL 0x210b - typedef struct USBWacomState { USBDevice dev; USBEndpoint *intr; @@ -86,11 +80,11 @@ static const USBDescIface desc_iface_wacom = { /* HID descriptor */ .data = (uint8_t[]) { 0x09, /* u8 bLength */ - 0x21, /* u8 bDescriptorType */ + USB_DT_HID, /* u8 bDescriptorType */ 0x01, 0x10, /* u16 HID_class */ 0x00, /* u8 country_code */ 0x01, /* u8 num_descriptors */ - 0x22, /* u8 type: Report */ + USB_DT_REPORT, /* u8 type: Report */ 0x6e, 0, /* u16 len */ }, }, diff --git a/hw/usb/hcd-ehci.c b/hw/usb/hcd-ehci.c index 1495e8f7fa..2b995443fb 100644 --- a/hw/usb/hcd-ehci.c +++ b/hw/usb/hcd-ehci.c @@ -352,7 +352,6 @@ static void ehci_trace_sitd(EHCIState *s, hwaddr addr, static void ehci_trace_guest_bug(EHCIState *s, const char *message) { trace_usb_ehci_guest_bug(message); - warn_report("%s", message); } static inline bool ehci_enabled(EHCIState *s) @@ -1373,7 +1372,10 @@ static int ehci_execute(EHCIPacket *p, const char *action) spd = (p->pid == USB_TOKEN_IN && NLPTR_TBIT(p->qtd.altnext) == 0); usb_packet_setup(&p->packet, p->pid, ep, 0, p->qtdaddr, spd, (p->qtd.token & QTD_TOKEN_IOC) != 0); - usb_packet_map(&p->packet, &p->sgl); + if (usb_packet_map(&p->packet, &p->sgl)) { + qemu_sglist_destroy(&p->sgl); + return -1; + } p->async = EHCI_ASYNC_INITIALIZED; } @@ -1445,6 +1447,7 @@ static int ehci_process_itd(EHCIState *ehci, dev = ehci_find_device(ehci, devaddr); if (dev == NULL) { ehci_trace_guest_bug(ehci, "no device found"); + qemu_sglist_destroy(&ehci->isgl); return -1; } pid = dir ? USB_TOKEN_IN : USB_TOKEN_OUT; @@ -1452,7 +1455,10 @@ static int ehci_process_itd(EHCIState *ehci, if (ep && ep->type == USB_ENDPOINT_XFER_ISOC) { usb_packet_setup(&ehci->ipacket, pid, ep, 0, addr, false, (itd->transact[i] & ITD_XACT_IOC) != 0); - usb_packet_map(&ehci->ipacket, &ehci->isgl); + if (usb_packet_map(&ehci->ipacket, &ehci->isgl)) { + qemu_sglist_destroy(&ehci->isgl); + return -1; + } usb_handle_packet(dev, &ehci->ipacket); usb_packet_unmap(&ehci->ipacket, &ehci->isgl); } else { diff --git a/hw/usb/hcd-xhci.c b/hw/usb/hcd-xhci.c index 67a18fe2b6..46a2186d91 100644 --- a/hw/usb/hcd-xhci.c +++ b/hw/usb/hcd-xhci.c @@ -1615,7 +1615,10 @@ static int xhci_setup_packet(XHCITransfer *xfer) xhci_xfer_create_sgl(xfer, dir == USB_TOKEN_IN); /* Also sets int_req */ usb_packet_setup(&xfer->packet, dir, ep, xfer->streamid, xfer->trbs[0].addr, false, xfer->int_req); - usb_packet_map(&xfer->packet, &xfer->sgl); + if (usb_packet_map(&xfer->packet, &xfer->sgl)) { + qemu_sglist_destroy(&xfer->sgl); + return -1; + } DPRINTF("xhci: setup packet pid 0x%x addr %d ep %d\n", xfer->packet.pid, ep->dev->addr, ep->nr); return 0; diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c index c474551d84..08604f787f 100644 --- a/hw/usb/host-libusb.c +++ b/hw/usb/host-libusb.c @@ -39,6 +39,11 @@ #endif #include +#ifdef CONFIG_LINUX +#include +#include +#endif + #include "qapi/error.h" #include "migration/vmstate.h" #include "monitor/monitor.h" @@ -885,6 +890,7 @@ static void usb_host_ep_update(USBHostDevice *s) static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd) { USBDevice *udev = USB_DEVICE(s); + int libusb_speed; int bus_num = 0; int addr = 0; int rc; @@ -935,7 +941,36 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd) usb_ep_init(udev); usb_host_ep_update(s); - udev->speed = speed_map[libusb_get_device_speed(dev)]; + libusb_speed = libusb_get_device_speed(dev); +#ifdef CONFIG_LINUX + if (hostfd && libusb_speed == 0) { + /* + * Workaround libusb bug: libusb_get_device_speed() does not + * work for libusb_wrap_sys_device() devices in v1.0.23. + * + * Speeds are defined in linux/usb/ch9.h, file not included + * due to name conflicts. + */ + int rc = ioctl(hostfd, USBDEVFS_GET_SPEED, NULL); + switch (rc) { + case 1: /* low */ + libusb_speed = LIBUSB_SPEED_LOW; + break; + case 2: /* full */ + libusb_speed = LIBUSB_SPEED_FULL; + break; + case 3: /* high */ + case 4: /* wireless */ + libusb_speed = LIBUSB_SPEED_HIGH; + break; + case 5: /* super */ + case 6: /* super plus */ + libusb_speed = LIBUSB_SPEED_SUPER; + break; + } + } +#endif + udev->speed = speed_map[libusb_speed]; usb_host_speed_compat(s); if (s->ddesc.iProduct) { diff --git a/hw/usb/meson.build b/hw/usb/meson.build index 3c44a1b069..b7c7ff23bf 100644 --- a/hw/usb/meson.build +++ b/hw/usb/meson.build @@ -50,6 +50,13 @@ if config_host.has_key('CONFIG_SMARTCARD') hw_usb_modules += {'smartcard': usbsmartcard_ss} endif +# U2F +softmmu_ss.add(when: 'CONFIG_USB_U2F', if_true: files('u2f.c')) +softmmu_ss.add(when: ['CONFIG_LINUX', 'CONFIG_USB_U2F'], if_true: [libudev, files('u2f-passthru.c')]) +if u2f.found() + softmmu_ss.add(when: 'CONFIG_USB_U2F', if_true: [u2f, files('u2f-emulated.c')]) +endif + # usb redirect if config_host.has_key('CONFIG_USB_REDIR') usbredir_ss = ss.source_set() diff --git a/hw/usb/u2f-emulated.c b/hw/usb/u2f-emulated.c new file mode 100644 index 0000000000..9e1b829f3d --- /dev/null +++ b/hw/usb/u2f-emulated.c @@ -0,0 +1,405 @@ +/* + * U2F USB Emulated device. + * + * Copyright (c) 2020 César Belley + * Written by César Belley + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include "qemu/module.h" +#include "qemu/thread.h" +#include "qemu/main-loop.h" +#include "qapi/error.h" +#include "hw/usb.h" +#include "hw/qdev-properties.h" + +#include + +#include "u2f.h" + +/* Counter which sync with a file */ +struct synced_counter { + /* Emulated device counter */ + struct u2f_emu_vdev_counter vdev_counter; + + /* Private attributes */ + uint32_t value; + FILE *fp; +}; + +static void counter_increment(struct u2f_emu_vdev_counter *vdev_counter) +{ + struct synced_counter *counter = (struct synced_counter *)vdev_counter; + ++counter->value; + + /* Write back */ + if (fseek(counter->fp, 0, SEEK_SET) == -1) { + return; + } + fprintf(counter->fp, "%u\n", counter->value); +} + +static uint32_t counter_read(struct u2f_emu_vdev_counter *vdev_counter) +{ + struct synced_counter *counter = (struct synced_counter *)vdev_counter; + return counter->value; +} + +typedef struct U2FEmulatedState U2FEmulatedState; + +#define PENDING_OUT_NUM 32 + +struct U2FEmulatedState { + U2FKeyState base; + + /* U2F virtual emulated device */ + u2f_emu_vdev *vdev; + QemuMutex vdev_mutex; + + /* Properties */ + char *dir; + char *cert; + char *privkey; + char *entropy; + char *counter; + struct synced_counter synced_counter; + + /* Pending packets received from the guest */ + uint8_t pending_out[PENDING_OUT_NUM][U2FHID_PACKET_SIZE]; + uint8_t pending_out_start; + uint8_t pending_out_end; + uint8_t pending_out_num; + QemuMutex pending_out_mutex; + + /* Emulation thread and sync */ + QemuCond key_cond; + QemuMutex key_mutex; + QemuThread key_thread; + bool stop_thread; + EventNotifier notifier; +}; + +#define TYPE_U2F_EMULATED "u2f-emulated" +#define EMULATED_U2F_KEY(obj) \ + OBJECT_CHECK(U2FEmulatedState, (obj), TYPE_U2F_EMULATED) + +static void u2f_emulated_reset(U2FEmulatedState *key) +{ + key->pending_out_start = 0; + key->pending_out_end = 0; + key->pending_out_num = 0; +} + +static void u2f_pending_out_add(U2FEmulatedState *key, + const uint8_t packet[U2FHID_PACKET_SIZE]) +{ + int index; + + if (key->pending_out_num >= PENDING_OUT_NUM) { + return; + } + + index = key->pending_out_end; + key->pending_out_end = (index + 1) % PENDING_OUT_NUM; + ++key->pending_out_num; + + memcpy(&key->pending_out[index], packet, U2FHID_PACKET_SIZE); +} + +static uint8_t *u2f_pending_out_get(U2FEmulatedState *key) +{ + int index; + + if (key->pending_out_num == 0) { + return NULL; + } + + index = key->pending_out_start; + key->pending_out_start = (index + 1) % PENDING_OUT_NUM; + --key->pending_out_num; + + return key->pending_out[index]; +} + +static void u2f_emulated_recv_from_guest(U2FKeyState *base, + const uint8_t packet[U2FHID_PACKET_SIZE]) +{ + U2FEmulatedState *key = EMULATED_U2F_KEY(base); + + qemu_mutex_lock(&key->pending_out_mutex); + u2f_pending_out_add(key, packet); + qemu_mutex_unlock(&key->pending_out_mutex); + + qemu_mutex_lock(&key->key_mutex); + qemu_cond_signal(&key->key_cond); + qemu_mutex_unlock(&key->key_mutex); +} + +static void *u2f_emulated_thread(void* arg) +{ + U2FEmulatedState *key = arg; + uint8_t packet[U2FHID_PACKET_SIZE]; + uint8_t *packet_out = NULL; + + + while (true) { + /* Wait signal */ + qemu_mutex_lock(&key->key_mutex); + qemu_cond_wait(&key->key_cond, &key->key_mutex); + qemu_mutex_unlock(&key->key_mutex); + + /* Exit thread check */ + if (key->stop_thread) { + key->stop_thread = false; + break; + } + + qemu_mutex_lock(&key->pending_out_mutex); + packet_out = u2f_pending_out_get(key); + if (packet_out == NULL) { + qemu_mutex_unlock(&key->pending_out_mutex); + continue; + } + memcpy(packet, packet_out, U2FHID_PACKET_SIZE); + qemu_mutex_unlock(&key->pending_out_mutex); + + qemu_mutex_lock(&key->vdev_mutex); + u2f_emu_vdev_send(key->vdev, U2F_EMU_USB, packet, + U2FHID_PACKET_SIZE); + + /* Notify response */ + if (u2f_emu_vdev_has_response(key->vdev, U2F_EMU_USB)) { + event_notifier_set(&key->notifier); + } + qemu_mutex_unlock(&key->vdev_mutex); + } + return NULL; +} + +static ssize_t u2f_emulated_read(const char *path, char *buffer, + size_t buffer_len) +{ + int fd; + ssize_t ret; + + fd = qemu_open(path, O_RDONLY); + if (fd < 0) { + return -1; + } + + ret = read(fd, buffer, buffer_len); + close(fd); + + return ret; +} + +static bool u2f_emulated_setup_counter(const char *path, + struct synced_counter *counter) +{ + int fd, ret; + FILE *fp; + + fd = qemu_open(path, O_RDWR); + if (fd < 0) { + return false; + } + fp = fdopen(fd, "r+"); + if (fp == NULL) { + close(fd); + return false; + } + ret = fscanf(fp, "%u", &counter->value); + if (ret == EOF) { + fclose(fp); + return false; + } + counter->fp = fp; + counter->vdev_counter.counter_increment = counter_increment; + counter->vdev_counter.counter_read = counter_read; + + return true; +} + +static u2f_emu_rc u2f_emulated_setup_vdev_manualy(U2FEmulatedState *key) +{ + ssize_t ret; + char cert_pem[4096], privkey_pem[2048]; + struct u2f_emu_vdev_setup setup_info; + + /* Certificate */ + ret = u2f_emulated_read(key->cert, cert_pem, sizeof(cert_pem)); + if (ret < 0) { + return -1; + } + + /* Private key */ + ret = u2f_emulated_read(key->privkey, privkey_pem, sizeof(privkey_pem)); + if (ret < 0) { + return -1; + } + + /* Entropy */ + ret = u2f_emulated_read(key->entropy, (char *)&setup_info.entropy, + sizeof(setup_info.entropy)); + if (ret < 0) { + return -1; + } + + /* Counter */ + if (!u2f_emulated_setup_counter(key->counter, &key->synced_counter)) { + return -1; + } + + /* Setup */ + setup_info.certificate = cert_pem; + setup_info.private_key = privkey_pem; + setup_info.counter = (struct u2f_emu_vdev_counter *)&key->synced_counter; + + return u2f_emu_vdev_new(&key->vdev, &setup_info); +} + +static void u2f_emulated_event_handler(EventNotifier *notifier) +{ + U2FEmulatedState *key = container_of(notifier, U2FEmulatedState, notifier); + size_t packet_size; + uint8_t *packet_in = NULL; + + event_notifier_test_and_clear(&key->notifier); + qemu_mutex_lock(&key->vdev_mutex); + while (u2f_emu_vdev_has_response(key->vdev, U2F_EMU_USB)) { + packet_size = u2f_emu_vdev_get_response(key->vdev, U2F_EMU_USB, + &packet_in); + if (packet_size == U2FHID_PACKET_SIZE) { + u2f_send_to_guest(&key->base, packet_in); + } + u2f_emu_vdev_free_response(packet_in); + } + qemu_mutex_unlock(&key->vdev_mutex); +} + +static void u2f_emulated_realize(U2FKeyState *base, Error **errp) +{ + U2FEmulatedState *key = EMULATED_U2F_KEY(base); + u2f_emu_rc rc; + + if (key->cert != NULL || key->privkey != NULL || key->entropy != NULL + || key->counter != NULL) { + if (key->cert != NULL && key->privkey != NULL + && key->entropy != NULL && key->counter != NULL) { + rc = u2f_emulated_setup_vdev_manualy(key); + } else { + error_setg(errp, "%s: cert, priv, entropy and counter " + "parameters must be provided to manualy configure " + "the emulated device", TYPE_U2F_EMULATED); + return; + } + } else if (key->dir != NULL) { + rc = u2f_emu_vdev_new_from_dir(&key->vdev, key->dir); + } else { + rc = u2f_emu_vdev_new_ephemeral(&key->vdev); + } + + if (rc != U2F_EMU_OK) { + error_setg(errp, "%s: Failed to setup the key", TYPE_U2F_EMULATED); + return; + } + + if (event_notifier_init(&key->notifier, false) < 0) { + error_setg(errp, "%s: Failed to initialize notifier", + TYPE_U2F_EMULATED); + return; + } + /* Notifier */ + event_notifier_set_handler(&key->notifier, u2f_emulated_event_handler); + + /* Synchronization */ + qemu_cond_init(&key->key_cond); + qemu_mutex_init(&key->vdev_mutex); + qemu_mutex_init(&key->pending_out_mutex); + qemu_mutex_init(&key->key_mutex); + u2f_emulated_reset(key); + + /* Thread */ + key->stop_thread = false; + qemu_thread_create(&key->key_thread, "u2f-key", u2f_emulated_thread, + key, QEMU_THREAD_JOINABLE); +} + +static void u2f_emulated_unrealize(U2FKeyState *base) +{ + U2FEmulatedState *key = EMULATED_U2F_KEY(base); + + /* Thread */ + key->stop_thread = true; + qemu_cond_signal(&key->key_cond); + qemu_thread_join(&key->key_thread); + + /* Notifier */ + event_notifier_set_handler(&key->notifier, NULL); + event_notifier_cleanup(&key->notifier); + + /* Synchronization */ + qemu_cond_destroy(&key->key_cond); + qemu_mutex_destroy(&key->vdev_mutex); + qemu_mutex_destroy(&key->key_mutex); + qemu_mutex_destroy(&key->pending_out_mutex); + + /* Vdev */ + u2f_emu_vdev_free(key->vdev); + if (key->synced_counter.fp != NULL) { + fclose(key->synced_counter.fp); + } +} + +static Property u2f_emulated_properties[] = { + DEFINE_PROP_STRING("dir", U2FEmulatedState, dir), + DEFINE_PROP_STRING("cert", U2FEmulatedState, cert), + DEFINE_PROP_STRING("privkey", U2FEmulatedState, privkey), + DEFINE_PROP_STRING("entropy", U2FEmulatedState, entropy), + DEFINE_PROP_STRING("counter", U2FEmulatedState, counter), + DEFINE_PROP_END_OF_LIST(), +}; + +static void u2f_emulated_class_init(ObjectClass *klass, void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + U2FKeyClass *kc = U2F_KEY_CLASS(klass); + + kc->realize = u2f_emulated_realize; + kc->unrealize = u2f_emulated_unrealize; + kc->recv_from_guest = u2f_emulated_recv_from_guest; + dc->desc = "QEMU U2F emulated key"; + device_class_set_props(dc, u2f_emulated_properties); +} + +static const TypeInfo u2f_key_emulated_info = { + .name = TYPE_U2F_EMULATED, + .parent = TYPE_U2F_KEY, + .instance_size = sizeof(U2FEmulatedState), + .class_init = u2f_emulated_class_init +}; + +static void u2f_key_emulated_register_types(void) +{ + type_register_static(&u2f_key_emulated_info); +} + +type_init(u2f_key_emulated_register_types) diff --git a/hw/usb/u2f-passthru.c b/hw/usb/u2f-passthru.c new file mode 100644 index 0000000000..e9c8aa4595 --- /dev/null +++ b/hw/usb/u2f-passthru.c @@ -0,0 +1,551 @@ +/* + * U2F USB Passthru device. + * + * Copyright (c) 2020 César Belley + * Written by César Belley + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include "qemu/module.h" +#include "qemu/main-loop.h" +#include "qemu/error-report.h" +#include "qapi/error.h" +#include "hw/qdev-properties.h" +#include "hw/usb.h" +#include "migration/vmstate.h" + +#include "u2f.h" + +#ifdef CONFIG_LIBUDEV +#include +#endif +#include +#include + +#define NONCE_SIZE 8 +#define BROADCAST_CID 0xFFFFFFFF +#define TRANSACTION_TIMEOUT 120000 + +struct transaction { + uint32_t cid; + uint16_t resp_bcnt; + uint16_t resp_size; + + /* Nonce for broadcast isolation */ + uint8_t nonce[NONCE_SIZE]; +}; + +typedef struct U2FPassthruState U2FPassthruState; + +#define CURRENT_TRANSACTIONS_NUM 4 + +struct U2FPassthruState { + U2FKeyState base; + + /* Host device */ + char *hidraw; + int hidraw_fd; + + /* Current Transactions */ + struct transaction current_transactions[CURRENT_TRANSACTIONS_NUM]; + uint8_t current_transactions_start; + uint8_t current_transactions_end; + uint8_t current_transactions_num; + + /* Transaction time checking */ + int64_t last_transaction_time; + QEMUTimer timer; +}; + +#define TYPE_U2F_PASSTHRU "u2f-passthru" +#define PASSTHRU_U2F_KEY(obj) \ + OBJECT_CHECK(U2FPassthruState, (obj), TYPE_U2F_PASSTHRU) + +/* Init packet sizes */ +#define PACKET_INIT_HEADER_SIZE 7 +#define PACKET_INIT_DATA_SIZE (U2FHID_PACKET_SIZE - PACKET_INIT_HEADER_SIZE) + +/* Cont packet sizes */ +#define PACKET_CONT_HEADER_SIZE 5 +#define PACKET_CONT_DATA_SIZE (U2FHID_PACKET_SIZE - PACKET_CONT_HEADER_SIZE) + +struct packet_init { + uint32_t cid; + uint8_t cmd; + uint8_t bcnth; + uint8_t bcntl; + uint8_t data[PACKET_INIT_DATA_SIZE]; +} QEMU_PACKED; + +static inline uint32_t packet_get_cid(const void *packet) +{ + return *((uint32_t *)packet); +} + +static inline bool packet_is_init(const void *packet) +{ + return ((uint8_t *)packet)[4] & (1 << 7); +} + +static inline uint16_t packet_init_get_bcnt( + const struct packet_init *packet_init) +{ + uint16_t bcnt = 0; + bcnt |= packet_init->bcnth << 8; + bcnt |= packet_init->bcntl; + + return bcnt; +} + +static void u2f_passthru_reset(U2FPassthruState *key) +{ + timer_del(&key->timer); + qemu_set_fd_handler(key->hidraw_fd, NULL, NULL, key); + key->last_transaction_time = 0; + key->current_transactions_start = 0; + key->current_transactions_end = 0; + key->current_transactions_num = 0; +} + +static void u2f_timeout_check(void *opaque) +{ + U2FPassthruState *key = opaque; + int64_t time = qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL); + + if (time > key->last_transaction_time + TRANSACTION_TIMEOUT) { + u2f_passthru_reset(key); + } else { + timer_mod(&key->timer, time + TRANSACTION_TIMEOUT / 4); + } +} + +static int u2f_transaction_get_index(U2FPassthruState *key, uint32_t cid) +{ + for (int i = 0; i < key->current_transactions_num; ++i) { + int index = (key->current_transactions_start + i) + % CURRENT_TRANSACTIONS_NUM; + if (cid == key->current_transactions[index].cid) { + return index; + } + } + return -1; +} + +static struct transaction *u2f_transaction_get(U2FPassthruState *key, + uint32_t cid) +{ + int index = u2f_transaction_get_index(key, cid); + if (index < 0) { + return NULL; + } + return &key->current_transactions[index]; +} + +static struct transaction *u2f_transaction_get_from_nonce(U2FPassthruState *key, + const uint8_t nonce[NONCE_SIZE]) +{ + for (int i = 0; i < key->current_transactions_num; ++i) { + int index = (key->current_transactions_start + i) + % CURRENT_TRANSACTIONS_NUM; + if (key->current_transactions[index].cid == BROADCAST_CID + && memcmp(nonce, key->current_transactions[index].nonce, + NONCE_SIZE) == 0) { + return &key->current_transactions[index]; + } + } + return NULL; +} + +static void u2f_transaction_close(U2FPassthruState *key, uint32_t cid) +{ + int index, next_index; + index = u2f_transaction_get_index(key, cid); + if (index < 0) { + return; + } + next_index = (index + 1) % CURRENT_TRANSACTIONS_NUM; + + /* Rearrange to ensure the oldest is at the start position */ + while (next_index != key->current_transactions_end) { + memcpy(&key->current_transactions[index], + &key->current_transactions[next_index], + sizeof(struct transaction)); + + index = next_index; + next_index = (index + 1) % CURRENT_TRANSACTIONS_NUM; + } + + key->current_transactions_end = index; + --key->current_transactions_num; + + if (key->current_transactions_num == 0) { + u2f_passthru_reset(key); + } +} + +static void u2f_transaction_add(U2FPassthruState *key, uint32_t cid, + const uint8_t nonce[NONCE_SIZE]) +{ + uint8_t index; + struct transaction *transaction; + + if (key->current_transactions_num >= CURRENT_TRANSACTIONS_NUM) { + /* Close the oldest transaction */ + index = key->current_transactions_start; + transaction = &key->current_transactions[index]; + u2f_transaction_close(key, transaction->cid); + } + + /* Index */ + index = key->current_transactions_end; + key->current_transactions_end = (index + 1) % CURRENT_TRANSACTIONS_NUM; + ++key->current_transactions_num; + + /* Transaction */ + transaction = &key->current_transactions[index]; + transaction->cid = cid; + transaction->resp_bcnt = 0; + transaction->resp_size = 0; + + /* Nonce */ + if (nonce != NULL) { + memcpy(transaction->nonce, nonce, NONCE_SIZE); + } +} + +static void u2f_passthru_read(void *opaque); + +static void u2f_transaction_start(U2FPassthruState *key, + const struct packet_init *packet_init) +{ + int64_t time; + + /* Transaction */ + if (packet_init->cid == BROADCAST_CID) { + u2f_transaction_add(key, packet_init->cid, packet_init->data); + } else { + u2f_transaction_add(key, packet_init->cid, NULL); + } + + /* Time */ + time = qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL); + if (key->last_transaction_time == 0) { + qemu_set_fd_handler(key->hidraw_fd, u2f_passthru_read, NULL, key); + timer_init_ms(&key->timer, QEMU_CLOCK_VIRTUAL, u2f_timeout_check, key); + timer_mod(&key->timer, time + TRANSACTION_TIMEOUT / 4); + } + key->last_transaction_time = time; +} + +static void u2f_passthru_recv_from_host(U2FPassthruState *key, + const uint8_t packet[U2FHID_PACKET_SIZE]) +{ + struct transaction *transaction; + uint32_t cid; + + /* Retrieve transaction */ + cid = packet_get_cid(packet); + if (cid == BROADCAST_CID) { + struct packet_init *packet_init; + if (!packet_is_init(packet)) { + return; + } + packet_init = (struct packet_init *)packet; + transaction = u2f_transaction_get_from_nonce(key, packet_init->data); + } else { + transaction = u2f_transaction_get(key, cid); + } + + /* Ignore no started transaction */ + if (transaction == NULL) { + return; + } + + if (packet_is_init(packet)) { + struct packet_init *packet_init = (struct packet_init *)packet; + transaction->resp_bcnt = packet_init_get_bcnt(packet_init); + transaction->resp_size = PACKET_INIT_DATA_SIZE; + + if (packet_init->cid == BROADCAST_CID) { + /* Nonce checking for legitimate response */ + if (memcmp(transaction->nonce, packet_init->data, NONCE_SIZE) + != 0) { + return; + } + } + } else { + transaction->resp_size += PACKET_CONT_DATA_SIZE; + } + + /* Transaction end check */ + if (transaction->resp_size >= transaction->resp_bcnt) { + u2f_transaction_close(key, cid); + } + u2f_send_to_guest(&key->base, packet); +} + +static void u2f_passthru_read(void *opaque) +{ + U2FPassthruState *key = opaque; + U2FKeyState *base = &key->base; + uint8_t packet[2 * U2FHID_PACKET_SIZE]; + int ret; + + /* Full size base queue check */ + if (base->pending_in_num >= U2FHID_PENDING_IN_NUM) { + return; + } + + ret = read(key->hidraw_fd, packet, sizeof(packet)); + if (ret < 0) { + /* Detach */ + if (base->dev.attached) { + usb_device_detach(&base->dev); + u2f_passthru_reset(key); + } + return; + } + if (ret != U2FHID_PACKET_SIZE) { + return; + } + u2f_passthru_recv_from_host(key, packet); +} + +static void u2f_passthru_recv_from_guest(U2FKeyState *base, + const uint8_t packet[U2FHID_PACKET_SIZE]) +{ + U2FPassthruState *key = PASSTHRU_U2F_KEY(base); + uint8_t host_packet[U2FHID_PACKET_SIZE + 1]; + ssize_t written; + + if (packet_is_init(packet)) { + u2f_transaction_start(key, (struct packet_init *)packet); + } + + host_packet[0] = 0; + memcpy(host_packet + 1, packet, U2FHID_PACKET_SIZE); + + written = write(key->hidraw_fd, host_packet, sizeof(host_packet)); + if (written != sizeof(host_packet)) { + error_report("%s: Bad written size (req 0x%zu, val 0x%zd)", + TYPE_U2F_PASSTHRU, sizeof(host_packet), written); + } +} + +static bool u2f_passthru_is_u2f_device(int fd) +{ + int ret, rdesc_size; + struct hidraw_report_descriptor rdesc; + const uint8_t u2f_hid_report_desc_header[] = { + 0x06, 0xd0, 0xf1, /* Usage Page (FIDO) */ + 0x09, 0x01, /* Usage (FIDO) */ + }; + + /* Get report descriptor size */ + ret = ioctl(fd, HIDIOCGRDESCSIZE, &rdesc_size); + if (ret < 0 || rdesc_size < sizeof(u2f_hid_report_desc_header)) { + return false; + } + + /* Get report descriptor */ + memset(&rdesc, 0x0, sizeof(rdesc)); + rdesc.size = rdesc_size; + ret = ioctl(fd, HIDIOCGRDESC, &rdesc); + if (ret < 0) { + return false; + } + + /* Header bytes cover specific U2F rdesc values */ + return memcmp(u2f_hid_report_desc_header, rdesc.value, + sizeof(u2f_hid_report_desc_header)) == 0; +} + +#ifdef CONFIG_LIBUDEV +static int u2f_passthru_open_from_device(struct udev_device *device) +{ + const char *devnode = udev_device_get_devnode(device); + + int fd = qemu_open(devnode, O_RDWR); + if (fd < 0) { + return -1; + } else if (!u2f_passthru_is_u2f_device(fd)) { + qemu_close(fd); + return -1; + } + return fd; +} + +static int u2f_passthru_open_from_enumerate(struct udev *udev, + struct udev_enumerate *enumerate) +{ + struct udev_list_entry *devices, *entry; + int ret, fd; + + ret = udev_enumerate_scan_devices(enumerate); + if (ret < 0) { + return -1; + } + + devices = udev_enumerate_get_list_entry(enumerate); + udev_list_entry_foreach(entry, devices) { + struct udev_device *device; + const char *syspath = udev_list_entry_get_name(entry); + + if (syspath == NULL) { + continue; + } + + device = udev_device_new_from_syspath(udev, syspath); + if (device == NULL) { + continue; + } + + fd = u2f_passthru_open_from_device(device); + udev_device_unref(device); + if (fd >= 0) { + return fd; + } + } + return -1; +} + +static int u2f_passthru_open_from_scan(void) +{ + struct udev *udev; + struct udev_enumerate *enumerate; + int ret, fd = -1; + + udev = udev_new(); + if (udev == NULL) { + return -1; + } + + enumerate = udev_enumerate_new(udev); + if (enumerate == NULL) { + udev_unref(udev); + return -1; + } + + ret = udev_enumerate_add_match_subsystem(enumerate, "hidraw"); + if (ret >= 0) { + fd = u2f_passthru_open_from_enumerate(udev, enumerate); + } + + udev_enumerate_unref(enumerate); + udev_unref(udev); + + return fd; +} +#endif + +static void u2f_passthru_unrealize(U2FKeyState *base) +{ + U2FPassthruState *key = PASSTHRU_U2F_KEY(base); + + u2f_passthru_reset(key); + qemu_close(key->hidraw_fd); +} + +static void u2f_passthru_realize(U2FKeyState *base, Error **errp) +{ + U2FPassthruState *key = PASSTHRU_U2F_KEY(base); + int fd; + + if (key->hidraw == NULL) { +#ifdef CONFIG_LIBUDEV + fd = u2f_passthru_open_from_scan(); + if (fd < 0) { + error_setg(errp, "%s: Failed to find a U2F USB device", + TYPE_U2F_PASSTHRU); + return; + } +#else + error_setg(errp, "%s: Missing hidraw", TYPE_U2F_PASSTHRU); + return; +#endif + } else { + fd = qemu_open(key->hidraw, O_RDWR); + if (fd < 0) { + error_setg(errp, "%s: Failed to open %s", TYPE_U2F_PASSTHRU, + key->hidraw); + return; + } + + if (!u2f_passthru_is_u2f_device(fd)) { + qemu_close(fd); + error_setg(errp, "%s: Passed hidraw does not represent " + "a U2F HID device", TYPE_U2F_PASSTHRU); + return; + } + } + key->hidraw_fd = fd; + u2f_passthru_reset(key); +} + +static int u2f_passthru_post_load(void *opaque, int version_id) +{ + U2FPassthruState *key = opaque; + u2f_passthru_reset(key); + return 0; +} + +static const VMStateDescription u2f_passthru_vmstate = { + .name = "u2f-key-passthru", + .version_id = 1, + .minimum_version_id = 1, + .post_load = u2f_passthru_post_load, + .fields = (VMStateField[]) { + VMSTATE_U2F_KEY(base, U2FPassthruState), + VMSTATE_END_OF_LIST() + } +}; + +static Property u2f_passthru_properties[] = { + DEFINE_PROP_STRING("hidraw", U2FPassthruState, hidraw), + DEFINE_PROP_END_OF_LIST(), +}; + +static void u2f_passthru_class_init(ObjectClass *klass, void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + U2FKeyClass *kc = U2F_KEY_CLASS(klass); + + kc->realize = u2f_passthru_realize; + kc->unrealize = u2f_passthru_unrealize; + kc->recv_from_guest = u2f_passthru_recv_from_guest; + dc->desc = "QEMU U2F passthrough key"; + dc->vmsd = &u2f_passthru_vmstate; + device_class_set_props(dc, u2f_passthru_properties); +} + +static const TypeInfo u2f_key_passthru_info = { + .name = TYPE_U2F_PASSTHRU, + .parent = TYPE_U2F_KEY, + .instance_size = sizeof(U2FPassthruState), + .class_init = u2f_passthru_class_init +}; + +static void u2f_key_passthru_register_types(void) +{ + type_register_static(&u2f_key_passthru_info); +} + +type_init(u2f_key_passthru_register_types) diff --git a/hw/usb/u2f.c b/hw/usb/u2f.c new file mode 100644 index 0000000000..bc09191f06 --- /dev/null +++ b/hw/usb/u2f.c @@ -0,0 +1,352 @@ +/* + * U2F USB device. + * + * Copyright (c) 2020 César Belley + * Written by César Belley + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "qemu/osdep.h" +#include "qemu/module.h" +#include "qapi/error.h" +#include "hw/usb.h" +#include "hw/usb/hid.h" +#include "migration/vmstate.h" +#include "desc.h" + +#include "u2f.h" + +/* U2F key Vendor / Product */ +#define U2F_KEY_VENDOR_NUM 0x46f4 /* CRC16() of "QEMU" */ +#define U2F_KEY_PRODUCT_NUM 0x0005 + +enum { + STR_MANUFACTURER = 1, + STR_PRODUCT, + STR_SERIALNUMBER, + STR_CONFIG, + STR_INTERFACE +}; + +static const USBDescStrings desc_strings = { + [STR_MANUFACTURER] = "QEMU", + [STR_PRODUCT] = "U2F USB key", + [STR_SERIALNUMBER] = "0", + [STR_CONFIG] = "U2F key config", + [STR_INTERFACE] = "U2F key interface" +}; + +static const USBDescIface desc_iface_u2f_key = { + .bInterfaceNumber = 0, + .bNumEndpoints = 2, + .bInterfaceClass = USB_CLASS_HID, + .bInterfaceSubClass = 0x0, + .bInterfaceProtocol = 0x0, + .ndesc = 1, + .descs = (USBDescOther[]) { + { + /* HID descriptor */ + .data = (uint8_t[]) { + 0x09, /* u8 bLength */ + USB_DT_HID, /* u8 bDescriptorType */ + 0x10, 0x01, /* u16 HID_class */ + 0x00, /* u8 country_code */ + 0x01, /* u8 num_descriptors */ + USB_DT_REPORT, /* u8 type: Report */ + 0x22, 0, /* u16 len */ + }, + }, + }, + .eps = (USBDescEndpoint[]) { + { + .bEndpointAddress = USB_DIR_IN | 0x01, + .bmAttributes = USB_ENDPOINT_XFER_INT, + .wMaxPacketSize = U2FHID_PACKET_SIZE, + .bInterval = 0x05, + }, { + .bEndpointAddress = USB_DIR_OUT | 0x01, + .bmAttributes = USB_ENDPOINT_XFER_INT, + .wMaxPacketSize = U2FHID_PACKET_SIZE, + .bInterval = 0x05, + }, + }, + +}; + +static const USBDescDevice desc_device_u2f_key = { + .bcdUSB = 0x0100, + .bMaxPacketSize0 = U2FHID_PACKET_SIZE, + .bNumConfigurations = 1, + .confs = (USBDescConfig[]) { + { + .bNumInterfaces = 1, + .bConfigurationValue = 1, + .iConfiguration = STR_CONFIG, + .bmAttributes = USB_CFG_ATT_ONE, + .bMaxPower = 15, + .nif = 1, + .ifs = &desc_iface_u2f_key, + }, + }, +}; + +static const USBDesc desc_u2f_key = { + .id = { + .idVendor = U2F_KEY_VENDOR_NUM, + .idProduct = U2F_KEY_PRODUCT_NUM, + .bcdDevice = 0, + .iManufacturer = STR_MANUFACTURER, + .iProduct = STR_PRODUCT, + .iSerialNumber = STR_SERIALNUMBER, + }, + .full = &desc_device_u2f_key, + .str = desc_strings, +}; + +static const uint8_t u2f_key_hid_report_desc[] = { + 0x06, 0xd0, 0xf1, /* Usage Page (FIDO) */ + 0x09, 0x01, /* Usage (FIDO) */ + 0xa1, 0x01, /* Collection (HID Application) */ + 0x09, 0x20, /* Usage (FIDO data in) */ + 0x15, 0x00, /* Logical Minimum (0) */ + 0x26, 0xFF, 0x00, /* Logical Maximum (0xff) */ + 0x75, 0x08, /* Report Size (8) */ + 0x95, 0x40, /* Report Count (0x40) */ + 0x81, 0x02, /* Input (Data, Variable, Absolute) */ + 0x09, 0x21, /* Usage (FIDO data out) */ + 0x15, 0x00, /* Logical Minimum (0) */ + 0x26, 0xFF, 0x00, /* Logical Maximum (0xFF) */ + 0x75, 0x08, /* Report Size (8) */ + 0x95, 0x40, /* Report Count (0x40) */ + 0x91, 0x02, /* Output (Data, Variable, Absolute) */ + 0xC0 /* End Collection */ +}; + +static void u2f_key_reset(U2FKeyState *key) +{ + key->pending_in_start = 0; + key->pending_in_end = 0; + key->pending_in_num = 0; +} + +static void u2f_key_handle_reset(USBDevice *dev) +{ + U2FKeyState *key = U2F_KEY(dev); + + u2f_key_reset(key); +} + +static void u2f_key_handle_control(USBDevice *dev, USBPacket *p, + int request, int value, int index, int length, uint8_t *data) +{ + U2FKeyState *key = U2F_KEY(dev); + int ret; + + ret = usb_desc_handle_control(dev, p, request, value, index, length, data); + if (ret >= 0) { + return; + } + + switch (request) { + case InterfaceRequest | USB_REQ_GET_DESCRIPTOR: + switch (value >> 8) { + case 0x22: + memcpy(data, u2f_key_hid_report_desc, + sizeof(u2f_key_hid_report_desc)); + p->actual_length = sizeof(u2f_key_hid_report_desc); + break; + default: + goto fail; + } + break; + case HID_GET_IDLE: + data[0] = key->idle; + p->actual_length = 1; + break; + case HID_SET_IDLE: + key->idle = (uint8_t)(value >> 8); + break; + default: + fail: + p->status = USB_RET_STALL; + break; + } + +} + +static void u2f_key_recv_from_guest(U2FKeyState *key, USBPacket *p) +{ + U2FKeyClass *kc = U2F_KEY_GET_CLASS(key); + uint8_t packet[U2FHID_PACKET_SIZE]; + + if (kc->recv_from_guest == NULL || p->iov.size != U2FHID_PACKET_SIZE) { + return; + } + + usb_packet_copy(p, packet, p->iov.size); + kc->recv_from_guest(key, packet); +} + +static void u2f_pending_in_add(U2FKeyState *key, + const uint8_t packet[U2FHID_PACKET_SIZE]) +{ + uint8_t index; + + if (key->pending_in_num >= U2FHID_PENDING_IN_NUM) { + return; + } + + index = key->pending_in_end; + key->pending_in_end = (index + 1) % U2FHID_PENDING_IN_NUM; + ++key->pending_in_num; + + memcpy(key->pending_in[index], packet, U2FHID_PACKET_SIZE); +} + +static uint8_t *u2f_pending_in_get(U2FKeyState *key) +{ + uint8_t index; + + if (key->pending_in_num == 0) { + return NULL; + } + + index = key->pending_in_start; + key->pending_in_start = (index + 1) % U2FHID_PENDING_IN_NUM; + --key->pending_in_num; + + return key->pending_in[index]; +} + +static void u2f_key_handle_data(USBDevice *dev, USBPacket *p) +{ + U2FKeyState *key = U2F_KEY(dev); + uint8_t *packet_in; + + /* Endpoint number check */ + if (p->ep->nr != 1) { + p->status = USB_RET_STALL; + return; + } + + switch (p->pid) { + case USB_TOKEN_OUT: + u2f_key_recv_from_guest(key, p); + break; + case USB_TOKEN_IN: + packet_in = u2f_pending_in_get(key); + if (packet_in == NULL) { + p->status = USB_RET_NAK; + return; + } + usb_packet_copy(p, packet_in, U2FHID_PACKET_SIZE); + break; + default: + p->status = USB_RET_STALL; + break; + } +} + +void u2f_send_to_guest(U2FKeyState *key, + const uint8_t packet[U2FHID_PACKET_SIZE]) +{ + u2f_pending_in_add(key, packet); + usb_wakeup(key->ep, 0); +} + +static void u2f_key_unrealize(USBDevice *dev) +{ + U2FKeyState *key = U2F_KEY(dev); + U2FKeyClass *kc = U2F_KEY_GET_CLASS(key); + + if (kc->unrealize != NULL) { + kc->unrealize(key); + } +} + +static void u2f_key_realize(USBDevice *dev, Error **errp) +{ + U2FKeyState *key = U2F_KEY(dev); + U2FKeyClass *kc = U2F_KEY_GET_CLASS(key); + Error *local_err = NULL; + + usb_desc_create_serial(dev); + usb_desc_init(dev); + u2f_key_reset(key); + + if (kc->realize != NULL) { + kc->realize(key, &local_err); + if (local_err != NULL) { + error_propagate(errp, local_err); + return; + } + } + key->ep = usb_ep_get(dev, USB_TOKEN_IN, 1); +} + +const VMStateDescription vmstate_u2f_key = { + .name = "u2f-key", + .version_id = 1, + .minimum_version_id = 1, + .fields = (VMStateField[]) { + VMSTATE_USB_DEVICE(dev, U2FKeyState), + VMSTATE_UINT8(idle, U2FKeyState), + VMSTATE_UINT8_2DARRAY(pending_in, U2FKeyState, + U2FHID_PENDING_IN_NUM, U2FHID_PACKET_SIZE), + VMSTATE_UINT8(pending_in_start, U2FKeyState), + VMSTATE_UINT8(pending_in_end, U2FKeyState), + VMSTATE_UINT8(pending_in_num, U2FKeyState), + VMSTATE_END_OF_LIST() + } +}; + +static void u2f_key_class_init(ObjectClass *klass, void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + USBDeviceClass *uc = USB_DEVICE_CLASS(klass); + + uc->product_desc = "QEMU U2F USB key"; + uc->usb_desc = &desc_u2f_key; + uc->handle_reset = u2f_key_handle_reset; + uc->handle_control = u2f_key_handle_control; + uc->handle_data = u2f_key_handle_data; + uc->handle_attach = usb_desc_attach; + uc->realize = u2f_key_realize; + uc->unrealize = u2f_key_unrealize; + dc->desc = "QEMU U2F key"; + dc->vmsd = &vmstate_u2f_key; +} + +static const TypeInfo u2f_key_info = { + .name = TYPE_U2F_KEY, + .parent = TYPE_USB_DEVICE, + .instance_size = sizeof(U2FKeyState), + .abstract = true, + .class_size = sizeof(U2FKeyClass), + .class_init = u2f_key_class_init, +}; + +static void u2f_key_register_types(void) +{ + type_register_static(&u2f_key_info); + usb_legacy_register(TYPE_U2F_KEY, "u2f-key", NULL); +} + +type_init(u2f_key_register_types) diff --git a/hw/usb/u2f.h b/hw/usb/u2f.h new file mode 100644 index 0000000000..db30f3586b --- /dev/null +++ b/hw/usb/u2f.h @@ -0,0 +1,92 @@ +/* + * U2F USB device. + * + * Copyright (c) 2020 César Belley + * Written by César Belley + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef U2F_H +#define U2F_H + +#include "hw/qdev-core.h" + +#define U2FHID_PACKET_SIZE 64 +#define U2FHID_PENDING_IN_NUM 32 + +typedef struct U2FKeyState U2FKeyState; +typedef struct U2FKeyInfo U2FKeyInfo; + +#define TYPE_U2F_KEY "u2f-key" +#define U2F_KEY(obj) \ + OBJECT_CHECK(U2FKeyState, (obj), TYPE_U2F_KEY) +#define U2F_KEY_CLASS(klass) \ + OBJECT_CLASS_CHECK(U2FKeyClass, (klass), TYPE_U2F_KEY) +#define U2F_KEY_GET_CLASS(obj) \ + OBJECT_GET_CLASS(U2FKeyClass, (obj), TYPE_U2F_KEY) + +/* + * Callbacks to be used by the U2F key base device (i.e. hw/u2f.c) + * to interact with its variants (i.e. hw/u2f-*.c) + */ +typedef struct U2FKeyClass { + /*< private >*/ + USBDeviceClass parent_class; + + /*< public >*/ + void (*recv_from_guest)(U2FKeyState *key, + const uint8_t packet[U2FHID_PACKET_SIZE]); + void (*realize)(U2FKeyState *key, Error **errp); + void (*unrealize)(U2FKeyState *key); +} U2FKeyClass; + +/* + * State of the U2F key base device (i.e. hw/u2f.c) + */ +typedef struct U2FKeyState { + USBDevice dev; + USBEndpoint *ep; + uint8_t idle; + + /* Pending packets to be send to the guest */ + uint8_t pending_in[U2FHID_PENDING_IN_NUM][U2FHID_PACKET_SIZE]; + uint8_t pending_in_start; + uint8_t pending_in_end; + uint8_t pending_in_num; +} U2FKeyState; + +/* + * API to be used by the U2F key device variants (i.e. hw/u2f-*.c) + * to interact with the the U2F key base device (i.e. hw/u2f.c) + */ +void u2f_send_to_guest(U2FKeyState *key, + const uint8_t packet[U2FHID_PACKET_SIZE]); + +extern const VMStateDescription vmstate_u2f_key; + +#define VMSTATE_U2F_KEY(_field, _state) { \ + .name = (stringify(_field)), \ + .size = sizeof(U2FKeyState), \ + .vmsd = &vmstate_u2f_key, \ + .flags = VMS_STRUCT, \ + .offset = vmstate_offset_value(_state, _field, U2FKeyState), \ +} + +#endif /* U2F_H */ diff --git a/include/hw/usb/hid.h b/include/hw/usb/hid.h new file mode 100644 index 0000000000..1c142584ff --- /dev/null +++ b/include/hw/usb/hid.h @@ -0,0 +1,17 @@ +#ifndef HW_USB_HID_H +#define HW_USB_HID_H + +/* HID interface requests */ +#define HID_GET_REPORT 0xa101 +#define HID_GET_IDLE 0xa102 +#define HID_GET_PROTOCOL 0xa103 +#define HID_SET_REPORT 0x2109 +#define HID_SET_IDLE 0x210a +#define HID_SET_PROTOCOL 0x210b + +/* HID descriptor types */ +#define USB_DT_HID 0x21 +#define USB_DT_REPORT 0x22 +#define USB_DT_PHY 0x23 + +#endif diff --git a/meson.build b/meson.build index 74f8ea0c2e..1e7aee85e3 100644 --- a/meson.build +++ b/meson.build @@ -377,6 +377,12 @@ if 'CONFIG_SMARTCARD' in config_host cacard = declare_dependency(compile_args: config_host['SMARTCARD_CFLAGS'].split(), link_args: config_host['SMARTCARD_LIBS'].split()) endif +u2f = not_found +if have_system + u2f = dependency('u2f-emu', required: get_option('u2f'), + method: 'pkg-config', + static: enable_static) +endif usbredir = not_found if 'CONFIG_USB_REDIR' in config_host usbredir = declare_dependency(compile_args: config_host['USB_REDIR_CFLAGS'].split(), @@ -1375,6 +1381,7 @@ summary_info += {'spice support': config_host.has_key('CONFIG_SPICE')} summary_info += {'rbd support': config_host.has_key('CONFIG_RBD')} summary_info += {'xfsctl support': config_host.has_key('CONFIG_XFS')} summary_info += {'smartcard support': config_host.has_key('CONFIG_SMARTCARD')} +summary_info += {'U2F support': u2f.found()} summary_info += {'libusb': config_host.has_key('CONFIG_USB_LIBUSB')} summary_info += {'usb net redir': config_host.has_key('CONFIG_USB_REDIR')} summary_info += {'OpenGL support': config_host.has_key('CONFIG_OPENGL')} diff --git a/meson_options.txt b/meson_options.txt index c55f9cd94c..aef2de6523 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,6 +1,7 @@ option('gettext', type : 'boolean', value : true) option('sdl', type : 'feature', value : 'auto') option('sdl_image', type : 'feature', value : 'auto') +option('u2f', type : 'feature', value : 'auto') option('vnc', type : 'feature', value : 'enabled') option('vnc_jpeg', type : 'feature', value : 'auto') option('vnc_png', type : 'feature', value : 'auto') diff --git a/scripts/u2f-setup-gen.py b/scripts/u2f-setup-gen.py new file mode 100755 index 0000000000..2122598fed --- /dev/null +++ b/scripts/u2f-setup-gen.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# +# Libu2f-emu setup directory generator for USB U2F key emulation. +# +# Copyright (c) 2020 César Belley +# Written by César Belley +# +# This work is licensed under the terms of the GNU GPL, version 2 +# or, at your option, any later version. See the COPYING file in +# the top-level directory. + +import sys +import os +from random import randint +from typing import Tuple + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.serialization import Encoding, \ + NoEncryption, PrivateFormat, PublicFormat +from OpenSSL import crypto + + +def write_setup_dir(dirpath: str, privkey_pem: bytes, cert_pem: bytes, + entropy: bytes, counter: int) -> None: + """ + Write the setup directory. + + Args: + dirpath: The directory path. + key_pem: The private key PEM. + cert_pem: The certificate PEM. + entropy: The 48 bytes of entropy. + counter: The counter value. + """ + # Directory + os.mkdir(dirpath) + + # Private key + with open(f'{dirpath}/private-key.pem', 'bw') as f: + f.write(privkey_pem) + + # Certificate + with open(f'{dirpath}/certificate.pem', 'bw') as f: + f.write(cert_pem) + + # Entropy + with open(f'{dirpath}/entropy', 'wb') as f: + f.write(entropy) + + # Counter + with open(f'{dirpath}/counter', 'w') as f: + f.write(f'{str(counter)}\n') + + +def generate_ec_key_pair() -> Tuple[str, str]: + """ + Generate an ec key pair. + + Returns: + The private and public key PEM. + """ + # Key generation + privkey = ec.generate_private_key(ec.SECP256R1, default_backend()) + pubkey = privkey.public_key() + + # PEM serialization + privkey_pem = privkey.private_bytes(encoding=Encoding.PEM, + format=PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=NoEncryption()) + pubkey_pem = pubkey.public_bytes(encoding=Encoding.PEM, + format=PublicFormat.SubjectPublicKeyInfo) + return privkey_pem, pubkey_pem + + +def generate_certificate(privkey_pem: str, pubkey_pem: str) -> str: + """ + Generate a x509 certificate from a key pair. + + Args: + privkey_pem: The private key PEM. + pubkey_pem: The public key PEM. + + Returns: + The certificate PEM. + """ + # Convert key pair + privkey = crypto.load_privatekey(crypto.FILETYPE_PEM, privkey_pem) + pubkey = crypto.load_publickey(crypto.FILETYPE_PEM, pubkey_pem) + + # New x509v3 certificate + cert = crypto.X509() + cert.set_version(0x2) + + # Serial number + cert.set_serial_number(randint(1, 2 ** 64)) + + # Before / After + cert.gmtime_adj_notBefore(0) + cert.gmtime_adj_notAfter(4 * (365 * 24 * 60 * 60)) + + # Public key + cert.set_pubkey(pubkey) + + # Subject name and issueer + cert.get_subject().CN = "U2F emulated" + cert.set_issuer(cert.get_subject()) + + # Extensions + cert.add_extensions([ + crypto.X509Extension(b"subjectKeyIdentifier", + False, b"hash", subject=cert), + ]) + cert.add_extensions([ + crypto.X509Extension(b"authorityKeyIdentifier", + False, b"keyid:always", issuer=cert), + ]) + cert.add_extensions([ + crypto.X509Extension(b"basicConstraints", True, b"CA:TRUE") + ]) + + # Signature + cert.sign(privkey, 'sha256') + + return crypto.dump_certificate(crypto.FILETYPE_PEM, cert) + + +def generate_setup_dir(dirpath: str) -> None: + """ + Generates the setup directory. + + Args: + dirpath: The directory path. + """ + # Key pair + privkey_pem, pubkey_pem = generate_ec_key_pair() + + # Certificate + certificate_pem = generate_certificate(privkey_pem, pubkey_pem) + + # Entropy + entropy = os.urandom(48) + + # Counter + counter = 0 + + # Write + write_setup_dir(dirpath, privkey_pem, certificate_pem, entropy, counter) + + +def main() -> None: + """ + Main function + """ + # Dir path + if len(sys.argv) != 2: + sys.stderr.write(f'Usage: {sys.argv[0]} \n') + exit(2) + dirpath = sys.argv[1] + + # Dir non existence + if os.path.exists(dirpath): + sys.stderr.write(f'Directory: {dirpath} already exists.\n') + exit(1) + + generate_setup_dir(dirpath) + + +if __name__ == '__main__': + main()