Exploring transparency apps for the Tillitis TKey

Author: Niels Möller
Date: 2025-10-19


It’s common practice to protect sensitive signing keys using a hardware security module (HSM). An HSM allows its users to sign messages, but not extract private keys for use at a later time and place.

However, when an HSM is used for online signing, remote compromise of the host the HSM is attached to allows the attacker to sign anything; the HSM only prevents key extraction. Let’s do better than that!

With a sufficiently flexible HSM, we can make it enforce additional constraints on the messages it signs, preventing important types of attacks.

The Tillitis TKey is such a flexible HSM. In this blog post we’ll look at how to use it to improve protection of signing keys related the Sigsum transparency system, and we’ll see how HSMs and transparency systems can complement each other. We have to start with a brief background on Sigsum and TKey.

Sigsum

The Sigsum system is a minimalistic transparency log system, to enable detection of unexpected or malicious use of a signing key. E.g., consider malicious but signed software updates, distributed to certain targeted users only. The objective of the Sigsum system is that every signature that is accepted by a user will also be visible to relevant monitors; that will not prevent the attack, but it enables detection.

The split-view problem

A split-view attack is when an attacker creates a proof of logging that appears valid, and at the same serves monitors a version of the log where the malicious log entry is missing. In the Sigsum system, the trusted cosigning witnesses protect against split-view attacks, which depends on the security of the witnesses’ signing keys. An attacker that gains access to the signing keys of a Sigsum log server and several of its cosigning witnesses, can mount a split-view attack.

Tillitis TKey

The Tillitis TKey is a radically open source USB hardware security device which allows running small arbitrary applications in a more secure environment. Each device is provisioned with a unique device secret (UDS). However, the UDS is not accessible to applications, instead, the firmware’s app loader hashes the UDS together with the the app binary, and only the result is made available to the app. This gives each app its own secret.

The current TKey version, “Bellatrix”, does not give apps any way to store persistent state on the device. The best an app can do is to ask the host to store an encrypted blob for it. However, the upcoming TKey “Castor” release lets an app allocate an area on the TKey flash chip for the app’s own use. This makes new types of apps possible.

Examples

Let us now look at a few examples of how to combine transparency techniques with an HSM that is capable of enforcing constraints on the data to be signed.

  1. Bringing transparency to legacy systems.

    There are plenty of systems that can enforce digital signatures, but will not be able to enforce transparency logging any time soon. E.g., verified boot mechanisms such as Intel BootGuard and UEFI Secure Boot. What if we could store a private signing key in an HSM, tied to an app that signs a message using that key only if the data to be signed has already been transparency logged? The HSM then needs to verify a Sigsum proof of inclusion.

    By provisioning the HSM’s corresponding public key, e.g., as a trusted key for Secure Boot, we get discoverability of the signatures that will be accepted by Secure Boot, without the server firmware being aware of it.

  2. Preventing log server key misuse.

    When operating a transparency log, there’s a risk of signing split-views, be that due to operator mistakes, or host compromise. What if we could store the log’s private key in an HSM tied to an app that records the most recent log state signed, and requires a valid consistency proof before signing an updated state?

    The HSM could even help enforcing log replication, by requiring signatures by log mirrors. The mirrors certify that all data corresponding to the new tree head is stored reliably.

  3. Preventing witness key misuse.

    An HSM app for a witness should also reject attempts to sign split-views. This is similar to the above log-signing app, but with records of the most recently signed state for several logs. For each cosignature, the HSM requires a valid consistency proof to the previous state of the right log.

Implementing transparency operations on the TKey

TKey device applications run essentially on the bare metal, on a 32-bit risc-v soft core. The TKey has 128 KiB of RAM. For the Castor model, at most 8 apps can use persistent storage, with an flash area of 128 KiB allocated per app.

TKey apps are typically written in C, but with rather limited library support. So a prerequisite of doing transparency log operations is a C implementation of needed primitives. We do this with the work-in-progress sigsum-c library, including verification of inclusion and consistency proofs, verification of Sigsum proofs, and code to parse the ASCII representation of a Sigsum proof and a Sigsum policy file. In other words, everything needed for off-line verification, but no support for log submission or monitoring.

----------------------------------------------------------
File                   blank        comment           code
----------------------------------------------------------
lib/policy.c              77             61            452
lib/verify.c              34             38            205
lib/ascii.c               41             27            168
lib/proof.c               24             32            119
lib/merkle.c              24             51             99
lib/checkpoint.c          13             27             79
lib/ascii.h               21             28             40
lib/crypto-internal.c     10             23             29
lib/crypto.h              15             26             27
lib/proof.h               12             23             26
lib/xalloc.c               9             23             24
lib/policy.h              11             23             23
lib/crypto-internal.h     15             24             20
lib/merkle.h              11             24             18
lib/checkpoint.h          13             30             15
lib/verify.h               9             29             12
lib/crypto.c               6             23             10
lib/xalloc.h               9             23              8
----------------------------------------------------------
SUM:                     354            535           1374
----------------------------------------------------------

Figure 1: Library source files in sigsum-c.

For the underlying cryptographic primitives, the GNU Nettle library is used, cross-compiled into static library files for risc-v. Nettle is configured to use mini-gmp for all bignum operations, which reduces code size and build complexity compared to using the full GNU GMP library, at the cost of lower performance.

To reduce parsing logic needed on the TKey, we use a simple binary serialization of objects such as consistency proofs or a Sigsum proof of logging. Policy is a bit more complex, in particular the representation of the quorum rule. To make the device code simpler, a compiled policy representation is used (the ASCII policy format defines the quorum by naming groups and witnesses and arranging them into a tree, while the compiled policy represents the quorum as a sequence of byte codes executed on a very simple stack machine). The sigsum-c project includes a conversion tool, see tool/sigsum-compile-policy.

Prototype apps

All code is in the tkey-sigsum-apps repository, with dependencies (sigsum-c, nettle, and a forked version of tkey-libs with workarounds) listed as git submodules. The apps/ directory contains the code intended to run on the TKey device, and the host/ directory contains the host tools, which are written i Go and interact with the corresponding TKey app via USB.

+---------------+                  +------+---------+
| Host PC       |   /dev/ttyACM0   | TKey | storage |
|               | <--------------> |      +---------+
| log-signer.go |       USB        | log-signer.c   |
+---------------+                  +----------------+

Figure 2: Communication between host and device app, with
device app accessing persistent storage on the TKey.

The apps can be run in Tillitis’ qemu, or on real hardware if you you have a TKey Castor prototype (a plain “TKey unlocked” can be turned into a Castor prototype by reflashing both the main firmware and the separate USB controller firmware).

Transparency for legacy systems

The signed-if-logged app targets the legacy use case: Adding transparency to existing systems that enforce signatures, but are unaware of transparency logging.

The compiled size of the device app is 80500 bytes (where roughly 16 KiB each are used for the lookup tables for the two supported elliptic curves). The signing operation, using ECDSA, takes approximately 15s, including the Sigsum proof verification.

The corresponding host tool is host/sign-if-logged/.

The key (pun intended) feature of this app is that it uses a signing key that is restricted to signing messages that have been transparently logged. To understand how it works, let’s look at the different phases after the TKey is inserted and powered up.

  1. The app is loaded onto the tkey using the tkey-runapp tool. The user has the option of supplying a “user secret” at this stage. The TKey firmware hashes together its builtin unique device secret, the app binary, and the user secret, if any. The result is the Compound Device Identifier (CDI) that is made available to the app (while the underlying unique device secret is not accessible).

  2. The host specifies the Sigsum policy to use, the list of authorized Sigsum submitter keys, and the kind of signature desired. There are currently four signature types, either Ed25519 or ECDSA using SHA256 and the secp256r1, and each one with or without requiring touch/physical presence before making a signature. On the host, run sign-if-logged --port SERIAL-PORT init [-t TYPE] KEY-FILE POLICY-FILE where the key file lists submitter keys, in OpenSSH public key format, one key per line, and the policy file is a binary compiled policy.

    The app uses the CDI provided by firmware as a key for HMAC-SHA256. It computes the HMAC over a message consisting of the signature algorithm byte, the list of submitter public keys, and the policy, and derives the private signing key to use from this HMAC digest.

    The compiled policy, as well as the representation of submitter keys, is canonicalized before passed to the app, so that uninteresting changes to the input text files, like reorder and comments, do not change the derived key.

  3. The host likely wants to query the app for the corresponding public key. This is done using the sign-if-logged --port SERIAL-PORT pubkey [-o OUT-FILE] command. The output is a hex string, starting with the algorithm type byte, followed by the public key (32 bytes for Ed25519, 65 bytes (including a leading 0x04 byte indicating an uncompressed point) for ECDSA).

  4. To sign a message, it must first be signed and submitted to a Sigsum log in the usual way (typically, using the sigsum-submit tool). The signing key used here must be one of the submitter keys authorized in Step 2 above. Next, the message and the Sigsum proof are passed to the app, using the command sign-if-logged [--port PORT] sign [-o OUT-FILE] MESSAGE-FILE [PROOF-FILE]. The proof file defaults to the message file followed by a .proof suffix. The message is of any size (up to some limit imposed by the app), and the Sigsum “checksum” in the log is the double SHA256 of the message file.

    The app receives a binary serialization of proof and message. It verifies the sigsum proof against the configured policy and list of submitter keys, using the sigsum_proof_verify function provided by the sigsum-c library. If all is well, the app waits for a touch event, if that is required for the configured algorithm type, signs the message, and returns the signature.

    For Ed25519, the message is signed directly (no “prehash”). For ECDSA, the message is hashed using SHA256, and this hash is signed (which means that in this case it’s technically not necessary to compute that hash on the TKey, it could by done on the host side).

    In either case, the signature is 64 bytes, represented as hex when output by the sign-if-logged tool on the host.

Raw hex format for keys and signatures was chosen for simplicity of implementation. The host/verify/ tool understands the used formats and can verify signatures of this form. They could be converted to any other standard formats as a post-processing step.

Note that this app doesn’t use persistent storage, so it should be a small matter of programming to make it work also for the more easily available TKey Bellatrix.

Log server consistency

The log-signer app is intended to be used by a Sigsum log operator. By requiring a consistency proof before signing a new tree head, it prevents the log server from participating in a split-view attack, even if the host is compromised, or misbehaving for some other reason.

It depends on persistent storage, a new feature in the next version of the TKey, code named “Castor”. The compiled size of the device app is 55300 bytes, and the signing operation takes approximately 0.7s.

The corresponding host tool is host/log-signer/. If we start with the tool, it has one subcommand for each operation supported by the app.

Use of storage

The app gets 128 KiB of storage, initially holding all-one bytes. The write operation is essentially a logical AND-operation, allowing the app to change any one-bit into a zero-bit. The app organizes the storage as 2048 records of 64 byte each. Changing a zero bit back to a one requires an erase operation; and each erase operation affects complete and aligned blocks of 4096 bytes. The number of reliable erase/write cycles is limited by the flash hardware. (Usually a rather large number. The flash chip on the TKey is specified for at least 100k erase/write cycles. With the round-robin writing scheme described below, this is enough for almost 200M state updates. With a state update every 10s, the flash would then be worn out after about 60 years).

The attacks we’re mainly concerned with for the log-signer app are rollback attacks. If an attacker can roll back to a state with a smaller tree size, the attacker can mount a split-view attack on previosuly signed larger sizes. We’re out of luck if an attacker gains physical access and the ability to make backups and restores of the underlying flash (since we don’t have any other persistent counters or the like). But we do want to protect against complete erase, e.g., if a later TKey revision provides a way to “factory reset” a TKey, erasing all app storage areas.

At startup, the app examines record 0, which is used as a special header. If that is all ones, the app initializes the storage. It generates a random 32-byte nonce, using the builtin randomness generator. Like for the sign-if-logged app, the CDI provided by the firmware is used as a HMAC-SHA256 key. The signing key is derived from the HMAC digest of a message that includes this random nonce. The nonce and the public key (32 bytes each) are written as record zero.

If the first record is not all ones, the nonce is used to re-derive the signing key, and corresponding public key is compared to the one stored in record one. If they don’t match, the app crashes, and it can be recovered only by restarting it and immediately issuing a wipe-all command.

Since the first record should not be erased unless state is completely wiped, the next 63 records are not usable, and tree state is stored starting at record 64.

Each record consists of a tree head (40 bytes), an HMAC-SHA256 digest, truncated to 16 bytes, and computed on a message including the tree head, and finally an 8 byte state word. A record is unused if the state word is all ones, obsolete if the state word is all zeros, and valid if the state word contains the right magic value, and the HMAC digest is valid.

Records are used in a round-robin fashion, with blocks of 64 records erased at a time as we wrap around. This scheme is intended to minimize the number of erase operations. At startup, we have to read all records to identify the valid record with the largest tree size. When writing a new state, we use the next record in cyclic order, and after the new record is written, we zero out the MAC digest and the state word of the previous record.

The wipe-all operation returns the storage area to the firmware, which will erase it, including the header with the nonce. Recreating the signing key after wipe thus needs both the previous contents of the header record on the flash, and either access to the TKey device, or its secrets.

Future work

We have been thinking about witness signer design for quite some time, but we don’t yet have any prototype. Its operation would be rather similar to the log signer app. Each record would need to identify the log, e.g., by hash of origin line. For Sigsum logs, the witness signer app can be responsible for verifying the log’s signature, and check that the log’s public key matches the key hash that is part of the origin line. For the origin lines of other logs, either the host would have to be responsible for maintaining the mapping between origin lines and keys, or we would need to introduce a certificate authority for this, and configure the app with the CA public key.

Signing performance is more of an issue for a witness than for a log, since a witness becomes more useful if it can handle a large number of logs and make a fair number of cosignatures per second (e.g., subscribing to the https://staging.witness-network.org/log-list-10qps-4klogs.1 list of logs, which is rather challenging on the TKey hardware).

Adding support for RSA keys in the sign-if-logged app would be helpful for handling Secure Boot keys. This is possible in principle. The main difficulties are that RSA key generation is complex and time consuming, and that Nettle’s RSA implementation is rather old style and written using GMP functions that rely on dynamic memory allocation for temporaries.

On the hardware side, a related Tillitis HSM device is in the works, which allows running arbitrary applications in the same way as the TKey, with much better performance. This could make a higher performance witness signer, as well as RSA signing, more practical.