Newer
Older

Tillitis TKey - first look

Date: 2026-04-13 22:00
Tags: tillitis-tkey

I got my hands on a Tillitis TKey, an open-source open-hardware security token, to experiment with.

Device overview

It turns out that USB security tokens are all different. The TKey has a touch button, "a" status LED (three separate LEDs, R/G/B), and a true random number generator, but lacks other features that might be required in some applications, such as persistent storage, a fingerprint reader, or a display screen. We'll see later how a stateless device can still hold secret keys. The type-C USB plug is annoying, especially because it will be unplugged and re-plugged a lot (as we'll see later); of course a non-standard type A adapter can be used. Physically, it also has a loop for a lanyard or keychain.

Electronically, it's an FPGA, connected by a UART interface to a small microcontroller which handles the USB connection. The UART is an intentionally simple interface implementing a security boundary. Bytes can be transferred from one side to the other with no complicated protocols at this layer, so that even if the USB microcontroller is compromised (as USB is a complex protocol) it can't affect the FPGA (other than jamming the data lines, perhaps). Sadly (read on) this also means there's no way to reset the FPGA from the USB side.

With the default "Bellatrix" firmware, the FPGA implements a RISC-V processor.

Two variants of the TKey are available for purchase: the ordinary TKey, and the TKey Unlocked. The ordinary TKey is finalized by programming the firmware and unique device key into the one-time-programmable memory in the FPGA. After this, it is not possible to reprogram the FPGA. The TKey Unlocked ships without this final step. The FPGA will fall back to loading from flash memory, and you can reconfigure it multiple times. It requires an extra programming tool, which is some flavour of Raspberry Pi soldered to an adapter board that mounts the TKey. If you take this option, you can choose to execute the OTP step yourself, permanently locking the FPGA bitstream, and a secure key of your own choosing. Since I am more interested in programming security applications rather than firmware, I got an ordinary TKey.

There are 4 GPIOs (more precisely, 2 GPIs and 2 GPOs) which are normally unused, and unreachable when the TKey is in its plastic case. If you break open the case you can solder something to them, and you can use them in application code, even with the current firmware. Of course, unless you implement some kind of secure handshake, whatever you attach here can be accessed by any application that runs on the TKey.

Principles of operation

When the TKey starts receiving power, it runs a bootloader. The host computer then uploads the application to the TKey by interacting with the bootloader. Once that is completed, the TKey jumps to the application and the host computer interacts only with the application until power is removed. No deviation from this process is permissible—any protocol error or software error causes the TKey to halt processing and flash red until power is removed.

Once application software has been loaded, there is no way to return to the bootloader until power is removed. This seems rather inconvenient—I wish they would have connected one of the UART control lines to the FPGA's reset pin. On the other hand, maybe there's a security concern about malicious host apps changing the application without your notice. Either way, if you're developing application software, it's probably a good idea to put some kind of USB power switch in line with the device. The next firmware revision, "Castor", is supposed to solve this situation.

The application doesn't have access to the device secret key. It only gets access to a sub-key which is derived from the device secret key, the hash of the application code, and an optional extra salt the host computer can supply (which is invisible to the application). This is how the key manages to "store" secret keys while not having any persistent state—it derives the same keys every time the same program is loaded. At the same time, if you try to read out the key by loading a key-readout program, you get a different key and your real key remains safe.

This also means if you fix a bug in the application, the key changes. That's probably what you want in a secure element. Whenever you use a USB security token, you should definitely have an independent recovery procedure to resolve this and other situations.

Compiling an application

Tillitis provides the tkey-libs repository with base components useful for compiling TKey applications. It's quite minimal and you could get by without it. Note that libsyscall is for the Castor firmware, so ignore that. In Bellatrix, the application is the only software running on the TKey once it is loaded.

I tried to compile the example-app from tkey-libs. clang just worked, but I didn't have a RISC-V linker installed, so that step failed. Anyway, I set myself up with a suitable toolchain by running crossdev --target riscv32-unknown-none-elf (your distro may vary) and then it worked. The example app compiled to a 200-byte binary at tkey-libs/example-app/blue.bin.

Running an application

Once I had compiled the example application, I loaded it with tkey-runapp from the tkey-devtools repository.

I had no problem compiling tkey-runapp and using it to load blue.bin onto the TKey. It's written in the Go language, so it requires a Go compiler. Once the app was loaded, the LEDs on the TKey started flashing in red-green-blue order. Success!

I haven't written any of my own apps yet, but it shouldn't be difficult to do...

Compiling an application (yourself)

What if we don't want to be tied to tkey-libs?

It's quite minimal—we can replicate everything it does, easily enough—except maybe the cryptographic operations, but you can use third-party code there.

Most importantly, we need the compiler scripts. We need to tell the compiler what kind of machine it's compiling for, and tell the linker where the program will be loaded in memory.
The clang compiler flags are -target riscv32-unknown-none-elf -march=rv32iczmmul -mabi=ilp32 -mcmodel=medany.
The memory layout is a flat binary loaded at 0x40000000 to the compiler, and up to 128KiB in size. Remember to set the stack pointer—Tillitis's libcrt0.S sets it to the top of memory—and has absolutely no protection against it crashing into other variables if too much stack is used, so be careful.
The program counter starts at address 0x40000000.
Optionally, you can set the memory protection range registers to force the CPU to crash if it ever executes an instruction outside the code segment.
There's no opposite protection that prevents writes outside of the data segment.

Running an application (yourself)

What if we don't want to be tied to tkey-runapp? (It's a Go program hosted on GitHub, after all)

The protocol is straightforward and well-documented.

The biggest hurdle here is that the serial port must be set to 62500 baud. Since there's a real UART interface in the board, not just a simulation of one, the baud rate actually matters and if you do not set this rate, the data will be corrupted in transit.

This is a problem because 62500 is not a standard IBM PC baud rate and, thanks to some really old API decisions in Linux, most Linux tools like stty only handle the standard baud rates.

As soon as you load application software, the application has full control of the serial port and you don't have to follow the documented protocol spec.