Playing with NitroKey 3 -- PC runner using USBIP

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.