Development report: fidati, a FIDO2 U2F software token
Table of Contents
During the last mid-sized pandemic lockdown I had some fun implementing a FIDO2 U2F1 software token in plain Go, called fidati
.
It lives on GitHub .
I didn’t write all this code just for the sake of reading and understanding a (not so great) protocol specification document, I wanted something that could actually be used on a day to day basis.
The firmware included in the fidati
repository is more catered for the development crowd, serving as a reference implementation of what a Tamago-based CTAP1 token should be.
A production-ready implementation can be found in GoKey .
The low-level stuff #
Since I’ve been experimenting with Tamago for some time now so the choice of firmware substrate has been obvious, hence the entirety of fidati
is written in Go.
The FIDO2 U2F protocol works across various transports like USB HID, NFC and Bluetooth LE - for fidati
, I decided to go with USB HID.
HID devices rely on reports to negotiate data formats with its counterpart.
During the descriptor negotiation part the device sends a HID report which describes what kind of data it must {send, receive}, how much, and the meaning of it.
HID drivers use report to identify and categorize devices instead of just descriptors.
There is a special FIDO2 report
that browsers and libraries seeks when querying for authentication tokens, and fidati
uses it.
U2FHID
is the protocol used to encapsulate CTAP messages on top of HID, it’s documented here
.
Once you go past some of the language barriers embedded in that document understanding U2FHID
is straightforward.
Data is exchanged in packets 64 byte long2, some arbitration logic is needed to let multiple softwares (like two browsers running at the same time) use fidati
at the same time.
The communication model is a simple request-response, each command must either execute completely or return an error.
Error types and commands are well-defined, but the standard allows vendor-specific commands3.
While my development board4 is supported by the USB stack included in Tamago, it didn’t provide any HID support, so I had to roll my own .
I also contributed back a small PR regarding the USB descriptor setup.
My experience with the USB protocol is limited, but after digging through multiple docs5 and a lot of swearing I got USB in/out communication over HID.
At the end of this process I had a bare-bones Tamago firmware which identified itself as a USB HID FIDO2 token, with enough logic to be able to parse U2FHID
packets and do {channel, packet} arbitration.
Being a CTAP1 token #
This document describes in much detail what it takes to be a CTAP1 token.
Essentially such token must do three things well: generate ECDSA key pairs, sign blobs and handle a counter.
For each website - called relying party in FIDO lingo - the token must generate and keep a private key.
Counters can either be global or for the individual relying party.
There are two phases in which CTAP1 tokens are involved:
- registration phase, where the website registers itself as a relying party on the token
- authentication phase, where the website asks for a second authentication confirmation to the token
On top of that each FIDO2 U2F token must present a valid X.509 certificate with some identifying informations during the registration phase.
This certificate should be shared by many tokens, and it’s used by relying parties to assess the trustability of it6.
After the initial registration phase, now the token is ready to sign authentication requests.
In the first fidati
iteration, all the generated key pairs and counters were accounted for separately, and stored on the board’s mass storage device (a microSD in my case).
While this approach worked well during the development phase, it wasn’t the best.
microSD’s aren’t known for their durability, and there’s a better way of deriving unique private keys for each website anyway.
The private key derivation function uses a seed to guarantee that each hardware token generates different keys.
The development firmware included in the fidati
repository relies on a microSD as a backing store for the global counter, while GoKey uses a hardware-backed monotonic counter.
Project layout and modularization #
fidati
started out as a complete Tamago firmware package.
Quickly I reached a point where to try new features or bug fixes I had to compile and execute everything instead of relying on standard tools and techniques, like unit tests.
Compiling a Tamago project requires a specialized compiler and build tags since it’s a special target of the ARM platform, without those running tests or complex linters is basically impossible.
Also simple things like go build
might incur in strange error messages due to missing tags, hence missing variables or function definitions.
After a great effort of refactoring, I broke apart big code cluster in separate packages, disconnecting the firmware glue from the core as much as possible.
This refactoring alone meant that I could run simulations of complex message handling in batch, without worrying too much about panic()
-ing routines.
A big chunk of fidati
is unit-tested now, the remaining part are either Tamago-specific code - which might need a separate mocking library on its own - or stuff that I haven’t got time to test yet.
Important tools like golangci-lint
now also work just fine7 on the majority of the codebase, too.
Conclusions and further work #
If there’s one thing fidati
taught me is that working on things that are clearly out of my comfort zone is fun.
I believe the hardest part of this project was getting packet arbitration right, because the FIDO backend software is very susceptible to format errors - this is a good thing.
The second hardest thing was understanding what USB and FIDO specs were telling me, resulting in many frustration moments… but hey, this is software development is all about!
fidati
is far from being perfect but I think it is in a position where progress can be achieved easily, especially on the testing/reliability side.
During the development process, I used the following tools to help me debug protocol quirks and nasty USB HID bugs:
lsusb -vvv
- Wireshark and
usbmon
echo 1 > /sys/module/hid/parameters/debug
to enable HID debugging logsflynn/u2f/u2fhid
- Yubico WebAuthn demo
- mdp’s U2F demo
I have a few interesting points I’d like to develop in the future:
- a Linux userland runtime, to further refine the testing process without having to rely on a development board
- CTAP2 support
- optimization of the
U2FHID
protocol parsing - a logo? 😄
Also known as CTAP1. ↩︎
This is enforced by the HID report. ↩︎
For example, one could implement a device upgrade feature . ↩︎
A USB Armory Mk.II. ↩︎
Often outdated and/or poorly written. ↩︎
For example, relying parties might want to blacklist certain tokens because of known vulnerabilities. ↩︎
It does need some hacks though, like shadowing the system Go installation in favor of Tamago:
env PATH="$HOME/Documents/tamago-go/bin/:$PATH" GO_EXTLINK_ENABLED=0 CGO_ENABLED=0 GOOS=tamago GOARM=7 GOARCH=arm golangci-lint run --build-tags "usbarmory debug" --tests=false ./...
↩︎