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:
commit
6c7e6ab5f5
41 files changed
+4218
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user