From nobody Tue Feb 10 20:09:08 2026 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org ARC-Seal: i=1; a=rsa-sha256; t=1598270029; cv=none; d=zohomail.com; s=zohoarc; b=C48oKCyUawyQIaV0OhWv5s/PEwOkr76mTDwcOVGmWP1FFmG/0hyZNT1Pen8Vai4jgsFHebEhSFXNHkXEuxzMr8qbfYtX4x2FAKn1Ff73OmemWfIQMDbMRbCS8B4ohEpTJfItVqAoGy2cKSEgPa6/l36sCWqv0tWFveIBQOpLmPA= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1598270029; h=Content-Type:Content-Transfer-Encoding:Cc:Date:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:To; bh=5fUbmdETYC27I/icw7M3q4gwDaV8n6osDGxXaCcsr7k=; b=WIZVfEeyaxxsqBi4ztfwDFD2wxT6QQWNMUGYHZEWuuHe0zBAmLvI+7iBiOc8/1zJc2sGGPCCI3NuN2W+vh4ceBnF9lx84vzHB1w36JexDT7znnHqFWHpV4EEPNVCv3eGvvz9nj9jZcrELxjJDjXV2byZrj0rec9A13I/bxJsPqY= ARC-Authentication-Results: i=1; mx.zohomail.com; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org Return-Path: Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1598270029839724.4793548724979; Mon, 24 Aug 2020 04:53:49 -0700 (PDT) Received: from localhost ([::1]:52976 helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1kAB2q-0003Hh-8C for importer@patchew.org; Mon, 24 Aug 2020 07:53:48 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]:51430) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1kAAyU-0002Xp-Rv for qemu-devel@nongnu.org; Mon, 24 Aug 2020 07:49:18 -0400 Received: from gate-2.cri.epita.net ([163.5.55.20]:54812 helo=mail-2.srv.cri.epita.fr) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1kAAyR-0003ck-U1 for qemu-devel@nongnu.org; Mon, 24 Aug 2020 07:49:18 -0400 Received: from MattGorko-Laptop.home (lfbn-idf1-1-1395-83.w90-79.abo.wanadoo.fr [90.79.87.83]) (Authenticated sender: cesar.belley) by mail-2.srv.cri.epita.fr (Postfix) with ESMTPSA id AEF05410E0; Mon, 24 Aug 2020 13:49:14 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=lse.epita.fr; s=cri; t=1598269754; bh=btRqdaUXVRbDKjkqGiCstodc5y/Je6n9QgR65B0DETM=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=dnhWop14o+n0eigCLLtbIvdXW9SzxDfCzFvuBZzddxA/lNb+4qpZiKldscw9Z2Iih wMKNtbBuKEziUdDBlEjZDWG+6auB3EYY2YV/LgvTjBAYOdu5NNY1pOeUCRMw5h7qkG Ut3qozgZf6PMiwWwvv94C79X+eyyE7ivvhhH5FqQ= From: =?UTF-8?q?C=C3=A9sar=20Belley?= To: qemu-devel@nongnu.org Subject: [PATCH v2 05/13] hw/usb: Add U2F key passthru mode Date: Mon, 24 Aug 2020 13:48:19 +0200 Message-Id: <20200824114827.81623-6-cesar.belley@lse.epita.fr> X-Mailer: git-send-email 2.28.0 In-Reply-To: <20200824114827.81623-1-cesar.belley@lse.epita.fr> References: <20200824114827.81623-1-cesar.belley@lse.epita.fr> MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Received-SPF: pass client-ip=163.5.55.20; envelope-from=srs0=45nv=cc=lse.epita.fr=cesar.belley@cri.epita.fr; helo=mail-2.srv.cri.epita.fr X-detected-operating-system: by eggs.gnu.org: First seen = 2020/08/24 07:49:04 X-ACL-Warn: Detected OS = Linux 2.2.x-3.x [generic] X-Spam_score_int: -16 X-Spam_score: -1.7 X-Spam_bar: - X-Spam_report: (-1.7 / 5.0 requ) BAYES_00=-1.9, DKIM_INVALID=0.1, DKIM_SIGNED=0.1, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=no autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.23 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: =?UTF-8?q?C=C3=A9sar=20Belley?= , kraxel@redhat.com Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: "Qemu-devel" 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=3D/dev/hidrawX Autoscan and U2F compatibility checking features are given at the end of the patch series. Signed-off-by: C=C3=A9sar Belley --- 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 0000000000..106b5abf9e --- /dev/null +++ b/hw/usb/u2f-passthru.c @@ -0,0 +1,423 @@ +/* + * U2F USB Passthru device. + * + * Copyright (c) 2020 C=C3=A9sar Belley + * Written by C=C3=A9sar Belley + * + * Permission is hereby granted, free of charge, to any person obtaining a= copy + * of this software and associated documentation files (the "Software"), t= o deal + * in the Software without restriction, including without limitation the r= ights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or se= ll + * 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 OT= HER + * 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_SIZ= E) + +/* Cont packet sizes */ +#define PACKET_CONT_HEADER_SIZE 5 +#define PACKET_CONT_DATA_SIZE (U2FHID_PACKET_SIZE - PACKET_CONT_HEADER_SIZ= E) + +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 =3D 0; + bcnt |=3D packet_init->bcnth << 8; + bcnt |=3D 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 =3D 0; + key->current_transactions_start =3D 0; + key->current_transactions_end =3D 0; + key->current_transactions_num =3D 0; +} + +static void u2f_timeout_check(void *opaque) +{ + U2FPassthruState *key =3D opaque; + int64_t time =3D 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 =3D 0; i < key->current_transactions_num; ++i) { + int index =3D (key->current_transactions_start + i) + % CURRENT_TRANSACTIONS_NUM; + if (cid =3D=3D key->current_transactions[index].cid) { + return index; + } + } + return -1; +} + +static struct transaction *u2f_transaction_get(U2FPassthruState *key, + uint32_t cid) +{ + int index =3D 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 =3D 0; i < key->current_transactions_num; ++i) { + int index =3D (key->current_transactions_start + i) + % CURRENT_TRANSACTIONS_NUM; + if (key->current_transactions[index].cid =3D=3D BROADCAST_CID + && memcmp(nonce, key->current_transactions[index].nonce, + NONCE_SIZE) =3D=3D 0) { + return &key->current_transactions[index]; + } + } + return NULL; +} + +static void u2f_transaction_close(U2FPassthruState *key, uint32_t cid) +{ + int index, next_index; + index =3D u2f_transaction_get_index(key, cid); + if (index < 0) { + return; + } + next_index =3D (index + 1) % CURRENT_TRANSACTIONS_NUM; + + /* Rearrange to ensure the oldest is at the start position */ + while (next_index !=3D key->current_transactions_end) { + memcpy(&key->current_transactions[index], + &key->current_transactions[next_index], + sizeof(struct transaction)); + + index =3D next_index; + next_index =3D (index + 1) % CURRENT_TRANSACTIONS_NUM; + } + + key->current_transactions_end =3D index; + --key->current_transactions_num; + + if (key->current_transactions_num =3D=3D 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 >=3D CURRENT_TRANSACTIONS_NUM) { + /* Close the oldest transaction */ + index =3D key->current_transactions_start; + transaction =3D &key->current_transactions[index]; + u2f_transaction_close(key, transaction->cid); + } + + /* Index */ + index =3D key->current_transactions_end; + key->current_transactions_end =3D (index + 1) % CURRENT_TRANSACTIONS_N= UM; + ++key->current_transactions_num; + + /* Transaction */ + transaction =3D &key->current_transactions[index]; + transaction->cid =3D cid; + transaction->resp_bcnt =3D 0; + transaction->resp_size =3D 0; + + /* Nonce */ + if (nonce !=3D 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 =3D=3D BROADCAST_CID) { + u2f_transaction_add(key, packet_init->cid, packet_init->data); + } else { + u2f_transaction_add(key, packet_init->cid, NULL); + } + + /* Time */ + time =3D qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL); + if (key->last_transaction_time =3D=3D 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 =3D time; +} + +static void u2f_passthru_recv_from_host(U2FPassthruState *key, + const uint8_t packet[U2FHID_PACKET_SIZ= E]) +{ + struct transaction *transaction; + uint32_t cid; + + /* Retrieve transaction */ + cid =3D packet_get_cid(packet); + if (cid =3D=3D BROADCAST_CID) { + struct packet_init *packet_init; + if (!packet_is_init(packet)) { + return; + } + packet_init =3D (struct packet_init *)packet; + transaction =3D u2f_transaction_get_from_nonce(key, packet_init->d= ata); + } else { + transaction =3D u2f_transaction_get(key, cid); + } + + /* Ignore no started transaction */ + if (transaction =3D=3D NULL) { + return; + } + + if (packet_is_init(packet)) { + struct packet_init *packet_init =3D (struct packet_init *)packet; + transaction->resp_bcnt =3D packet_init_get_bcnt(packet_init); + transaction->resp_size =3D PACKET_INIT_DATA_SIZE; + + if (packet_init->cid =3D=3D BROADCAST_CID) { + /* Nonce checking for legitimate response */ + if (memcmp(transaction->nonce, packet_init->data, NONCE_SIZE) + !=3D 0) { + return; + } + } + } else { + transaction->resp_size +=3D PACKET_CONT_DATA_SIZE; + } + + /* Transaction end check */ + if (transaction->resp_size >=3D transaction->resp_bcnt) { + u2f_transaction_close(key, cid); + } + u2f_send_to_guest(&key->base, packet); +} + +static void u2f_passthru_read(void *opaque) +{ + U2FPassthruState *key =3D opaque; + U2FKeyState *base =3D &key->base; + uint8_t packet[2 * U2FHID_PACKET_SIZE]; + int ret; + + /* Full size base queue check */ + if (base->pending_in_num >=3D U2FHID_PENDING_IN_NUM) { + return; + } + + ret =3D 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 !=3D 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_SIZ= E]) +{ + U2FPassthruState *key =3D 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] =3D 0; + memcpy(host_packet + 1, packet, U2FHID_PACKET_SIZE); + + written =3D write(key->hidraw_fd, host_packet, sizeof(host_packet)); + if (written !=3D 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 =3D PASSTHRU_U2F_KEY(base); + + u2f_passthru_reset(key); + qemu_close(key->hidraw_fd); +} + +static void u2f_passthru_realize(U2FKeyState *base, Error **errp) +{ + U2FPassthruState *key =3D PASSTHRU_U2F_KEY(base); + int fd; + + if (key->hidraw =3D=3D NULL) { + error_setg(errp, "%s: Missing hidraw", TYPE_U2F_PASSTHRU); + return; + } + + fd =3D 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 =3D fd; + u2f_passthru_reset(key); +} + +static int u2f_passthru_post_load(void *opaque, int version_id) +{ + U2FPassthruState *key =3D opaque; + u2f_passthru_reset(key); + return 0; +} + +static const VMStateDescription u2f_passthru_vmstate =3D { + .name =3D "u2f-key-passthru", + .version_id =3D 1, + .minimum_version_id =3D 1, + .post_load =3D u2f_passthru_post_load, + .fields =3D (VMStateField[]) { + VMSTATE_U2F_KEY(base, U2FPassthruState), + VMSTATE_END_OF_LIST() + } +}; + +static Property u2f_passthru_properties[] =3D { + DEFINE_PROP_STRING("hidraw", U2FPassthruState, hidraw), + DEFINE_PROP_END_OF_LIST(), +}; + +static void u2f_passthru_class_init(ObjectClass *klass, void *data) +{ + DeviceClass *dc =3D DEVICE_CLASS(klass); + U2FKeyClass *kc =3D U2F_KEY_CLASS(klass); + + kc->realize =3D u2f_passthru_realize; + kc->unrealize =3D u2f_passthru_unrealize; + kc->recv_from_guest =3D u2f_passthru_recv_from_guest; + dc->desc =3D "QEMU U2F passthrough key"; + dc->vmsd =3D &u2f_passthru_vmstate; + device_class_set_props(dc, u2f_passthru_properties); +} + +static const TypeInfo u2f_key_passthru_info =3D { + .name =3D TYPE_U2F_PASSTHRU, + .parent =3D TYPE_U2F_KEY, + .instance_size =3D sizeof(U2FPassthruState), + .class_init =3D 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) --=20 2.28.0