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,106 @@
|
||||
"""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()
|
||||
Reference in new issue
Block a user