I’ve been wanting to use my brand new NitroKey 3, but TOTP is not supported yet. So, I’m looking to implement it myself, since firmware and tooling are open-source.
NitroKey 3’s firmware is based on Trussed framework. In essence, it’s been designed so that anyone can implement an independent Trussed application. Each such application is like a module that can be added to Trussed-based product. So if I write a Trussed app, I’d be able to add it to NK3’s firmware.
If you don’t know what TOTP is, or know roughly what it is, but want to understand the plumbing, I wrote a series of 3 blog posts about TOTP and HOTP:
Part 1 - the gist of how TOTO and HTOP work
Part 2 - details specific to HOTP
Part 3 - details specific to TOTP and common to both, TOTP and HOTP
Where do I start?
Trussed is a framework. By itself it’s not enough for anyone to run applications. It requires something called a runner. On a USB key one may think of it as an equivalent of operating system’s kernel that’s responsible for triggering/dispatching between individual apps. It is also possible to implement a runner as a PC application. This approach is great for testing the Trussed apps that you’re developing.
One such runner is PC-USBIP. It simulates a USB device that you can interact with, and, therefore, you can test your new Trussed app without flashing it to actual USB key.
Running PC-USBIP on Debian
Since I’m using Debian, I’ve been looking to making it work on Debian specifically. The 1st thing I had to do was installing necessary packages on my Bullseye:
$ sudo apt install -y libclang-11-dev usbip
The USBIP runner and Trussed are written in Rust and use Rust’s package management system, Cargo. You’ll need fairly recent version of both. The one in Bullseye is a bit dated, so I installed Rust and Cargo locally in my home directory using instructions provided on Rust’s Getting Started guide. Now that I have them both:
$ which rustc
/home/patryk/.cargo/bin/rustc
...
$ which cargo
/home/patryk/.cargo/bin/cargo
I can actually start the runner:
$ make
cargo build --features=enable-logs
Compiling version_check v0.9.4
Compiling proc-macro2 v1.0.40
... (long list of external Rust packages being compiled)
env RUST_LOG=debug cargo run --features=enable-logs &
sleep 1
Finished dev [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/usbip-simulation`
INFO usbip_simulation > Initializing Trussed
INFO solo_usbip::platform::store > Created new state file
INFO usbip_simulation > Initializing allocator
DEBUG usbip_device > initialized new endpoint Endpoint { pipe_in: None, pipe_out: Some(Pipe { data: [], ty: Interrupt, max_packet_size: 64, interval: 5 }), pending_ins: [], stalled: true, setup_flag: false, in_comp
lete_flag: false } as address 1
DEBUG usbip_device > initialized new endpoint Endpoint { pipe_in: Some(Pipe { data: [], ty: Interrupt, max_packet_size: 64, interval: 5 }), pipe_out: Some(Pipe { data: [], ty: Interrupt, max_packet_size: 64, interv
al: 5 }), pending_ins: [], stalled: true, setup_flag: false, in_complete_flag: false } as address 1
DEBUG usbip_device > initialized new endpoint Endpoint { pipe_in: None, pipe_out: Some(Pipe { data: [], ty: Control, max_packet_size: 8, interval: 0 }), pending_ins: [], stalled: true, setup_flag: false, in_complet
e_flag: false } as address 0
DEBUG usbip_device > initialized new endpoint Endpoint { pipe_in: Some(Pipe { data: [], ty: Control, max_packet_size: 8, interval: 0 }), pipe_out: Some(Pipe { data: [], ty: Control, max_packet_size: 8, interval: 0
}), pending_ins: [], stalled: true, setup_flag: false, in_complete_flag: false } as address 0
INFO usbip_device > usb device is being enabled
INFO usbip_simulation > Ready for work
lsmod | grep vhci-hcd || sudo modprobe vhci-hcd
sudo usbip list -r "localhost"
INFO usbip_device::handler > new connection from: 127.0.0.1:36418
DEBUG usbip_device::op > request version is 273
INFO usbip_device::op > received request to list devices
Exportable USB devices
======================
- localhost
1-1: Pandora International Ltd. : unknown product (1111:1010)
: /sys/devices/pci0000:00/0000:00:01.2/usb1/1-1
: (Defined at Interface level) (00/00/00)
: 0 - (Defined at Interface level) (00/00/00)
sudo usbip attach -r "localhost" -b "1-1"
INFO usbip_device::handler > new connection from: 127.0.0.1:36428
DEBUG usbip_device::op > request version is 273
INFO usbip_device::op > received request to connect device 1-1
INFO usbip_device::handler > device is leaving reset state
sudo usbip attach -r "localhost" -b "1-1"
DEBUG usbip_device::handler > UsbIpRequest { header: UsbIpHeader { command: Request, seqnum: 1, devid: 65538, direction: IN, ep: 0 }, cmd: Cmd(UsbIpCmdSubmit { transfer_flags: DIR_MASK, transfer_buffer_length: 64, setup: [
... (logs continue)
And then on another terminal:
$ lsusb
...
Bus 009 Device 003: ID 20a0:42b2 Clay Logic FIDO authenticator
...
Alright, so far so good – the simulated USB device is there. 😀
Talking to the simulated key using nitropy
Alright, now it’s time to try and talk to the simulated USB key. There’s a CLI tool for NK3 specifically, pynitrokey. So:
$ git clone https://github.com/Nitrokey/pynitrokey.git
...
$ cd pynitrokey
$ python3 -m venv venv
$ source venv/bin/activate
(venv) $ make init # This installs a bunch of dependencies
(venv) $ make build # This builds nitropy tool and puts it in venv/bin, which we're using
As mentioned in pynitrokey
’s
Developer’s Guide
you also have to patch venv/lib/python3.9/site-packages/fido2/hid/base.py
file as shown
here
.
Basically replace the statement raise OSError("failed to write entire packet")
with pass
within write_packet()
method of the FileCtapHidConnection
class.
After doing that:
(venv) $ nitropy fido2 set-pin
Command line tool to interact with Nitrokey devices 0.4.26
Please enter new pin:
Please confirm new pin:
done - please use new pin to verify key
That looks good. I was able to set the pin on my fake USB key – on the console I used to start the USBIP runner
I saw a bunch of logs after that set-pin
operation. If that fails for you, however, you’ll have to restart
USBIP simulation – that should fix the problem. To do that, execute the following in the terminal window you used
to start the simulator:
$ make stop
sudo usbip detach -p "00"
usbip: info: Port 0 is now detached!
killall usbip-simulation
$ make start-sim
...
$ make attach
...
And then try again. In addition to that, when you stop the simulation and don’t restart it, and then try setting the pin, you’ll see something like this:
(venv) $ nitropy fido2 set-pin
Command line tool to interact with Nitrokey devices 0.4.26
Please enter new pin:
Please confirm new pin:
Critical error:
failed setting new pin, maybe it's already set?
to change an already set pin, please use:
$ nitropy fido2 change-pin
Exception encountered: NoSoloFoundError('no Nitrokey FIDO2 found')
--------------------------------------------------------------------------------
Critical error occurred, exiting now
Unexpected? Is this a bug? Would you like to get support/help?
- You can report issues at: https://support.nitrokey.com/
- Writing an e-mail to support@nitrokey.com is also possible
- Please attach the log: '/tmp/nitropy.log.vcnartw7' with any support/help request!
- Please check if you have udev rules installed: https://docs.nitrokey.com/nitrokey3/linux/firmware-update.html#troubleshooting
Yeah, talking to the simulated USB key works! Next I’ll try to create a Hello World Trussed app and use it
within my PC-USBIP runner. And I’ll try modifying pynitrokey
to talk to the simulated key and talk to my
Hello World app.