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>
This commit is contained in:
ckochandClaude Opus 5.5 committed 2026-10-14 15:07:38 -04:00
commit 6c7e6ab5f5
41 files changed
+4218

No files matched your search

+60
View File
@@ -0,0 +1,60 @@
// The BOOTSEL button doubles as the touch button.
#include "button.h"
#include "hardware/gpio.h"
#include "hardware/structs/ioqspi.h"
#include "hardware/structs/sio.h"
#include "hardware/sync.h"
#include "platform.h"
#define SAMPLE_INTERVAL_MS 10
#define DEBOUNCE_MS 30
#define QSPI_SS_INDEX 1
static bool pressed;
static uint32_t last_sample_ms;
static uint32_t last_change_ms;
static bool press_pending;
static uint32_t press_ms;
// BOOTSEL pulls the flash chip-select line low. Reading it means briefly
// releasing that line, so flash is unusable: run from RAM with interrupts off.
static bool __no_inline_not_in_flash_func(bootsel_pressed)(void) {
uint32_t irq = save_and_disable_interrupts();
hw_write_masked(&ioqspi_hw->io[QSPI_SS_INDEX].ctrl, GPIO_OVERRIDE_LOW << IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_LSB,
IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_BITS);
for (volatile int i = 0; i < 1000; i++) {
// let the line settle; sleep functions live in flash
}
#if PICO_RP2040
bool down = !(sio_hw->gpio_hi_in & (1u << QSPI_SS_INDEX));
#else
bool down = !(sio_hw->gpio_hi_in & SIO_GPIO_HI_IN_QSPI_CSN_BITS);
#endif
hw_write_masked(&ioqspi_hw->io[QSPI_SS_INDEX].ctrl, GPIO_OVERRIDE_NORMAL << IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_LSB,
IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_BITS);
restore_interrupts(irq);
return down;
}
void button_poll(void) {
uint32_t now = platform_millis();
if (now - last_sample_ms < SAMPLE_INTERVAL_MS) return;
last_sample_ms = now;
bool down = bootsel_pressed();
if (down == pressed || now - last_change_ms < DEBOUNCE_MS) return;
pressed = down;
last_change_ms = now;
if (pressed) {
press_pending = true;
press_ms = now;
}
}
bool platform_button_take_press(uint32_t max_age_ms) {
button_poll();
if (!press_pending) return false;
press_pending = false;
return platform_millis() - press_ms <= max_age_ms;
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
// Samples the touch button. Call from the main loop so presses made between
// requests are seen.
void button_poll(void);
+24
View File
@@ -0,0 +1,24 @@
#include "button.h"
#include "ctaphid.h"
#include "pin.h"
#include "platform.h"
#include "presence.h"
#include "storage.h"
#include "tusb.h"
int main(void) {
uint8_t packet[HID_PACKET_SIZE];
platform_init();
storage_init();
pin_init();
ctaphid_init();
tud_init(0);
for (;;) {
while (platform_hid_receive(packet)) ctaphid_handle_packet(packet);
ctaphid_tick();
button_poll();
presence_tick();
}
}
+26
View File
@@ -0,0 +1,26 @@
// mbedtls build configuration for the firmware: SHA-256, HMAC, P-256 ECDSA/ECDH
// and AES-256-CBC (PIN protocols) only.
#ifndef SKEY_MBEDTLS_CONFIG_H
#define SKEY_MBEDTLS_CONFIG_H
#define MBEDTLS_NO_PLATFORM_ENTROPY
#define MBEDTLS_MD_C
#define MBEDTLS_SHA256_C
#define MBEDTLS_BIGNUM_C
#define MBEDTLS_ECP_C
#define MBEDTLS_ECP_DP_SECP256R1_ENABLED
#define MBEDTLS_ECP_NIST_OPTIM
#define MBEDTLS_ECDSA_C
#define MBEDTLS_ECDSA_DETERMINISTIC
#define MBEDTLS_HMAC_DRBG_C
#define MBEDTLS_ASN1_PARSE_C
#define MBEDTLS_ASN1_WRITE_C
#define MBEDTLS_AES_C
#define MBEDTLS_CIPHER_MODE_CBC
#define MBEDTLS_AES_ROM_TABLES // keep the lookup tables in flash, not RAM
#define MBEDTLS_AES_FEWER_TABLES
#endif
+86
View File
@@ -0,0 +1,86 @@
#include <string.h>
#include "crypto.h"
#include "hardware/flash.h"
#include "hardware/sync.h"
#include "pico/rand.h"
#include "pico/stdlib.h"
#include "platform.h"
// The storage region is the last 64 KB of flash, far past the firmware image.
#define STORAGE_FLASH_OFFSET (PICO_FLASH_SIZE_BYTES - STORAGE_SIZE)
_Static_assert(STORAGE_SECTOR_SIZE == FLASH_SECTOR_SIZE, "sector size mismatch");
_Static_assert(STORAGE_PAGE_SIZE == FLASH_PAGE_SIZE, "page size mismatch");
extern char __flash_binary_end;
void platform_led(bool on) {
#ifdef PICO_DEFAULT_LED_PIN
gpio_put(PICO_DEFAULT_LED_PIN, on);
#else
(void)on;
#endif
}
void platform_init(void) {
if ((uintptr_t)&__flash_binary_end > XIP_BASE + STORAGE_FLASH_OFFSET)
platform_panic("firmware overlaps the storage region");
#ifdef PICO_DEFAULT_LED_PIN
gpio_init(PICO_DEFAULT_LED_PIN);
gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT);
#endif
}
uint32_t platform_millis(void) {
return to_ms_since_boot(get_absolute_time());
}
// pico_rand mixes fresh hardware entropy (ROSC jitter, timers, and the TRNG
// on RP2350) into every call. Each output block hashes many of those calls.
void platform_random(uint8_t *buf, size_t len) {
uint64_t samples[32];
uint8_t block[32];
while (len > 0) {
for (size_t i = 0; i < 32; i++) samples[i] = get_rand_64();
crypto_part_t part = {samples, sizeof(samples)};
crypto_sha256(&part, 1, block);
size_t n = len < sizeof(block) ? len : sizeof(block);
memcpy(buf, block, n);
buf += n;
len -= n;
}
crypto_wipe(samples, sizeof(samples));
crypto_wipe(block, sizeof(block));
}
void platform_panic(const char *msg) {
panic("%s", msg);
}
void platform_wink(void) {
for (int i = 0; i < 3; i++) {
platform_led(true);
sleep_ms(100);
platform_led(false);
sleep_ms(100);
}
}
void platform_flash_read(uint32_t offset, void *buf, size_t len) {
memcpy(buf, (const void *)(XIP_BASE + STORAGE_FLASH_OFFSET + offset), len);
}
// Flash is unavailable for execution while it is being written, so interrupts
// (whose handlers live in flash) stay off for the duration.
void platform_flash_erase(uint32_t offset) {
uint32_t irq = save_and_disable_interrupts();
flash_range_erase(STORAGE_FLASH_OFFSET + offset, FLASH_SECTOR_SIZE);
restore_interrupts(irq);
}
void platform_flash_program(uint32_t offset, const void *buf, size_t len) {
uint32_t irq = save_and_disable_interrupts();
flash_range_program(STORAGE_FLASH_OFFSET + offset, buf, len);
restore_interrupts(irq);
}
+20
View File
@@ -0,0 +1,20 @@
#ifndef TUSB_CONFIG_H
#define TUSB_CONFIG_H
#define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | OPT_MODE_FULL_SPEED)
#ifndef CFG_TUSB_OS
#define CFG_TUSB_OS OPT_OS_NONE
#endif
#define CFG_TUD_ENDPOINT0_SIZE 64
// One HID interface: the FIDO CTAPHID transport.
#define CFG_TUD_HID 1
#define CFG_TUD_HID_EP_BUFSIZE 64
#define CFG_TUD_CDC 0
#define CFG_TUD_MSC 0
#define CFG_TUD_MIDI 0
#define CFG_TUD_VENDOR 0
#endif
+147
View File
@@ -0,0 +1,147 @@
// 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);
}