diff mbox series

[v3,05/12] hw/usb: Add U2F key passthru mode

Message ID 20200826114209.28821-6-cesar.belley@lse.epita.fr
State New
Headers show
Series Introduce USB U2F key device | expand

Commit Message

César Belley Aug. 26, 2020, 11:42 a.m. UTC
This patch adds the U2F key pass-through mode.

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 pass-through mode is associated with a device inheriting from
u2f-key base.

To work, it needs the path to a U2F hidraw, obtained from the Qemu
command line, and passed by the user:

    qemu -usb -device u2f-passthru,hidraw=/dev/hidrawX

Autoscan and U2F compatibility checking features are given at the end
of the patch series.

Signed-off-by: César Belley <cesar.belley@lse.epita.fr>
---
 hw/usb/u2f-passthru.c | 423 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 423 insertions(+)
 create mode 100644 hw/usb/u2f-passthru.c
diff mbox series

Patch

diff --git a/hw/usb/u2f-passthru.c b/hw/usb/u2f-passthru.c
new file mode 100644
index 0000000000..106b5abf9e
--- /dev/null
+++ b/hw/usb/u2f-passthru.c
@@ -0,0 +1,423 @@ 
+/*
+ * U2F USB Passthru device.
+ *
+ * Copyright (c) 2020 César Belley <cesar.belley@lse.epita.fr>
+ * Written by César Belley <cesar.belley@lse.epita.fr>
+ *
+ * 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"
+
+#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%lx, val 0x%lx)",
+                     TYPE_U2F_PASSTHRU, sizeof(host_packet), written);
+    }
+}
+
+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) {
+        error_setg(errp, "%s: Missing hidraw", TYPE_U2F_PASSTHRU);
+        return;
+    }
+
+    fd = qemu_open(key->hidraw, O_RDWR);
+    if (fd < 0) {
+        error_setg(errp, "%s: Failed to open %s", TYPE_U2F_PASSTHRU,
+                   key->hidraw);
+        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)