1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2024-12-29 04:50:03 +01:00
RIOT/sys/fido2/ctap/transport/hid/ctap_hid.c

724 lines
20 KiB
C
Raw Normal View History

2021-02-19 00:17:40 +01:00
/*
* Copyright (C) 2021 Freie Universität Berlin
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup fido2_ctap_transport_hid
* @{
* @file
*
* @author Nils Ollrogge <nils.ollrogge@fu-berlin.de>
* @}
*/
#include <string.h>
#include "xtimer.h"
#include "usb/usbus.h"
#include "usb/usbus/hid_io.h"
#include "fido2/ctap.h"
#include "fido2/ctap/transport/hid/ctap_hid.h"
#include "fido2/ctap/ctap_utils.h"
#define ENABLE_DEBUG (0)
#include "debug.h"
/**
* @brief CTAP HID report descriptor
*
* CTAP specification (version 20190130) section 8.1.8.2
*/
const uint8_t _hid_report_desc[] = {
0x06, 0xD0, 0xF1, /**< HID_UsagePage ( FIDO_USAGE_PAGE ) */
0x09, 0x01, /**< HID_Usage ( FIDO_USAGE_CTAPHID ) */
0xA1, 0x01, /**< HID_Collection ( HID_Application ) */
0x09, 0x20, /**< HID_Usage ( FIDO_USAGE_DATA_IN ) */
0x15, 0x00, /**< HID_LogicalMin ( 0 ) */
0x26, 0xFF, 0x00, /**< HID_LogicalMaxS ( 0xff ) */
0x75, 0x08, /**< HID_ReportSize ( 8 ) */
0x95, 0x40, /**< HID_ReportCount ( HID_INPUT_REPORT_BYTES ) */
0x81, 0x02, /**< HID_Input ( HID_Data | HID_Absolute | HID_Variable ) */
0x09, 0x21, /**< HID_Usage ( FIDO_USAGE_DATA_OUT ) */
0x15, 0x00, /**< HID_LogicalMin ( 0 ) */
0x26, 0xFF, 0x00, /**< HID_LogicalMaxS ( 0xff ) */
0x75, 0x08, /**< HID_ReportSize ( 8 ) */
0x95, 0x40, /**< HID_ReportCount ( HID_OUTPUT_REPORT_BYTES ) */
0x91, 0x02, /**< HID_Output ( HID_Data | HID_Absolute | HID_Variable ) */
0xC0, /**< HID_EndCollection */
};
/**
* @brief CTAP_HID buffer struct
*
*/
typedef struct {
uint32_t cid; /**< channel identifier */
uint8_t cmd; /**< CTAP_HID command */
uint8_t buffer[CTAP_HID_BUFFER_SIZE]; /**< data buffer */
uint16_t offset; /**< current offset into data buffer */
int16_t seq; /**< current sequence number */
uint16_t bcnt; /**< expected amount of bytes to be received */
uint8_t err; /**< error type if error */
bool is_locked; /**< buffer is locked by transaction */
bool should_cancel; /**< flag if current transaction should be cancelled */
} ctap_hid_state_t;
/**
* @brief Serialize data and transmit it via USB HID layer
*/
static void _ctap_hid_write(uint8_t cmd, uint32_t cid, const void *_data, size_t size);
/**
* @brief CTAPHID_CBOR command
*
* CTAP specification (version 20190130) section 8.1.9.1.2
*/
static void _handle_cbor_packet(uint8_t cmd, uint32_t cid, uint8_t *buf, uint16_t bcnt);
/**
* @brief CTAPHID_INIT command
*
* CTAP specification (version 20190130) section 8.1.9.1.3
*/
static uint32_t _handle_init_packet(uint32_t cid, uint16_t bcnt,
const uint8_t *nonce);
/**
* @brief CTAPHID_WINK command
*
* CTAP specification (version 20190130) section 8.1.9.2.1
*/
static void _wink(uint32_t cid, uint8_t cmd);
/**
* @brief Encode response to CTAPHID_INIT command
*/
static void _send_init_response(uint32_t cid_old, uint32_t cid_new,
const uint8_t *nonce);
/**
* @brief Clear the CTAP packet buffer
*/
static void _clear_ctap_buffer(void);
/**
* @brief Buffer packet belonging to currently processed transaction
*/
static uint8_t _buffer_pkt(const ctap_hid_pkt_t *pkt);
/**
* @brief Send error code to cid
*/
static void _send_error_response(uint32_t cid, uint8_t err);
/**
* @brief Refresh the last_used timestamp for this cid
*/
static int8_t _refresh_cid(uint32_t cid);
/**
* @brief Allocate a new logical channel
*/
static int8_t _add_cid(uint32_t cid);
/**
* @brief Delete logical channel
*/
static int8_t _delete_cid(uint32_t cid);
/**
* @brief Check if a logical channel with cid exists
*/
static bool _cid_exists(uint32_t cid);
/**
* @brief Parse packet length from pkt
*/
static inline uint16_t _get_packet_len(const ctap_hid_pkt_t *pkt);
/**
* @brief Process CTAPHID transaction
*/
static void _process_transaction(event_t *arg);
/**
* @brief Check if packet is an initialization packet
*/
static inline bool _is_init_type_pkt(const ctap_hid_pkt_t *pkt);
/* usbus functionality */
/**
* @brief USB stack
*/
static char _usb_stack[USBUS_STACKSIZE];
/**
* @brief USBUS context
*/
static usbus_t _usbus;
/**
* @brief Indicate if authenticator is busy processing a transactions
*
* Transactions are atomic, therefore only 1 transaction can be processed at
* once
*/
static bool _is_busy = false;
/**
* @brief State for handling transactions
*/
static ctap_hid_state_t _state;
/**
* @brief Logical CTAPHID channels
*/
static ctap_hid_cid_t g_cids[CTAP_HID_CIDS_MAX];
/**
* @brief Incremental channel ids
*
* channel id 0 is reserved
*/
static uint32_t _cid = 1;
/**
* @brief CTAP transport layer event queue
*/
static event_queue_t *_queue;
/**
* @brief CTAPHID event
*/
static event_t _ctap_hid_event = { .handler = _process_transaction };
/**
* @brief USBUS context
*/
static usbus_t _usbus;
static void _usb_cb(void *arg)
{
(void)arg;
uint8_t buffer[CONFIG_USBUS_HID_INTERRUPT_EP_SIZE];
int read;
read = usb_hid_io_read(buffer, CONFIG_USBUS_HID_INTERRUPT_EP_SIZE);
if (read == CONFIG_USBUS_HID_INTERRUPT_EP_SIZE) {
fido2_ctap_transport_hid_handle_packet(buffer);
}
}
void fido2_ctap_transport_hid_init(event_queue_t *queue)
{
_queue = queue;
usbdev_t *usbdev = usbdev_get_ctx(0);
assert(usbdev);
usbus_init(&_usbus, usbdev);
usb_hid_io_init(&_usbus, _hid_report_desc, sizeof(_hid_report_desc));
usb_hid_io_set_rx_cb(_usb_cb, NULL);
usbus_create(_usb_stack, sizeof(_usb_stack), USBUS_PRIO, USBUS_TNAME, &_usbus);
}
void fido2_ctap_transport_hid_handle_packet(void *pkt_raw)
{
ctap_hid_pkt_t *pkt = (ctap_hid_pkt_t *)pkt_raw;
uint32_t cid = pkt->cid;
uint8_t status = CTAP_HID_BUFFER_STATUS_BUFFERING;
if (cid == 0x00) {
/* cid = 0x00 always invalid */
_send_error_response(cid, CTAP_HID_ERR_INVALID_CHANNEL);
return;
}
else if (_is_busy) {
if (_state.cid == cid) {
/* CTAP specification (version 20190130) section 8.1.5.3 */
if (_is_init_type_pkt(pkt)) {
if (pkt->init.cmd == CTAP_HID_COMMAND_INIT) {
/* abort */
_clear_ctap_buffer();
status = _buffer_pkt(pkt);
}
else if (_state.is_locked && pkt->init.cmd ==
CTAP_HID_COMMAND_CANCEL) {
_state.should_cancel = true;
}
/* random init type pkt. invalid sequence of pkts */
else {
_send_error_response(cid, CTAP_HID_ERR_INVALID_SEQ);
return;
}
}
/* packet for this cid is currently being worked */
else if (_state.is_locked) {
_send_error_response(cid, CTAP_HID_ERR_CHANNEL_BUSY);
return;
}
else {
/* buffer cont packets */
status = _buffer_pkt(pkt);
}
}
/* transactions are atomic. Deny all other cids if busy with one cid */
else {
_send_error_response(cid, CTAP_HID_ERR_CHANNEL_BUSY);
return;
}
}
else {
/* first init packet received starts a transaction */
if (_is_init_type_pkt(pkt)) {
_is_busy = true;
status = _buffer_pkt(pkt);
}
/* ignore rest */
}
if (status == CTAP_HID_BUFFER_STATUS_ERROR) {
_send_error_response(cid, _state.err);
_delete_cid(cid);
_clear_ctap_buffer();
_is_busy = false;
return;
}
/* pkt->init.bcnt bytes have been received. Transaction can now be processed */
if (status == CTAP_HID_BUFFER_STATUS_DONE) {
_state.is_locked = 1;
event_post(_queue, &_ctap_hid_event);
_is_busy = false;
}
else {
/* refresh timestamp of cid that is being buffered */
_refresh_cid(_state.cid);
}
}
static uint8_t _buffer_pkt(const ctap_hid_pkt_t *pkt)
{
if (_is_init_type_pkt(pkt)) {
/**
* broadcast cid only allowed for CTAP_HID_COMMAND_INIT
*/
if (pkt->cid == CTAP_HID_BROADCAST_CID &&
pkt->init.cmd != CTAP_HID_COMMAND_INIT) {
_send_error_response(pkt->cid, CTAP_HID_ERR_INVALID_CHANNEL);
}
/**
* received CTAP_HID_COMMAND_CANCEL while buffering packet.
* Cancel request.
*/
if (pkt->init.cmd == CTAP_HID_COMMAND_CANCEL && !_state.is_locked &&
pkt->cid == _state.cid) {
_state.err = CTAP2_ERR_KEEPALIVE_CANCEL;
return CTAP_HID_BUFFER_STATUS_ERROR;
}
_state.bcnt = _get_packet_len(pkt);
/* check for init transaction size described in CTAP specification
(version 20190130) section 8.1.9.1.3 */
if (pkt->init.cmd == CTAP_HID_COMMAND_INIT && _state.bcnt != 8) {
_state.err = CTAP_HID_ERR_INVALID_LEN;
return CTAP_HID_BUFFER_STATUS_ERROR;
}
/* don't allow transactions bigger than max buffer size */
if (_state.bcnt > CTAP_HID_BUFFER_SIZE) {
_state.err = CTAP_HID_ERR_INVALID_LEN;
return CTAP_HID_BUFFER_STATUS_ERROR;
}
uint16_t size = (_state.bcnt < CTAP_HID_INIT_PAYLOAD_SIZE) ?
_state.bcnt : CTAP_HID_INIT_PAYLOAD_SIZE;
_state.cmd = pkt->init.cmd;
_state.cid = pkt->cid;
_state.seq = -1;
memcpy(_state.buffer, pkt->init.payload, size);
_state.offset = size;
}
else {
int left = _state.bcnt - _state.offset;
int diff = left - CTAP_HID_CONT_PAYLOAD_SIZE;
_state.seq++;
/* seqs have to increase sequentially */
if (pkt->cont.seq != _state.seq) {
_state.err = CTAP_HID_ERR_INVALID_SEQ;
return CTAP_HID_BUFFER_STATUS_ERROR;
}
/* check for potential buffer overflow */
if (_state.offset + CTAP_HID_CONT_PAYLOAD_SIZE > CTAP_HID_BUFFER_SIZE) {
_state.err = CTAP_HID_ERR_INVALID_LEN;
return CTAP_HID_BUFFER_STATUS_ERROR;
}
if (diff <= 0) {
memcpy(_state.buffer + _state.offset, pkt->cont.payload, left);
_state.offset += left;
}
else {
memcpy(_state.buffer + _state.offset, pkt->cont.payload,
CTAP_HID_CONT_PAYLOAD_SIZE);
_state.offset += CTAP_HID_CONT_PAYLOAD_SIZE;
}
}
return _state.offset == _state.bcnt ?
CTAP_HID_BUFFER_STATUS_DONE : CTAP_HID_BUFFER_STATUS_BUFFERING;
}
static void _process_transaction(event_t *arg)
{
(void)arg;
uint8_t *buf = (uint8_t *)&_state.buffer;
uint32_t cid = _state.cid;
uint16_t bcnt = _state.bcnt;
uint8_t cmd = _state.cmd;
if (cmd == CTAP_HID_COMMAND_INIT) {
_handle_init_packet(cid, bcnt, buf);
}
else {
/* readding deleted cid */
if (!_cid_exists(cid) && _add_cid(cid) == -1) {
_send_error_response(cid, CTAP_HID_ERR_CHANNEL_BUSY);
}
else {
switch (cmd) {
case CTAP_HID_COMMAND_MSG:
/* not implemented as of now */
DEBUG("CTAP_HID: MSG COMMAND \n");
_send_error_response(cid, CTAP_HID_ERR_INVALID_CMD);
break;
case CTAP_HID_COMMAND_CBOR:
DEBUG("CTAP_HID: CBOR COMMAND \n");
_handle_cbor_packet(cmd, cid, buf, bcnt);
break;
case CTAP_HID_COMMAND_WINK:
DEBUG("CTAP_HID: wink \n");
_wink(cid, cmd);
break;
case CTAP_HID_COMMAND_PING:
DEBUG("CTAP_HID: PING \n");
_ctap_hid_write(cmd, cid, buf, bcnt);
break;
case CTAP_HID_COMMAND_CANCEL:
/*
* no transaction is currently being processed,
* no reason to send cancel
*/
break;
default:
_send_error_response(cid, CTAP_HID_ERR_INVALID_CMD);
DEBUG("Ctaphid: unknown command %u \n", cmd);
}
}
}
/* transaction done, cleanup */
_clear_ctap_buffer();
}
static uint32_t _handle_init_packet(uint32_t cid, uint16_t bcnt,
const uint8_t *nonce)
{
uint32_t cid_new = 0;
/* cid 0 is reserved */
if (cid == 0) {
_send_error_response(cid, CTAP_HID_ERR_INVALID_CHANNEL);
return 0;
}
/* check for len described in standard */
if (bcnt != 8) {
_send_error_response(cid, CTAP_HID_ERR_INVALID_LEN);
return 0;
}
/* create new channel */
if (cid == CTAP_HID_BROADCAST_CID) {
cid_new = _cid++;
if (_add_cid(cid_new) == -1) {
_send_error_response(cid, CTAP_HID_ERR_CHANNEL_BUSY);
return 0;
}
_send_init_response(cid, cid_new, nonce);
}
/* synchronize channel */
else {
cid_new = cid;
if (!_cid_exists(cid)) {
if (_add_cid(cid) == -1) {
/* reached cid limit */
_send_error_response(cid, CTAP_HID_ERR_CHANNEL_BUSY);
return 0;
}
}
_send_init_response(cid, cid, nonce);
}
return cid_new;
}
static void _handle_cbor_packet(uint8_t cmd, uint32_t cid, uint8_t *buf, uint16_t bcnt)
{
ctap_resp_t resp;
uint8_t err;
size_t size;
if (bcnt == 0) {
err = CTAP_HID_ERR_INVALID_LEN;
cmd = CTAP_HID_COMMAND_ERROR;
_ctap_hid_write(cmd, cid, &err, sizeof(err));
return;
}
memset(&resp, 0, sizeof(ctap_resp_t));
ctap_req_t req;
req.method = *buf;
req.buf = buf + 1;
req.len = bcnt - 1;
size = fido2_ctap_handle_request(&req, &resp);
/* transaction done, clear should_cancel flag */
_state.should_cancel = false;
if (resp.status == CTAP2_OK && size > 0) {
/* status + data */
_ctap_hid_write(cmd, cid, &resp, size + sizeof(resp.status));
}
else {
/* status only */
_ctap_hid_write(cmd, cid, &resp.status, sizeof(resp.status));
}
}
static inline bool _is_init_type_pkt(const ctap_hid_pkt_t *pkt)
{
return ((pkt->init.cmd & CTAP_HID_INIT_PACKET) == CTAP_HID_INIT_PACKET);
}
static void _clear_ctap_buffer(void)
{
memset(&_state, 0, sizeof(_state));
}
bool fido2_ctap_transport_hid_should_cancel(void)
{
return _state.should_cancel;
}
void fido2_ctap_transport_hid_check_timeouts(void)
{
uint64_t now = xtimer_now_usec64();
for (uint8_t i = 0; i < CTAP_HID_CIDS_MAX; i++) {
/* transaction timed out because cont packets didn't arrive in time */
if (_is_busy && g_cids[i].taken &&
(now - g_cids[i].last_used) >= CTAP_HID_TRANSACTION_TIMEOUT &&
_state.cid == g_cids[i].cid && !_state.is_locked) {
_send_error_response(g_cids[i].cid, CTAP_HID_ERR_MSG_TIMEOUT);
_delete_cid(g_cids[i].cid);
_clear_ctap_buffer();
_is_busy = false;
}
}
}
static int8_t _add_cid(uint32_t cid)
{
uint64_t oldest = xtimer_now_usec64();
int8_t index_oldest = -1;
for (int i = 0; i < CTAP_HID_CIDS_MAX; i++) {
if (!g_cids[i].taken) {
g_cids[i].taken = true;
g_cids[i].cid = cid;
g_cids[i].last_used = xtimer_now_usec64();
return CTAP_HID_OK;
}
if (g_cids[i].last_used < oldest) {
oldest = g_cids[i].last_used;
index_oldest = i;
}
}
/* remove oldest cid to make place for a new one */
if (index_oldest > -1) {
g_cids[index_oldest].taken = true;
g_cids[index_oldest].cid = cid;
g_cids[index_oldest].last_used = xtimer_now_usec64();
return CTAP_HID_OK;
}
return CTAP_HID_ERR_OTHER;
}
static int8_t _refresh_cid(uint32_t cid)
{
for (int i = 0; i < CTAP_HID_CIDS_MAX; i++) {
if (g_cids[i].cid == cid) {
g_cids[i].last_used = xtimer_now_usec64();
return CTAP_HID_OK;
}
}
return CTAP_HID_ERR_OTHER;
}
static int8_t _delete_cid(uint32_t cid)
{
for (int i = 0; i < CTAP_HID_CIDS_MAX; i++) {
if (g_cids[i].cid == cid) {
g_cids[i].taken = false;
g_cids[i].cid = 0;
return CTAP_HID_OK;
}
}
return CTAP_HID_ERR_OTHER;
}
static bool _cid_exists(uint32_t cid)
{
for (int i = 0; i < CTAP_HID_CIDS_MAX; i++) {
if (g_cids[i].cid == cid) {
return true;
}
}
return false;
}
static inline uint16_t _get_packet_len(const ctap_hid_pkt_t *pkt)
{
return (uint16_t)((pkt->init.bcnth << 8) | pkt->init.bcntl);
}
static void _wink(uint32_t cid, uint8_t cmd)
{
#if !IS_ACTIVE(CONFIG_FIDO2_CTAP_DISABLE_LED)
uint32_t delay = CTAP_HID_WINK_DELAY;
for (int i = 1; i <= 8; i++) {
#ifdef LED0_TOGGLE
LED0_TOGGLE;
xtimer_msleep(delay);
#endif
#ifdef LED1_TOGGLE
LED1_TOGGLE;
xtimer_msleep(delay);
#endif
#ifdef LED2_TOGGLE
LED2_TOGGLE;
xtimer_msleep(delay);
#endif
#ifdef LED3_TOGGLE
LED3_TOGGLE;
xtimer_msleep(delay);
#endif
delay /= 2;
}
#endif /* CONFIG_FIDO2_CTAP_DISABLE_LED */
_ctap_hid_write(cmd, cid, NULL, 0);
}
static void _send_error_response(uint32_t cid, uint8_t err)
{
DEBUG("ctap_trans_hid err resp: %02x \n", err);
_ctap_hid_write(CTAP_HID_COMMAND_ERROR, cid, &err, sizeof(err));
}
static void _send_init_response(uint32_t cid_old, uint32_t cid_new,
const uint8_t *nonce)
{
ctap_hid_init_resp_t resp;
memset(&resp, 0, sizeof(ctap_hid_init_resp_t));
resp.cid = cid_new;
resp.protocol_version = CTAP_HID_PROTOCOL_VERSION;
resp.version_major = 0;
resp.version_minor = 0;
resp.build_version = 0;
memcpy(resp.nonce, nonce, sizeof(resp.nonce));
uint8_t cmd = (CTAP_HID_INIT_PACKET | CTAP_HID_COMMAND_INIT);
resp.capabilities = CTAP_HID_CAPABILITY_CBOR | CTAP_HID_CAPABILITY_WINK
| CTAP_HID_CAPABILITY_NMSG;
_ctap_hid_write(cmd, cid_old, &resp, sizeof(ctap_hid_init_resp_t));
}
void _ctap_hid_write(uint8_t cmd, uint32_t cid, const void *_data, size_t len)
{
const uint8_t *data = (uint8_t *)_data;
uint8_t buf[CONFIG_USBUS_HID_INTERRUPT_EP_SIZE] = { 0 };
uint16_t bytes_written = 0;
uint8_t seq = 0;
uint8_t offset = 0;
memcpy(buf, &cid, sizeof(cid));
offset += sizeof(cid);
buf[offset++] = cmd;
/* high part of payload length first */
buf[offset++] = (len & 0xff00) >> 8;
buf[offset++] = (len & 0xff) >> 0;
if (data == NULL) {
usb_hid_io_write(buf, CONFIG_USBUS_HID_INTERRUPT_EP_SIZE);
return;
}
for (size_t i = 0; i < len; i++) {
if (offset == 0) {
memcpy(buf, &cid, sizeof(cid));
offset += sizeof(cid);
/* initialization packet */
if (bytes_written == 0) {
buf[offset++] = cmd;
buf[offset++] = (len & 0xff00) >> 8;
buf[offset++] = (len & 0xff) >> 0;
}
/* continuation packet */
else {
buf[offset++] = seq++;
}
}
buf[offset++] = data[i];
bytes_written++;
if (offset == CONFIG_USBUS_HID_INTERRUPT_EP_SIZE) {
usb_hid_io_write(buf, CONFIG_USBUS_HID_INTERRUPT_EP_SIZE);
offset = 0;
}
}
if (offset > 0) {
memset(buf + offset, 0, CONFIG_USBUS_HID_INTERRUPT_EP_SIZE - offset);
usb_hid_io_write(buf, CONFIG_USBUS_HID_INTERRUPT_EP_SIZE);
}
}