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

107 lines
3.1 KiB
Python

"""Runs the authenticator simulator and exposes it as a python-fido2 HID device."""
import os
import pathlib
import subprocess
import threading
import pytest
from fido2.hid import CtapHidDevice
from fido2.hid.base import CtapHidConnection, HidDescriptor
ROOT = pathlib.Path(__file__).resolve().parent.parent
SIMULATOR = pathlib.Path(os.environ.get("SKEY_SIM", ROOT / "build-host" / "skey_sim"))
PACKET_SIZE = 64
class PipeConnection(CtapHidConnection):
"""Carries HID reports over the simulator's stdin/stdout."""
def __init__(self, proc: subprocess.Popen):
self.proc = proc
def write_packet(self, data: bytes) -> None:
assert len(data) == PACKET_SIZE
self.proc.stdin.write(data)
self.proc.stdin.flush()
def read_packet(self) -> bytes:
data = self.proc.stdout.read(PACKET_SIZE)
if len(data) != PACKET_SIZE:
raise OSError(f"simulator exited with {self.proc.wait()}")
return data
def close(self) -> None:
self.proc.stdin.close()
assert self.proc.wait(timeout=10) == 0, "simulator crashed"
class Simulator:
"""One simulated key. Restarting keeps its flash, like unplugging a real one.
Without a button file every request is approved; with one, press() is a
touch of the button.
"""
def __init__(self, flash_file: pathlib.Path, button_file: pathlib.Path | None = None):
self.flash_file = flash_file
self.button_file = button_file
self.device = None
def start(self) -> CtapHidDevice:
if not SIMULATOR.exists():
pytest.fail(f"{SIMULATOR} not found; build it with: cmake -B build-host -DSKEY_HOST=ON && cmake --build build-host")
env = {**os.environ, "SKEY_FLASH_FILE": str(self.flash_file)}
if self.button_file:
env["SKEY_BUTTON_FILE"] = str(self.button_file)
proc = subprocess.Popen([SIMULATOR], stdin=subprocess.PIPE, stdout=subprocess.PIPE, env=env)
descriptor = HidDescriptor(
path="skey-sim",
vid=0x1209,
pid=0x0001,
report_size_in=PACKET_SIZE,
report_size_out=PACKET_SIZE,
product_name="Skey simulator",
serial_number="sim",
)
self.device = CtapHidDevice(descriptor, PipeConnection(proc))
return self.device
def stop(self) -> None:
if self.device:
self.device.close()
self.device = None
def restart(self) -> CtapHidDevice:
self.stop()
return self.start()
def press(self) -> None:
self.button_file.touch()
def press_after(self, seconds: float) -> threading.Timer:
timer = threading.Timer(seconds, self.press)
timer.start()
return timer
@pytest.fixture
def sim(tmp_path):
simulator = Simulator(tmp_path / "flash.bin")
simulator.start()
yield simulator
simulator.stop()
@pytest.fixture
def device(sim):
return sim.device
@pytest.fixture
def button_sim(tmp_path):
simulator = Simulator(tmp_path / "flash.bin", tmp_path / "button")
simulator.start()
yield simulator
simulator.stop()