Так уж вышло, что у меня есть внутренняя софтина, утилизирующая touch policy в YubiKey при аутентификации в SSH и около. Это когда для подтверждения действия (подписи в моём случае) пользователь должен потискать юбикей за контактную площадку (не зря же он называется именно Yubi
Key
). А любой софтине нужны интеграционные тесты, и чем эти тесты ближе к реальности — тем лучше. Ничего не поделаешь, придётся учиться “тискать” YubiKey программно, о чём и будет этот постик.
Каков план?
Итак, нам понадобится:
- сами юбикеи, у меня это крошки YubiKey 5 Nano с capacitive touch сенсором;
- какой-то способ их “трогать” с хоста;
- USB-хаб, в который будем вставлять наши юбики. По моим прикидкам нагрузки, 2–3 штуки должно быть в самый раз;
- всякая мелочёвка, в духе “шапок” для юбикеев.
Вроде ничего не забыл. Погнали экспериментировать!
Про колхоз прототип
Странно было бы думать, что я первый, кому понадобилось симулировать прикосновения к юбику. Поэтому в интернетах можно найти множество решений этой задачки. Например:
После серии экспериментов с конденсаторами, оптопарой и NPN-транзистором я остановился на последнем. С конденсатором (как в посте выше) я вообще не смог добиться стабильной работы, оптопара работала неплохо, а транзистор завёлся совсем отлично — плюс крохотный :) Единственное, важно учесть, что у самого транзистора есть выходная (переходная?) ёмкость, поэтому критично подобрать правильный транзистор — YubiKey очень чувствителен.
Для сборки прототипа пришлось отправиться в ближайший магазин радиодеталей (в моём столе подходящего транзистора не нашлось), из которого выплыл с парочкой BRF90A (Ccb
и Cbe
около 0.3pF) — точно должен подойти.
Осталось набросать небольшую схемку подключения на базе RP2040-One от Waveshare (выбор MCU тут не принципиален — что нашлось в столе, то и пошло в дело):
И подсобрать:
Волнительный момент истины с попыткой стриггерить OTP у юбикея:
Ну не красота ли? Красота! А значит — самое время начинать собирать целевое решение.
Про USB-хаб
Хаб я долго не искал, у меня завалялся четырёхпортовый MH4PU-P от ORICO на базе VL817:
Сам хаб я не могу рекомендовать — он просто лежал у меня в столе без дела. ORICO, как и большая часть производителей подобных хабов, сэкономили целый доллар (или меньше?) и не распаяли управление питанием портов (что сам VL817 вполне себе умеет). Ну да не суть — зато у него отличная выемка для крепления, куда отлично встанет наш аддон!
Про аддон
Разумеется, без изменений в изначальный прототип не обошлось. Я решил:
- заменить RP2040-One на RP2040-Zero от тех же Waveshare;
- заменить BFR90A на BRF182 — последний меньше и SMD, а характеристики плюс-минус одинаковые для нашей задачки;
- увеличить количество триггеров до 4 (иначе зачем всё это);
- добавить каждому триггеру свою индикацию;
- ну и влепить дисплей (WeAct 0.96 Inch OLED IIC) — всё становится лучше на 20% с дисплеем!
С изменениями определились, можно рисовать схемку аддона:
И разводить платку:
Кому интересно поштырить поближе — вот копия в онлайн редакторе.
Дальше — заказ на JLCPCB, неделька на производство + доставку, и вот он, будущий аддон:
Осталось чуть запаять и подсобрать:
Момент истины:
Прекрасная вторая жизнь для хаба, я считаю.
О фирмваре
Железка — это хорошо. Но что такое железка без души? То-то же… Душу для H4ptiX я решил написать на базе Zephyr Project. Не потому что он тут сильно выделяется — просто мне он больше нравится, чем Raspberry Pi Pico SDK ;)
Сама firmware вышла незамысловатой и предоставляет:
- HID-интерфейс для общения с хостом, используя protobuf в качестве формата сообщений (см. rpc.proto);
- возможность кастомизировать продолжительность прикосновения и его задержку;
- индикацию состояния как встроенным RGB на RP2040 Zero, так и дисплеем.
Вместо HID можно было бы использовать CDC. Но:
- я захотел HID, и что вы мне сделаете? Я в другом городе;
- HID выглядит удобнее для небольших бинарных сообщений;
- HID мне всё равно нужен для общения с YubiKey (дискавери, софт-ребут).
Firmware для H4ptiX
можно собрать самому или скачать из релиза. Есть три варианта сборки:
h4ptix_rp2040_lonely.u2f
— для одного юбика (прототип);h4ptix_rp2040_basic.u2f
— базовый аддон на 4 порта;h4ptix_rp2040_display.u2f
— основной аддон с дисплеем.
О софте
Софтовая часть тоже незамысловатая и состоит из двух частей.
Библиотечка на Golang для общения с H4ptix
по HID, минимальный пример:
h, err := h4ptix.NewH4ptix()
if err != nil {
log.Fatalf("create haptix: %v\n", err)
}
usbPort := 1
err = h.Trigger(h4ptix.TriggerReq{
Port: usbPort,
})
if err != nil {
log.Fatalf("ooops, ship happens: %v\n", err)
}
И демонёнок yubictld (тоже на Golang офк), который умеет:
- дискаверить юбики, вставленные в тот же хаб, что и
H4ptiX
(пока только Linux, но будет и остальное); - выбирать случайный свободный юбик и лочить его за клиентом, дабы запуски разных тестов не конкурировали между собой;
- софтово (аппаратно хаб не умеет) ребутать YubiKey с помощью FIDO2 интерфейса;
- и, конечно, “тискает” юбик по команде.
Как пример, простенький test suite с использованием yubictld
, генерацией ключа в PIV с touch policy Always (см. PIV PIN, touch and bio policies):
package test
import (
"context"
"crypto"
"crypto/rand"
"crypto/sha256"
"strings"
"testing"
"time"
"github.com/go-piv/piv-go/v2/piv"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/buglloc/yubictld/pkg/yubictl"
)
type ExampleTestSuite struct {
suite.Suite
yc *yubictl.Yubikey
yk *piv.YubiKey
}
func (s *ExampleTestSuite) setupYubictl() {
svc := yubictl.NewSvcClient("http://localhost:3000")
yk, err := svc.Acquire(context.Background())
s.Require().NoError(err)
s.T().Logf("acquired yubikey: %d", yk.Serial())
s.yc = yk
}
func (s *ExampleTestSuite) setupYubikey() {
cards, err := piv.Cards()
require.NoError(s.T(), err)
var yk *piv.YubiKey
for _, card := range cards {
if !strings.Contains(strings.ToLower(card), "yubikey") {
continue
}
if yk, err = piv.Open(card); err != nil {
s.T().Logf("unable to open yubikey %s: %v", card, err)
continue
}
serial, err := yk.Serial()
s.Require().NoError(err)
if serial != s.yc.Serial() {
_ = yk.Close()
s.T().Logf("skip yubikey: %d", serial)
continue
}
s.T().Logf("open yubikey: %d", serial)
s.yk = yk
break
}
}
func (s *ExampleTestSuite) SetupSuite() {
s.setupYubictl()
s.setupYubikey()
}
func (s *ExampleTestSuite) TearDownSuite() {
s.T().Logf("release yubikey: %d", s.yc.Serial())
err := s.yc.Release(context.Background())
s.Assert().NoError(err)
s.T().Logf("close yubikey")
err = s.yk.Close()
s.Assert().NoError(err)
}
func (s *ExampleTestSuite) SetupTest() {
err := s.yk.Reset()
s.Require().NoError(err)
}
func (s *ExampleTestSuite) TestExample() {
key := piv.Key{
Algorithm: piv.AlgorithmEC256,
PINPolicy: piv.PINPolicyAlways,
TouchPolicy: piv.TouchPolicyAlways,
}
pub, err := s.yk.GenerateKey(piv.DefaultManagementKey, piv.SlotAuthentication, key)
s.Require().NoError(err)
auth := piv.KeyAuth{PIN: piv.DefaultPIN}
priv, err := s.yk.PrivateKey(piv.SlotAuthentication, pub, auth)
s.Require().NoError(err)
signer, ok := priv.(crypto.Signer)
s.Require().True(ok)
data := sha256.Sum256([]byte("foo"))
for {
s.Run("should_ok", func() {
// шедулим тисканье через 1 секунду
err := s.yc.Touch(context.Background(), yubictl.TouchWithDelay(time.Second))
s.Require().NoError(err)
// просим юбик подписать на ключе, который требует ласки
signature, err := signer.Sign(rand.Reader, data[:], crypto.SHA256)
// проверяем, что все пошло хорошо
s.Require().NoError(err)
s.Require().NotEmpty(signature)
time.Sleep(time.Second)
})
}
}
func TestExampleTestSuite(t *testing.T) {
suite.Run(t, new(ExampleTestSuite))
}
В действии:
На этом всё, ура :)
Заключение
Не знаю, как вам, а мне кажется, что получилось довольно просто, но не дурно. Сейчас yubictld
+ H4ptiX
в тестовом режиме работают вместе уже около недели — планирую ещё погонять, собрать валенки и встраивать в CI/CD процесс выкатки.
Конечно, очень не хватает управления питанием USB-портов хаба (особенно если гонять тесты для FIDO2), но об этом — в следующих сериях. Не переключайтесь ;)
На всякий случай уточню, что использовать подобную схему не для тестового окружения идея, прямо скажем, не очень, потому что вы этим сводите всю суть проверки присутствие пользователя на нет.
А пока у меня всё. Всем кота! (づ˶•༝•˶)づ♡