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>
122 lines
3.8 KiB
C
122 lines
3.8 KiB
C
// 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);
|
|
}
|