All of lore.kernel.org
 help / color / mirror / Atom feed
From: Gerd Hoffmann <kraxel@redhat.com>
To: qemu-devel@nongnu.org
Cc: "Daniel P. Berrangé" <berrange@redhat.com>,
	"César Belley" <cesar.belley@lse.epita.fr>,
	"Gerd Hoffmann" <kraxel@redhat.com>,
	"Cleber Rosa" <crosa@redhat.com>,
	"Paolo Bonzini" <pbonzini@redhat.com>,
	"Eduardo Habkost" <ehabkost@redhat.com>
Subject: [PULL 09/18] hw/usb: Add U2F key passthru mode
Date: Wed, 26 Aug 2020 16:52:30 +0200	[thread overview]
Message-ID: <20200826145239.6077-10-kraxel@redhat.com> (raw)
In-Reply-To: <20200826145239.6077-1-kraxel@redhat.com>

From: César Belley <cesar.belley@lse.epita.fr>

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>
Message-id: 20200826114209.28821-6-cesar.belley@lse.epita.fr
Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
---
 hw/usb/u2f-passthru.c | 423 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 423 insertions(+)
 create mode 100644 hw/usb/u2f-passthru.c

diff --git a/hw/usb/u2f-passthru.c b/hw/usb/u2f-passthru.c
new file mode 100644
index 000000000000..106b5abf9ecc
--- /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)
-- 
2.27.0



  parent reply	other threads:[~2020-08-26 14:53 UTC|newest]

Thread overview: 22+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2020-08-26 14:52 [PULL 00/18] Usb 20200826 patches Gerd Hoffmann
2020-08-26 14:52 ` [PULL 01/18] hw: xhci: check return value of 'usb_packet_map' Gerd Hoffmann
2020-08-26 14:52 ` [PULL 02/18] hw: ehci: destroy sglist in error path Gerd Hoffmann
2020-08-26 14:52 ` [PULL 03/18] hw: ehci: check return value of 'usb_packet_map' Gerd Hoffmann
2020-08-26 14:52 ` [PULL 04/18] ehci: drop pointless warn_report for guest bugs Gerd Hoffmann
2020-08-26 14:52 ` [PULL 05/18] hw/usb: Regroup USB HID protocol values Gerd Hoffmann
2020-08-26 14:52 ` [PULL 06/18] docs: Add USB U2F key device documentation Gerd Hoffmann
2020-08-26 14:52 ` [PULL 07/18] hw/usb: Add U2F key base class Gerd Hoffmann
2020-08-26 14:52 ` [PULL 08/18] hw/usb: Add U2F key base class implementation Gerd Hoffmann
2020-08-26 14:52 ` Gerd Hoffmann [this message]
2020-08-26 14:52 ` [PULL 10/18] hw/usb: Add U2F key emulated mode Gerd Hoffmann
2020-08-26 14:52 ` [PULL 11/18] meson: Add U2F key to meson Gerd Hoffmann
2020-08-26 14:52 ` [PULL 12/18] docs/system: Add U2F key to the USB devices examples Gerd Hoffmann
2020-08-26 14:52 ` [PULL 13/18] docs/qdev-device-use.txt: Add USB U2F key to the QDEV " Gerd Hoffmann
2020-08-26 14:52 ` [PULL 14/18] scripts: Add u2f-setup-gen script Gerd Hoffmann
2020-08-26 14:52 ` [PULL 15/18] hw/usb: Add U2F device check to passthru mode Gerd Hoffmann
2020-08-26 14:52 ` [PULL 16/18] hw/usb: Add U2F device autoscan " Gerd Hoffmann
2020-08-26 14:52 ` [PULL 17/18] usb-host: workaround libusb bug Gerd Hoffmann
2020-08-26 14:52 ` [PULL 18/18] usb: fix setup_len init (CVE-2020-14364) Gerd Hoffmann
2020-08-26 16:05 ` [PULL 00/18] Usb 20200826 patches Peter Maydell
2020-08-28  8:08 [PULL 00/18] Usb 20200828 patches Gerd Hoffmann
2020-08-28  8:08 ` [PULL 09/18] hw/usb: Add U2F key passthru mode Gerd Hoffmann
2020-08-31  8:32 [PULL 00/18] Usb 20200831 patches Gerd Hoffmann
2020-08-31  8:32 ` [PULL 09/18] hw/usb: Add U2F key passthru mode Gerd Hoffmann

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20200826145239.6077-10-kraxel@redhat.com \
    --to=kraxel@redhat.com \
    --cc=berrange@redhat.com \
    --cc=cesar.belley@lse.epita.fr \
    --cc=crosa@redhat.com \
    --cc=ehabkost@redhat.com \
    --cc=pbonzini@redhat.com \
    --cc=qemu-devel@nongnu.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.