Files
ckochandClaude Opus 5.5 6c7e6ab5f5 Skey: a FIDO2 security key on the Raspberry Pi Pico
Firmware for the Pico / Pico 2 that acts as a USB FIDO2 authenticator:
CTAP2 and U2F, passkeys, BOOTSEL as the touch button, and clientPIN with
PIN/UV auth protocols 1 and 2. A host simulator runs the same core so the
python-fido2 test suite exercises it without hardware.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 15:07:38 -04:00

148 lines
4.9 KiB
C

// USB descriptors and the HID plumbing for the FIDO interface.
#include <string.h>
#include "pico/unique_id.h"
#include "platform.h"
#include "tusb.h"
// pid.codes test VID/PID: fine for personal use, not for distribution. Hosts
// recognize FIDO devices by the HID usage page, not the IDs.
#define USB_VID 0x1209
#define USB_PID 0x0001
#define SEND_TIMEOUT_MS 1000
#define RX_QUEUE_LEN 16 // power of two
enum { STR_LANGID, STR_MANUFACTURER, STR_PRODUCT, STR_SERIAL, STR_INTERFACE };
static const tusb_desc_device_t desc_device = {
.bLength = sizeof(tusb_desc_device_t),
.bDescriptorType = TUSB_DESC_DEVICE,
.bcdUSB = 0x0200,
.bDeviceClass = 0x00,
.bDeviceSubClass = 0x00,
.bDeviceProtocol = 0x00,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.idVendor = USB_VID,
.idProduct = USB_PID,
.bcdDevice = 0x0100,
.iManufacturer = STR_MANUFACTURER,
.iProduct = STR_PRODUCT,
.iSerialNumber = STR_SERIAL,
.bNumConfigurations = 1,
};
// FIDO HID report descriptor (CTAP 2.1, section 11.2.8.1): 64-byte reports both ways.
static const uint8_t desc_hid_report[] = {
0x06, 0xd0, 0xf1, // Usage Page (FIDO Alliance)
0x09, 0x01, // Usage (CTAPHID)
0xa1, 0x01, // Collection (Application)
0x09, 0x20, // Usage (Input Report Data)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xff, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x40, // Report Count (64)
0x81, 0x02, // Input (Data, Variable, Absolute)
0x09, 0x21, // Usage (Output Report Data)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xff, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x40, // Report Count (64)
0x91, 0x02, // Output (Data, Variable, Absolute)
0xc0, // End Collection
};
#define EP_HID_OUT 0x01
#define EP_HID_IN 0x81
#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_INOUT_DESC_LEN)
static const uint8_t desc_configuration[] = {
TUD_CONFIG_DESCRIPTOR(1, 1, 0, CONFIG_TOTAL_LEN, 0, 100),
TUD_HID_INOUT_DESCRIPTOR(0, STR_INTERFACE, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EP_HID_OUT,
EP_HID_IN, CFG_TUD_HID_EP_BUFSIZE, 5),
};
static uint8_t rx_queue[RX_QUEUE_LEN][HID_PACKET_SIZE];
static unsigned rx_head, rx_tail;
const uint8_t *tud_descriptor_device_cb(void) {
return (const uint8_t *)&desc_device;
}
const uint8_t *tud_descriptor_configuration_cb(uint8_t index) {
(void)index;
return desc_configuration;
}
const uint8_t *tud_hid_descriptor_report_cb(uint8_t instance) {
(void)instance;
return desc_hid_report;
}
const uint16_t *tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
static uint16_t desc[32];
char serial[2 * PICO_UNIQUE_BOARD_ID_SIZE_BYTES + 1];
const char *str;
size_t len;
(void)langid;
switch (index) {
case STR_LANGID:
desc[1] = 0x0409; // English (US)
desc[0] = (uint16_t)(TUSB_DESC_STRING << 8 | 4);
return desc;
case STR_MANUFACTURER: str = "Skey"; break;
case STR_PRODUCT: str = "Skey FIDO2 Key"; break;
case STR_SERIAL:
pico_get_unique_board_id_string(serial, sizeof(serial));
str = serial;
break;
case STR_INTERFACE: str = "FIDO"; break;
default: return NULL;
}
len = strlen(str);
if (len > 31) len = 31;
for (size_t i = 0; i < len; i++) desc[1 + i] = (uint8_t)str[i];
desc[0] = (uint16_t)(TUSB_DESC_STRING << 8 | (2 * len + 2));
return desc;
}
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer,
uint16_t reqlen) {
(void)instance;
(void)report_id;
(void)report_type;
(void)buffer;
(void)reqlen;
return 0;
}
// Runs inside tud_task(). Packets are queued so they are handled from the main
// loop, never re-entrantly while a response is being sent.
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type,
const uint8_t *buffer, uint16_t bufsize) {
(void)instance;
(void)report_id;
(void)report_type;
if (bufsize != HID_PACKET_SIZE || rx_head - rx_tail == RX_QUEUE_LEN) return;
memcpy(rx_queue[rx_head % RX_QUEUE_LEN], buffer, HID_PACKET_SIZE);
rx_head++;
}
bool platform_hid_receive(uint8_t packet[HID_PACKET_SIZE]) {
tud_task();
if (rx_head == rx_tail) return false;
memcpy(packet, rx_queue[rx_tail % RX_QUEUE_LEN], HID_PACKET_SIZE);
rx_tail++;
return true;
}
void platform_hid_send(const uint8_t packet[HID_PACKET_SIZE]) {
uint32_t start = platform_millis();
while (!tud_hid_ready()) {
if (!tud_mounted() || platform_millis() - start > SEND_TIMEOUT_MS) return; // host went away
tud_task();
}
tud_hid_report(0, packet, HID_PACKET_SIZE);
}