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

+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "platform.h"
// Reads one packet from stdin, waiting up to timeout_ms. Exits the process
// when stdin is closed.
bool host_receive(uint8_t packet[HID_PACKET_SIZE], int timeout_ms);
+23
View File
@@ -0,0 +1,23 @@
// Simulator: speaks CTAPHID over stdin/stdout, one 64-byte packet at a time.
#include "ctaphid.h"
#include "host.h"
#include "pin.h"
#include "platform.h"
#include "presence.h"
#include "storage.h"
#define TICK_MS 50
int main(void) {
uint8_t packet[HID_PACKET_SIZE];
platform_init();
storage_init();
pin_init();
ctaphid_init();
for (;;) {
if (host_receive(packet, TICK_MS)) ctaphid_handle_packet(packet);
ctaphid_tick();
presence_tick();
}
}
+121
View File
@@ -0,0 +1,121 @@
// Simulator backend: flash is emulated in a file (SKEY_FLASH_FILE), randomness
// comes from the OS, and HID packets travel over stdin/stdout.
//
// The button: without SKEY_BUTTON_FILE every request is approved. With it,
// creating that file is a press (its mtime is the press time), and the
// simulator deletes it when the press is used.
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/random.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include "host.h"
#include "platform.h"
static uint8_t flash[STORAGE_SIZE];
static const char *flash_path;
static const char *button_path;
static struct timespec start;
void platform_init(void) {
flash_path = getenv("SKEY_FLASH_FILE");
if (!flash_path) flash_path = "skey_flash.bin";
button_path = getenv("SKEY_BUTTON_FILE");
memset(flash, 0xff, sizeof(flash));
FILE *f = fopen(flash_path, "rb");
if (f) {
if (fread(flash, 1, sizeof(flash), f) != sizeof(flash)) memset(flash, 0xff, sizeof(flash));
fclose(f);
}
clock_gettime(CLOCK_MONOTONIC, &start);
}
uint32_t platform_millis(void) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
return (uint32_t)((now.tv_sec - start.tv_sec) * 1000 + (now.tv_nsec - start.tv_nsec) / 1000000);
}
void platform_random(uint8_t *buf, size_t len) {
while (len > 0) {
ssize_t n = getrandom(buf, len, 0);
if (n < 0) platform_panic("getrandom failed");
buf += n;
len -= (size_t)n;
}
}
void platform_panic(const char *msg) {
fprintf(stderr, "skey_sim: %s\n", msg);
abort();
}
bool platform_button_take_press(uint32_t max_age_ms) {
struct stat st;
struct timespec now;
if (!button_path) return true;
if (stat(button_path, &st) != 0) return false;
unlink(button_path);
clock_gettime(CLOCK_REALTIME, &now);
int64_t age_ms = (int64_t)(now.tv_sec - st.st_mtim.tv_sec) * 1000 + (now.tv_nsec - st.st_mtim.tv_nsec) / 1000000;
return age_ms <= (int64_t)max_age_ms;
}
void platform_led(bool on) {
(void)on;
}
void platform_wink(void) {
fprintf(stderr, "skey_sim: wink\n");
}
static void save_flash(void) {
FILE *f = fopen(flash_path, "wb");
if (!f || fwrite(flash, 1, sizeof(flash), f) != sizeof(flash)) platform_panic("cannot write flash file");
fclose(f);
}
void platform_flash_read(uint32_t offset, void *buf, size_t len) {
if (offset > STORAGE_SIZE || len > STORAGE_SIZE - offset) platform_panic("flash read out of range");
memcpy(buf, flash + offset, len);
}
void platform_flash_erase(uint32_t offset) {
if (offset % STORAGE_SECTOR_SIZE || offset >= STORAGE_SIZE) platform_panic("bad flash erase");
memset(flash + offset, 0xff, STORAGE_SECTOR_SIZE);
save_flash();
}
// Behaves like NOR flash: whole pages only, and programming only clears bits.
void platform_flash_program(uint32_t offset, const void *buf, size_t len) {
const uint8_t *src = buf;
if (offset % STORAGE_PAGE_SIZE || len % STORAGE_PAGE_SIZE || offset > STORAGE_SIZE ||
len > STORAGE_SIZE - offset)
platform_panic("bad flash program");
for (size_t i = 0; i < len; i++) flash[offset + i] &= src[i];
save_flash();
}
void platform_hid_send(const uint8_t packet[HID_PACKET_SIZE]) {
fwrite(packet, 1, HID_PACKET_SIZE, stdout);
fflush(stdout);
}
bool host_receive(uint8_t packet[HID_PACKET_SIZE], int timeout_ms) {
struct pollfd pfd = {.fd = STDIN_FILENO, .events = POLLIN};
if (poll(&pfd, 1, timeout_ms) <= 0) return false;
for (size_t got = 0; got < HID_PACKET_SIZE;) {
ssize_t n = read(STDIN_FILENO, packet + got, HID_PACKET_SIZE - got);
if (n <= 0) exit(0); // the host closed the pipe: unplugged
got += (size_t)n;
}
return true;
}
bool platform_hid_receive(uint8_t packet[HID_PACKET_SIZE]) {
return host_receive(packet, 0);
}