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>
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
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
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
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 -Landfido2-token -I <dev>(packagefido2-tools)ssh-keygen -t ecdsa-skfor 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:
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):
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.
authenticatorResetonly 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
- Credential management (list and delete passkeys), then advertise FIDO_2_1
- hmac-secret / prf extension
- Pico 2: keep the master secret in OTP and enable secure boot
- Optional external button on a GPIO pin