Severity: Low.
A vulnerability has been found in tkey-device-signer and verisigner that makes it possible to disclose portions of the TKey’s data in RAM over the USB interface. To exploit the vulnerability an attacker needs to use a custom client application and to touch the TKey.
The threat model defines a set of assets, attack types and threat actors.
This vulnerability analysis shows that the attack is of software type and,
*type 1, 2 or 3 as defined in the threat model.
Tillitis recommends:
*Custom client applications using tkey-device-signer are also recommended to be updated.
Note: Updating the device application changes the identity, meaning that the Ed25519 key pair also changes. All services using your current identity need to be updated with the new identity. See this guide.
All client applications running tkey-device-signer version 0.0.7 and 0.0.8.
From Tillitis this includes:
All client applications running verisigner v0.0.3 and earlier.
From Tillitis this includes:
The following release contain security updates that address the vulnerability described in this bulletin:
The following client applications are updated to embed tkey-device-signer v1.0.0:
For user compiled applications, see section Reproducible builds below.
Note: verisigner is going to be deprecated in favor of using tkey-device-signer v1.0.0 in the forthcoming tkey-verification v1.0.0.
The vulnerability is of type CWE-367: Time-of-check Time-of-use (TOCTOU). If this is exploited it allows for a CWE-125: Out-of-bounds Read.
An exploit is made by setting an increasingly larger size of the message to sign (using CMD_SET_SIZE) and relying on the TOCTOU bug to get additional data in the message.
First the message size is set to 4096 bytes (maximum size), then immediately a new message size of 4097 bytes is set. The device app will return an error but still use the new size (4097). Another variable that keeps track of what is left to be sent by the client keeps the old value (4096).
When 4096 bytes of data is sent from client to TKey, this creates a signature of the sent data plus the extra byte in memory according to the new message size (4097). Repeating this method again, but using a size of 4098 bytes, will include yet another byte of RAM in the signature. This is continued with 4099, 4100, …
The signature returned is computed over an unknown byte every time. One can iterate over every possible value of the last byte (brute forcing) and do a signature verification. When the signature verifies correctly the unknown byte is revealed.
Eventually the signing operation’s own memory is part of the message to sign. The message itself will change during the signing operation, making it much harder and probably impossible to produce verifiable signatures over a known message. This stops the exploit.
There are a few interesting assets in RAM:
The locations in memory are:
Version | location of message | location of private key | location of signing context |
0.0.7, verisigner-v0.0.3 | message = 0x4001ec20 | local_cdi = 0x4001ebc0 | ctx = 0x4001fd40 |
0.0.8 | message = 0x4001ee98 | secret_key = 0x4001edf8 | r = 0x4001ebe0* |
The memory visualized for v0.0.7 and verisigner-0.0.3:
--------------------------- <--- 0x40020000
| STACK |
| .. |
| Signing context | <--- An exploit stops here
| .. |
| message ↑ | <--- start of exploit, grows
| .. | towards higher addresses
| |
| Private key |
| .. |
| - - - - - - - - - - - - |
| .. |
| |
| Unused area, |
| randomized at power up |
| |
| .. |
| - - - - - - - - - - - - |
| APP |
--------------------------- <--- 0x40000000
The memory visualized for v0.0.8:
--------------------------- <--- 0x40020000
| STACK |
| .. |
| message ↑ | <--- start of exploit, grows
| .. | towards higher addresses | |
| Private key |
| .. |
| Signing context | <--- An exploit stops here
| .. |
| - - - - - - - - - - - - |
| .. |
| |
| Unused area, |
| randomized at power up |
| |
| .. |
| - - - - - - - - - - - - |
| APP | <--- Wraps and continue to read upwards
--------------------------- <--- 0x40000000
The message buffer contains the data to be signed and grows towards higher addresses, so the out-of-bounds read occur on addresses above the message buffer’s end. In order to include the private key in the signing operation, all the previous content in RAM needs to be leaked first.
The signing context is located in between the message and the private key for all versions, v0.0.7, v0.0.8, and verisigner-v0.0.3. The difference in v0.0.8 is the need to wrap around and continue to read from the start of RAM. The possibility to wrap around and continue to read from the beginning of RAM is an unwanted behavior in the FPGA design. This is addressed in release TK1-2024.03.
The location of the signing context on the stack is crucial for stopping an exploit. The signing context is used by the Ed25519 signing operation to store intermediate calculations. In order to leak it you have to include the signing context as part of the message being signed. However, during the signing the message is used in two separate hash operations. Since the execution is in series this means that the context, which is included in the message to be signed, will change between these two function calls. This makes it impossible to create a valid signature over a known message.
The implementations differ, but the same operations are performed. The details below are from v0.0.8, but the same general idea applies for v0.0.7 and verisigner-v0.0.3.
When generating a signature the function call looks like this:
static void ed25519_dom_sign(u8 signature [64],
const u8 secret_key[32],
const u8 *dom, size_t dom_size,
const u8 *message,
size_t message_size)
{
u8 a[64]; // secret scalar and prefix
u8 r[32]; // secret deterministic "random" nonce
u8 h[32]; // publically verifiable hash of the
message (not wiped)
u8 R[32]; // first half of the signature (allows
overlapping inputs)
const u8 *pk = secret_key + 32;
crypto_sha512(a, secret_key, 32);
crypto_eddsa_trim_scalar(a, a);
hash_reduce(r, dom, dom_size, a + 32, 32, message, message_size, 0, 0);
crypto_eddsa_scalarbase(R, r);
hash_reduce(h, dom, dom_size, R, 32, pk, 32, message, message_size);
COPY(signature, R, 32);
crypto_eddsa_mul_add(signature + 32, h, a, r);
WIPE_BUFFER(a);
WIPE_BUFFER(r);
}
The interesting section in this function is:
hash_reduce(r, dom, dom_size, a + 32, 32, message,
message_size, 0, 0);
crypto_eddsa_scalarbase(R, r);
hash_reduce(h, dom, dom_size, R, 32, pk, 32, message,
message_size);
The message, which is a pointer to a location on the stack, is used twice in order to calculate the signature. The variable r, which is of type u8 r[32], stores the return value of hash_reduce() on the stack. This gives that, when the context is included in the message to be signed, the content of message will change before the next function call of hash_reduce(). Also note that a, which is used in the first hash_reduce(), is derived from the secret key so r cannot be considered guessable or known.
The exploit is effectively stopped by the order of the data on the stack. C compilers don’t provide any guarantee for how the data is ordered, and can be vastly different depending on compiler, compiler version, and compiler optimization level.
Tillitis have reproducible builds and verification of both tkey-device-signer and verisigner in place, so the analysis above covers binaries built with other compiler versions as long as they have the same SHA512 digests. The digests are included in the repositories. The recommended way of building is to use the OCI image, tkey-builder, with the right version for the device application’s tag. See the device applications’ documentation in the repositories for more information.
We would like to thank the security researcher Sergei Volokitin of Hexplot, who discovered the vulnerability and reported it through our bug bounty program.
2024-04-16 17:00 CEST: Initial publication