tillitis / tillitis-key1

Board designs, FPGA verilog, firmware for TKey, the flexible and open USB security key 🔑
https://www.tillitis.se
395 stars 24 forks source link

How do we retrieve the Tkey name & version? #147

Closed LoupVaillant closed 1 year ago

LoupVaillant commented 1 year ago

So at this point I have read the fine manual, scoured the client Go code and the Firmware C code, to try and decipher what magic numbers exactly I'm supposed to send to the TKey to get its name & version, and all I managed to get so far was the blinking red LED.

Currently, the following program triggers the invalid blinking red state (I'm using Ubuntu):

#include <stdio.h>
#include <stdint.h>

int main(void)
{
    FILE   *tkey   = fopen("/dev/ttyACM0", "r+");
    uint8_t cmd[2] = { 0x10, 0x01, };
    fwrite(cmd, 1, sizeof(cmd), tkey);
    return 0;
}

As far as I understand I'm supposed to send 2 bytes: the framing header and the command. I've read the header should look like this:

The only bit I have to set is bit 4, so 0x10 should work. Now I did notice that your client code uses the value 2 for the tag, but trying 0x50 didn't work either. Plus, I couldn't find any place in the firmware that validates that tag (all 4 values look valid). As for the payload, it looks like it should be just the command byte: FW_CMD_NAME_VERSION = 0x01.

Everything I've read tells me sending {0x10, 0x01}, should work, but all it does is crashing the TKey! What did I miss?

(Note: I have successfully verified my TKey as genuine so I'm pretty sure it works.)

LoupVaillant commented 1 year ago

Ok, after a good night sleep, it appears we need to set the baud rate manually (apparently either Linux or USB is too stupid to fetch/communicate the actual baud rate of the device). Now I need to figure out the way to do that from C — for some reason it seems to require more than one system call.

LoupVaillant commented 1 year ago

I found a way to change the baud rate, but it still doesn't work. Here's what I've tried:

#include <asm/termbits.h>
#include <assert.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <unistd.h>

int main(void)
{
    // Open device
    int tkey = open("/dev/ttyACM0", O_RDWR | O_NONBLOCK | O_NOCTTY);
    assert(tkey != -1);

    // Change output speed
    struct termios2 tio;
    assert(ioctl(tkey,TCGETS2, &tio) == 0);
    tio.c_cflag &= ~CBAUD;
    tio.c_cflag |= BOTHER;
    tio.c_ospeed = 62500;  // This is the TKey's bauds rate, right?
    assert(ioctl(tkey, TCSETS2, &tio) == 0);

    // Send command
    uint8_t cmd[2] = { 0x10, 0x01, };
    assert (write(tkey, cmd, sizeof(cmd)) == sizeof(cmd));

    sleep(1); // We need to wait for the Tkey to crash
    return 0;
}

I basically followed the tty_ioctl (there's an example at the end), and it still doesn't work, the TKey still goes blinking red. Any idea how to make this work? (Short of using Go's stdlib of course.)

dehanj commented 1 year ago

Hi, Just want to let you know that we have seen this and we will get back to you asap, but Mondays some times have a tendency to be full of things you have to to..

LoupVaillant commented 1 year ago

No problem.

Scouring wider and digging deeper, I learned that there are more parameters to serial interfaces than just speed:

Maybe we need to set up more than the speed?

Also, I've found another way to crash the tkey (rest of the program is the same, except includes):

struct serial_struct serial;
assert(ioctl(tkey, TIOCGSERIAL, &serial) == 0);
printf("Baud base: %d\n", serial.baud_base);
serial.custom_divisor = serial.baud_base / 62500;
serial.flags         &= ~ASYNC_SPD_MASK;
serial.flags         |=  ASYNC_SPD_CUST;
assert(ioctl(tkey, TIOCSSERIAL, &serial) == 0);

One thing i've noticed here is that serial.baud_base has a value of a big flat zero. I'm not going very far with such a broken number. I have no idea what is happening, so far the tutorials I've seen on the internet promised me the baud base would be set to the correct value after the ioctl(2) call.

Something's going on, I'll run the official TKey verification program again (on Windows and Linux this time).

LoupVaillant commented 1 year ago

TKey verification app successfully verified my key both on Windows and Linux. I have the purple light, and the following successful output on Linux:

Auto-detected serial port /dev/ttyACM0
Connecting to device on serial port /dev/ttyACM0 ...
Firmware name0:'tk1 ' name1:'mkdf' version:5
TKey UDI: 0x01337081000000ee(BE) VendorID:0x1337 ProductID:2 ProductRev:1
Fetching verification data from https://tkey.tillitis.se/verify/01337081000000ee ...
Verification data was created 2023-05-05T09:22:22Z
Auto-detected serial port /dev/ttyACM0
Connecting to device on serial port /dev/ttyACM0 ...
Firmware name0:'tk1 ' name1:'mkdf' version:5
Loading verisigner-app built from tag:verisigner-v0.0.3 hash:f8ecdcda53a296636a0297c250b27fb6… ...
App loaded.
App name0:'veri' name1:'sign' version:3
TKey firmware was verified, size:4192 hash:3769540390ee3d990ea3f9e4cc9a0d1a…
TKey is genuine!

Now I know for sure my system is capable of talking with the TKey. I just don't how. I guess the proper sequence of system calls is buried somewhere in the Go standard library, but there are so many indirections I have yet to exfiltrate it (I did managed to reproduce their auto-detection though).

mchack-work commented 1 year ago

The speed 62500 bit/second is correct. Sending {0x10, 0x01} or as the Go package does, {0x50,0x01} should work.

You might want to try this with the qemu emulator instead to get some debug output and maybe modifiy it to send even more debug output to the terminal. Compile the firmware without -DNOCONSOLE to be able to see output and use the htif_puts() and friends. Note that qemu uses a pty instead of a real serial port tty. That might come with its own problems.

You may want to begin with using qemu with the test firmware, target testfw.elf, instead of the real firmware to just get some human readable output.

Another way of testing and developing is to use tkey-runapp and load a simple device app that just outputs something. Then attach your C client program to just read something and see that it works.

We mean to supply C libraries on the client side as well, eventually.

LoupVaillant commented 1 year ago

I'll see about trying that, but… if the TKey goes blinking red when I send 2 presumably correct bytes, I can only conclude the data it actually received has been garbled somehow. It is overwhelmingly likely that the QEMU emulator will either:

We mean to supply C libraries on the client side as well, eventually.

That would be real nice.

mchack-work commented 1 year ago

qemu isn't picky about speed at all. We're not emulating the USB controller. If you're seeing garbled data there too, something else is wrong. Might be a useful hint.

LoupVaillant commented 1 year ago

At last, it works!

Seriously, opening a serial port correctly with raw system calls is a nightmare. There's no way I could ever have found how to do that by myself. I'm not aware of any comprehensive tutorial on the internet, so I did the only thing I could: painstakingly prying the standard Go library open.

Here is my working program:

#include <asm/termbits.h>
#include <assert.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

int main(void)
{
    ///////////////////////
    /// BEGIN NIGHTMARE ///
    ///////////////////////

    int tkey = open("/dev/ttyACM0", O_RDWR | O_NDELAY | O_NOCTTY);
    assert(tkey != -1);

    // Get settings
    struct termios2 tio;
    assert(ioctl(tkey,TCGETS2, &tio) == 0);

    // Set local mode
    tio.c_cflag |= CREAD;
    tio.c_cflag |= CLOCAL;

    // Set raw mode
    tio.c_lflag &= ~ICANON;
    tio.c_lflag &= ~ECHO;
    tio.c_lflag &= ~ECHOE;
    tio.c_lflag &= ~ECHOK;
    tio.c_lflag &= ~ECHONL;
    tio.c_lflag &= ~ECHOCTL;
    tio.c_lflag &= ~ECHOPRT;
    tio.c_lflag &= ~ECHOKE;
    tio.c_lflag &= ~ISIG;
    tio.c_lflag &= ~IEXTEN;

    tio.c_iflag &= ~IXON;
    tio.c_iflag &= ~IXOFF;
    tio.c_iflag &= ~IXANY;
    tio.c_iflag &= ~INPCK;
    tio.c_iflag &= ~IGNPAR;
    tio.c_iflag &= ~PARMRK;
    tio.c_iflag &= ~ISTRIP;
    tio.c_iflag &= ~IGNBRK;
    tio.c_iflag &= ~BRKINT;
    tio.c_iflag &= ~INLCR;
    tio.c_iflag &= ~IGNCR;
    tio.c_iflag &= ~ICRNL;
    tio.c_iflag &= ~IUCLC;

    tio.c_oflag &= ~OPOST;

    // Block reads until at least one char is available (no timeout)
    tio.c_cc[VMIN ] = 1;
    tio.c_cc[VTIME] = 0;

    // Disable flow control
    tio.c_cflag &= ~CRTSCTS;

    // Skip modem control, because the following fails:
    //   int status;
    //   ioctl(tkey, TIOCMGET, &status);

    // Disable parity
    tio.c_cflag &= ~PARENB;
    tio.c_cflag &= ~PARODD;
    tio.c_cflag &= ~CMSPAR;
    tio.c_iflag &= ~INPCK;

    // Set data bits
    tio.c_cflag &= ~CSIZE;
    tio.c_cflag |= 8;  // 8-bit bytes

    // Set stop bit (1)
    tio.c_cflag &= ~CSTOPB;

    // Set speed
    tio.c_cflag &= ~CBAUD;
    tio.c_cflag |= BOTHER;
    tio.c_cflag &= ~(CBAUD << IBSHIFT);  // Advised by man page, not in Golang
    tio.c_cflag |= BOTHER << IBSHIFT;    // Advised by man page, not in Golang
    tio.c_ispeed = 62500;
    tio.c_ospeed = 62500;

    // Set modified settings
    assert(ioctl(tkey, TCSETS2, &tio) == 0);

    // Set the port back to blocking (why did we open(2) with O_NDELAY??)
    int flags = fcntl(tkey, F_GETFL);
    assert(flags != -1);
    assert(fcntl(tkey, F_SETFL, flags & ~O_NONBLOCK) == 0);

    // Acquire exclusive access (I guess it makes sense for a serial port)
    assert(ioctl(tkey,TIOCEXCL) == 0) ;

    // Skip Pipe hack that the Go library uses to get around blocking reads

    /////////////////////
    /// END NIGHTMARE ///
    /////////////////////

    // Send command
    uint8_t cmd[2] = { 0x10, 0x01, };
    assert (write(tkey, cmd, sizeof(cmd)) == sizeof(cmd));

    // Read response
    uint8_t rsp_header;
    assert(read(tkey, &rsp_header, 1) == 1);
    uint8_t header_version  = (rsp_header >> 7) & 1;
    uint8_t header_tag      = (rsp_header >> 5) & 3;
    uint8_t header_endpoint = (rsp_header >> 3) & 3;
    uint8_t header_ok       = (rsp_header >> 2) & 1;
    uint8_t header_size     = (rsp_header >> 0) & 3;

    printf("Response header: %x\n", rsp_header);
    printf("    version : %u\n", header_version );
    printf("    tag     : %u\n", header_tag     );
    printf("    endpoint: %u\n", header_endpoint);
    printf("    ok      : %u\n", header_ok      );
    printf("    size    : %u\n", header_size    );

    assert(header_size == 2);
    uint8_t buf[32];
    for (size_t i = 0; i < sizeof(buf); i++) {
        assert(read(tkey, buf + i, 1) == 1);
    }
    char     name[9] = {0}; memcpy(name    , buf    , 8);
    uint32_t version;       memcpy(&version, buf + 8, 4); // Little Endian host

    printf("---\n");
    printf("Name   : %s\n", name);
    printf("Version: %u\n", version);

    return 0;
}

Now I can work. :sleepy:

LoupVaillant commented 1 year ago

One last note: the UART paragraph of the developer handbook briefly mentions the speed of the connection, but I don't see an explicit mention that this translate to a serial interface on the client side. The protocols page in particular describes what bytes should be sent and received at the framing level, but does not say that:

Adding it in might help.

dehanj commented 1 year ago

Great that you got it to work. Thanks for the feedback, we will update the dev-handbook.