PicoFIDO — свой 2FA-токен из Raspberry Pi Pico за пять баксов

PicoFIDO — свой 2FA-токен из Raspberry Pi Pico за пять баксов

YubiKey стоит как небольшой ужин в ресторане. Raspberry Pi Pico — как стакан кофе. А функция та же: подписывает WebAuthn challenge, хранит passkey и не даёт фишингу шанса. Разница только в том, что внутри Yubico у вас закрытая прошивка и доверие к вендору, а у Pico — открытый код, который можно прочитать, собрать и при желании поправить.

Покажу как это сделать за вечер.


Что понадобится

  • Raspberry Pi Pico (RP2040) или Pico 2 (RP2350) — берите оригинал, не клон, на клонах попадаются битые USB-контроллеры
  • USB-кабель для прошивки и работы
  • Пять минут терпения

Опционально — кнопка и корпус, чтобы выглядело как ключ, а не как голая плата с гвоздями. На AliExpress есть готовые комплекты “под Pico FIDO” за пару долларов.


Прошивка

Идём в polhenarejos/pico-fido releases и качаем последний pico_fido_pico-X.Y.uf2 (для RP2040) или pico_fido_pico2-X.Y.uf2 (для RP2350).

Дальше классический Pico-флоу:

  1. Зажимаем кнопку BOOTSEL на плате
  2. Втыкаем USB в компьютер, кнопку держим
  3. В Finder/Проводнике появляется диск RPI-RP2
  4. Перетаскиваем UF2-файл на этот диск
  5. Pico перезагружается, в системе появляется HID-устройство Pico Key

Всё, токен живой.


Первая настройка — задать PIN

Без PIN ключ работает только как старый U2F-токен (классическая 2FA-кнопка). Passkey’ы сохранять не сможет — это требование протокола CTAP2.

Проще всего задать PIN через Chrome:

chrome://settings/securityKeys

Жмём Create a PIN, вводим минимум 4 символа, тапаем по плате (это и есть подтверждение присутствия пользователя). Готово.

То же можно сделать из терминала через python-fido2:

pip install fido2
python3 -c "
from fido2.hid import CtapHidDevice
from fido2.client import ClientPin
from fido2.ctap2 import Ctap2
import getpass
dev = next(iter(CtapHidDevice.list_devices()))
ClientPin(Ctap2(dev)).set_pin(getpass.getpass('New PIN: '))
"

Регистрация в сервисах

GitHub, Google, Cloudflare, AWS, 1Password — везде один и тот же путь:

  1. Заходим в настройки безопасности аккаунта
  2. Ищем Security keys или Passkeys
  3. Жмём Add new
  4. Браузер просит ввести PIN ключа и тапнуть кнопку
  5. Через секунду сервис привязан

Важный нюанс — на GitHub есть два разных раздела:

  • Two-factor authentication → Security keys — это классический 2FA. Ключ ничего не хранит, сервер сам помнит credentialId. Подходит когда вам нужен второй фактор поверх пароля.
  • Passkeys → Add a passkey — это passkey. Креденшл сохраняется НА токене, вход без пароля. Требует PIN.

Не путайте — раздел разный, поведение разное.


Подводный камень с macOS

Тут я наступил на грабли и потратил вечер на дебаг, так что делюсь.

На стоковой прошивке pico-fido v7.6 + SDK 8.6 есть неприятный баг: обработчик CTAPHID_INIT на первом вызове запускает тяжёлую инициализацию (генерация ключей при первом запуске или инкремент OTP-счётчика), которая заканчивается записью во flash. А запись во flash на RP2040 требует отключения всех прерываний на 50-150 мс — за это время macOS успевает решить, что USB-pipe стух, и больше команды на токен не шлёт. До физического передёргивания кабеля.

Проявляется так: Chrome, Safari и Firefox дружно показывают “ждём ключ”, при том что в ioreg устройство видно, а Python через hidapi честно получает ответ на ПЕРВЫЙ CTAPHID_INIT — а на второй уже таймаут.

Фикс — однострочный, перенести init_fido() из обработчика CTAPHID_INIT в picokey_init(), который вызывается на boot ДО старта USB. Я уже отправил PR в апстрим, но пока не смерджили — если столкнётесь, собирайте с моего форка или применяйте патч руками.

Проверить что у вас не этот баг можно так:

python3 -c "
import hid, os, struct
t = next(d for d in hid.enumerate() if d['vendor_id']==0x2E8A and d.get('usage_page')==0xF1D0)
dev = hid.device(); dev.open_path(t['path'])
for i in range(5):
    nonce = os.urandom(8)
    dev.write(bytes([0])+struct.pack('>IBBB',0xFFFFFFFF,0x86,0,8)+nonce+b'\x00'*47)
    r = dev.read(64, timeout_ms=2000)
    print(f'INIT {i+1}: {\"OK\" if r else \"FAIL\"}')
"

Все пять должны пройти. Если падает на втором — у вас тот самый баг.


Итог

За цену чашки кофе и час времени у вас на руках полноценный FIDO2-аутентификатор: passkey’и, U2F, OTP, всё открытое, всё проверяемое. Не для параноиков с золотыми наручными — а для нормальной личной гигиены, чтобы не зависеть от вендора и не платить $50 за чёрный пластиковый брелок.

Один минус — DIY-токен не получит сертификацию FIDO Alliance, поэтому строго регулируемые сервисы (некоторые банки, госконторы) его могут не принять. Для всего остального — пользуйтесь на здоровье.


Фото на обложке: Michael H. (Laserlicht) / Wikimedia Commons, CC BY-SA 4.0