Files
Skey/README.md
T
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

148 lines
5.1 KiB
Markdown

# Skey
A FIDO2 / WebAuthn security key built on a Raspberry Pi Pico or Pico 2. It
plugs in over USB and works with browsers for passkeys and 2FA, and with
`ssh-keygen -t ecdsa-sk`. No drivers are needed.
## What works
- CTAP2: makeCredential, getAssertion, getNextAssertion, getInfo, reset, selection
- U2F (CTAP1) register/authenticate, so older sites work too
- ES256 (P-256) credentials with packed attestation
- Up to 64 discoverable credentials (passkeys)
- Touch to confirm, using the Pico's BOOTSEL button
- PIN (clientPIN, PIN/UV auth protocols 1 and 2), for sites that require user
verification
- Unlimited non-discoverable credentials. The credential ID carries everything
needed to re-derive the key, so nothing is stored for them.
Not yet supported: hmac-secret, credential management.
## Layout
```
src/ authenticator core: CTAPHID, CTAP1, CTAP2, CBOR, crypto, storage
firmware/ Pico specifics: USB descriptors, flash, RNG, LED, BOOTSEL button
host/ desktop simulator speaking CTAPHID over stdin/stdout
tests/ python-fido2 tests run against the simulator
tools/ attestation certificate generator
```
The core only talks to hardware through `src/platform.h`, so the simulator
runs exactly the same protocol and storage code as the firmware. The
simulator's flash file even follows NOR rules: erase whole sectors, and
program whole pages that can only clear bits.
## Setup
```bash
sudo apt install build-essential cmake ninja-build git gcc-arm-none-eabi \
libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib libmbedtls-dev \
python3-fido2 python3-pytest libusb-1.0-0-dev pkg-config
git clone --depth 1 https://github.com/raspberrypi/pico-sdk deps/pico-sdk
git -C deps/pico-sdk submodule update --init --depth 1 lib/tinyusb lib/mbedtls
```
Set `PICO_SDK_PATH` instead if you already have an SDK checkout.
## Simulator and tests
```bash
cmake -B build-host -G Ninja -DSKEY_HOST=ON
cmake --build build-host
python3 -m pytest tests
```
The simulator approves every request unless `SKEY_BUTTON_FILE` is set. When it
is set, creating that file counts as a button press. The tests in
`tests/test_presence.py` use this.
## Firmware
```bash
cmake -B build-pico -G Ninja -DPICO_BOARD=pico # or pico2
cmake --build build-pico
```
Hold BOOTSEL while plugging in the Pico, then copy `build-pico/skey.uf2` to the
`RPI-RP2` (or `RP2350`) drive. The board restarts as "Skey FIDO2 Key".
## Using the button
The BOOTSEL button is the touch button. Press it briefly; holding it while
plugging in still enters the bootloader as usual.
| LED | Meaning |
|---|---|
| off | idle |
| on | handling a request |
| fast blink | waiting for a touch: press BOOTSEL |
| slow blink | an older U2F site asked for a touch: press BOOTSEL |
- A press only counts if it happens after the request arrives, and each press
approves one request.
- An unanswered request times out after 30 seconds.
- Closing the browser prompt cancels the wait.
To try it out:
- https://webauthn.io: register, then authenticate
- `fido2-token -L` and `fido2-token -I <dev>` (package `fido2-tools`)
- `ssh-keygen -t ecdsa-sk` for an SSH key backed by the device
On Linux, systemd recognizes FIDO devices by their HID usage page, so no udev
rules are needed. The exception is snap-packaged browsers, such as Ubuntu's
Firefox. Snaps can only open keys on a fixed list of USB IDs, so without the
rule below the browser waits forever and never sees the key:
```bash
sudo cp udev/69-skey-snap.rules /etc/udev/rules.d/
sudo udevadm control --reload
```
Then unplug the key and plug it back in.
## PIN
The key works without a PIN. Once you set one:
- every new registration asks for it;
- logins ask for it when the site requires user verification;
- the key returns account names, so the browser can show which passkey you are
picking.
Set or change it in Chrome under Settings > Privacy and security > Security >
Manage security keys. From a terminal, use `fido2-token` (package
`fido2-tools`):
```bash
fido2-token -L # find the device, e.g. /dev/hidraw11
fido2-token -S /dev/hidraw11 # set a PIN
fido2-token -C /dev/hidraw11 # change it
```
A PIN must be at least 4 characters. 8 wrong guesses in a row lock the PIN
for good, and only a reset (which also deletes every credential) clears it.
After 3 wrong guesses, the key also needs unplugging before it accepts more.
## Security notes
- **Secrets live in flash.** The master secret sits in the last 64 KB of flash
in plain form. Anyone holding the key can read it via BOOTSEL. Pico 2 secure
boot plus OTP storage would fix that.
- **Attestation key.** The first build creates one in `attestation/`
(gitignored). Every key you flash from that checkout shares it, just like a
vendor's batch attestation.
- **Reset window.** `authenticatorReset` only works within 10 seconds of
plugging in.
- **Randomness.** The Pico 2 has a hardware TRNG. The RP2040 relies on ROSC
jitter via `pico_rand`, whitened with SHA-256.
## Roadmap
1. Credential management (list and delete passkeys), then advertise FIDO_2_1
2. hmac-secret / prf extension
3. Pico 2: keep the master secret in OTP and enable secure boot
4. Optional external button on a GPIO pin