TEE-Rust: A library for enhancing TEEs with quantum cryptography

Fabrizio Genovese · March 3, 2025

If you have been following this blog, by now you know that we devised a couple of ways to use quantum techniques to “fortify” TEEs and make them capable of running one-time programs in a secure way. In one post, we described how quantum resources could be used to protect TEEs against re-entry attacks. In another post, we leveraged garbled circuits and the same techniques to protect TEEs when their root of trust is stolen.

The quantum techniques used in the posts were QRACs and conjugate coding, which are different — but similar — methods to encode two classical bits(trings) into a sequence of qubits, in such a way that only one bit(string) can be recovered.

Rather quickly, we resolved to implement all this into a tiny library which we called TEE-Rust. Needless to say, this library is written in Rust; it does not make use of the standard library, so it can run on embedded systems. In this respect, we provide several examples that are meant to be run on a ESP32c6 microchip, which provides TEE capabilities such as encrypted flash and ram memories and secure bootloader albeit being rather inexpensive.

TEE-Rust is versatile enough to be used to implement both the one-time program protocols highlighted in the re-entry protection and garbled circuits posts. Here, we will give you a conceptual overview of how the library works.

TEE-Rust uses conjugate coding as the main underlying quantum technique. The reason to prefer conjugate coding over QRACs stems from the fact that the former is easier to implement and requires a less specialized quantum setup to be carried out. Before going forward, we give a bite-sized summary of how conjugate coding works.

Recap: Conjugate coding

This whole section is a summary of the contents covered in this post. Feel free to skip it if you already read the post.

The main underlying fact ensuring that conjugate coding does its job is that measurements in the quantum world yield probabilistic results, and change the system being measured in a non-reversible way.

We recall that a measurement of a qubit is specified by choosing a given axis on the Bloch sphere, for instance the $\ket{0}$–$\ket{1}$ basis. Measuring makes a state $\ket{\psi}$ collapse to either the state $\ket{0}$ or the state $\ket{1}$ with a certain probability, which is proportional to how close our state $\ket{\psi}$ was to either of the two states. This is known as the Born rule.

We now fix two measurement axes that are orthogonal with respect to each other, as depicted below:

Orthogonal states are unbiased with respect to each other.

Notice how the state $\ket{0}$ is completely unbiased with respect to the $\ket{+}$–$\ket{-}$ basis: Measuring along this axis would produce outcomes $\ket{+}$ or $\ket{-}$ with equal probability (50%). On the other hand, measurimg the same state $\ket{0}$ along the $\ket{0}$–$\ket{1}$ axis would return $\ket{0}$ with certainty. A similar argument holds for the states $\ket{1}, \ket{+}, \ket{-}$. Finally, when measuring any state $\ket{\psi}$, said state collapses to whatever outcome we get in a non-recoverable way. This guarantees that whatever measurement we perform will destroy the original information.

Conjugate coding uses these facts to pack two different classical bits into a couple of qubits in such a way that only one of the bits can be recovered.

  • For the first classical bit, we encode $0$ in the state $\ket{0}$, $1$ in the state $\ket{1}$;
  • For the second classical bit, we encode $0$ in the state $\ket{+}$, $1$ in the state $\ket{-}$.

From this we have two quantum bits, $q_0, q_1$, each encoding one of the two classical bits. We know that we can recover the first bit with certainty by measuring $q_0$ in the $\ket{0}–\ket{1}$ basis, whereas $q_1$ must be measured in the $\ket{+}–\ket{-}$ basis. We also know that if we measure $q_i$ in the wrong basis we just get noise.

We now concatenate $q_0$ and $q_1$ randomly in a sequence that can either be $q_0 q_1$ or $q_1 q_0$. Without knowing in which order the qubits were concatenated, it is impossible to recover both bits at the same time:

  • If we measure along the $\ket{0}$–$\ket{1}$ basis, we obtain a sequence that certainly contains the value of the first bit, plus another bit of pure noise;
  • If we measure along the $\ket{+}$–$\ket{-}$ basis, we obtain a sequence that certainly contains the value of the second bit, plus another bit of pure noise;
  • If we measure the first qubit along a basis and the second along another, we may either obtain both bits with certainty, or a purely random string.

…The point is that it is impossible to establish in which of these scenarios we end up being without knowing the concatenation order. So no information can be extracted without knowing it. TEE-Rust will hold this information securely inside the TEE.

Preparation phase

Back to TEE-Rust: All the quantum protocols in our scope can be divided into three distinct phases: A preparation phase where the sending party feeds secret information to the TEE. The same party, then, prepares the quantum states and sends them over. This is followed by a measurement phase, where the party operating the TEE decides to execute the one-time program. To do so, the party measures the received qubits in a way that depends on the program input, and feeds the measurement outcomes to the TEE. This is followed by a result phase where TEE-Rust cross-checks the information between the previous phases and decides if to execute the program or not.

The data pertaining to the preparation phase is held in a struct having the following signature:

pub struct ConjugateCodingPrepare {
    secret_size:       usize,
    security_size:     usize,
    pub total_size:    usize,
    orderings:     SecretBox<Vec<u8>>,
    bitmask:       SecretBox<Vec<u8>>,
    security0:     SecretBox<Vec<u8>>,
    security1:     SecretBox<Vec<u8>>,
}

All the fields of the struct are either integers or vectors of bytes — the SecretBox type is just a wrapper for the Vec type which we use for cryptographic hardening.

In layman terms, this structure does is encoding a (possibly incomplete) table. To see this, notice that, first of all:

  • securitySize represents the number of bytes that the TEE will know as in the re-entry protection post protocol;
  • secretSize represents the number of bytes that the TEE will never have access to as in the garbled circuit post protocol;
  • totalSize is required to equal secretSize+securitySize;
  • orderings and bitmask are required to have length totalSize;
  • bitmask is required to contain precisely $8\cdot$ secretSize $0$s and $8\cdot$securitySize $1$s;
  • security0 and security1 are required to have length securitySize.

All this means the following: bitmask says in which position the security bits are located. Looking at the lengths of the vectors from the requirements above, security0, security1 and bitmask allow us to build a table like in the following example:

An example of table encoded by ConjugateCodingPrepare.

This table will be used at measurement time to check if the measured bits pass the security verification. Again, what we will measure is a sequence of qubits that is double the length of the bitmask vector. Every couple of qubits $q_0q_1$ will encode a column of the table above, but — as conjugate coding prescribes — we do not know which qubit encodes which row. This information is contained in the orderings vector: A $0$ in the vector means that $q_0$ encodes the corresponding bit in the first row, whereas a $1$ means that $q_0$ encodes the corresponding bit in the second row. Factoring this information in and iterating on the previous example, we get:

An example of table encoded by ConjugateCodingPrepare, with orderings marked.

Where we annotated each table cell with the qubit that will encode that value. As you can see, some cells are empty. This means that the TEE does not have access to those values. This is the case for the garbled circuit post, and it is what makes the TEE resistant against stealing the root of trust. In the case of our re-entry protection post instead, we do not need the empty cells, and the library can be instantiated with securitySize set to $0$ and bitmask containing only $1$s.

Further technical details

We implement the following functions for this struct:

  • new instantiates the struct. It returns a Result type as it checks that all the requirements around size and composition of the bitstring vector are met;
  • default instantiates the struct setting everything to $0$ (the sizes, the length of the vectors, etc.);
  • from_plaintext works like new, but accepts Vec<u8> as inputs instead of the more verbose SecretBox<Vec<u8>>. It is basically a wrapper around new;
  • We also implement a diagnostics function which works only when the debug feature is present, and that prints the content of the struct on terminal. This is obviously a big no-no when cryptographic security is concerned, which is why we only enable this function in debug mode.

As for traits, we implement:

  • Format and Debug for pretty printing and displaying reasons;
  • Zeroize and ZeroizeOnDrop to enhance security: These traits make it so as soon as a struct is not needed anymore and is dropped, its location in memory gets overwritten with $0$s.

A word of caution: The information in the preparation phase is very sensible, and must be communicated securely to the TEE using encryption. The encryption wrappers are outside of the scope of the library, but we provide a toy example around this in the library repo.

Measurement phase

In the measurement phase, the TEE acquires the results of the qubits measurements and the choiche of basis for each measurement. This information is held in the following struct:

pub struct ConjugateCodingMeasure {
    outcomes: SecretBox<Vec<u8>>,
    choices: SecretBox<Vec<u8>>,
}

Here, choices is required to have length totalSize, whereas outcomes, denoting the measurement outcomes, is required to have length $2\cdot$total_size — as we know, in conjugate coding we need two qubits for each couple of classical bits we want to encode. Going back to our running example, this allows us to enrich our table with two more entries:

The table above enriched with the information stored in ConjugateCodingMeasure.

Here, we have grouped the outcomes vector in chunks of two bits: This makes sense, as each of these couples of bits is used to encode the corresponding column of bitstring $0$ and $1$. Clearly, in measuring each couple of qubits with the same choice of basis, we also know that one qubit in each couple will be noise, whereas the other one will contain useful information.

Moreover, the choices vector, dictating the choice of basis for measuring each couple, tells us which of the two bitstrings we wanted to measure: Here, $0$ means that we wanted to measure the corresponding bit of bitstring $0$, whereas $1$ means we wanted to measure the corresponding bit of bitstring $1$. We reflected this information in the table by graying out the cells that we did not choose to measure.

Technical details

The functions implemented for this struct are as in the ConjugateCodingPrepare case:

  • new instantiates the struct. It returns a Result type as it checks that all the requirements around size and composition of the bitstring vector are met;
  • default instantiates the struct setting everything to $0$;
  • from_plaintext is the usual wrapper around new;
  • As before, we also implement diagnostics, available when the debug crate feature is on.

As for traits, we implement again Format, Debug, Zeroize and ZeroizeOnDrop.

Notice: Contrary to the preparation phase, the information around measurement does not need any encryption as it is provided by the same party operating the TEE.

Result phase

In the result phase, the data acquired in the preparation and measurement phases is used to verify the security of the protocol and compute the secret. The relevant information is stored in the following struct:

pub struct ConjugateCodingResult {
    purged: SecretBox<Vec<u8>>,
    secret: SecretBox<Vec<u8>>,
}

The secret vector has length secret_size and, quite obviously, contains the secret. On the other hand, the purged vector equals the outcomes vector purged from the noisy bits. As usual, let’s refer to our running example by enriching the table further:

The running example table enriched with the purged and secret vectors, computed from the previous information.

First of all, let’s go over how purged is computed. For each couple of bits in outcomes, we look at the corresponding bits in choices and orderings, and proceed by cases:

  • If orderings is $0$, we know that the first bit of the couple encodes the corresponding bit of the first bitstring, whereas the second bit of the couple encodes the second bitstring;
    • Then, if choices is $0$, we keep the first bit of the couple and throw away the second;
    • If choices is $1$, we keep the second bit and throw away the first;
  • If orderings is $1$ things are reversed: the first bit of the couple encodes the corresponding bit of the second bitstring, whereas the second bit of the couple encodes the first bitstring;
    • It follows that if choices is $0$, we keep the second bit of the couple and throw away the first;
    • If choices is $1$, we keep the first bit bit and throw away the second.

A more mathematically concise way to say this is that we look at orderings $\texttt{XOR}$ choices: If this evaluates to $0$ we keep the first bit, otherwise we keep the second.

Now that we have the purged vector, we write each of its bits in the corresponding non-greyed out cell of the bitstrings. In the cells that are marked as security bits, we now have two numbers. We have to check that these two numbers are equal: If they are, then the security test passes. If they are not, it fails. For instance, in the example above we see that the security test passes everywhere with the exception of the last cell of bitstring $0$, where the TEE was expecting to see a $1$ but instead got fed a $0$.

If the test passes, we compute the final secret by just taking the purged vector and getting rid of all the bits in the positions marked with $1$s in the bitmask: This effectively returns the portion of the bitstring that the TEE did not have access to, which has indeed length secret_size.

Notice that in the case of our first post, secret_size would be $0$, and so the protocol would not return any secret. This is makes sense as the protocol uses the quantum information only to decide if the program must be executed or not.

In the case of our second post, we would have a mixture of secret and security bits: The security bits are required to avoid replaying the protocol, but the only bits we need to open the garbled circuit are the secret ones, which are the ones that the library returns. Notice how, in this case, there are cells in the table that remain empty: This information not only is not available by the TEE, but it also got permanently destroyed at measurement time. This is precisely what protects the TEE against stealing the root of trust.

Technical details

The traits we implement for this struct are the usual Format, Debug, Zeroize and ZeroizeOnDrop. Aside from the diagnostics function available in debug mode, the only public function we implement here has the following signature:

pub fn new(
    preparation: &ConjugateCodingPrepare,
    measurement: &ConjugateCodingMeasure,
    error:       usize
) -> Result<ConjugateCodingResult, ConjugateCodingResultError>

To use this, we pass ownership of the structs referring to the previous phases, and these in turn are used to build the final struct. Again, the function returns an erorr in case the security verification fails. An important thing to notice is the presence of an error factor: This accounts for the fact that quantum communication protocols are lossy. Going back to the running example above, error specifies how many discrepancies between security and measured bits are we willing to tolerate. Setting error to $0$ in this case would return a verification failed error, whereas setting it to $1$ would let us pass the test.

The right error parameter needs to be calibrated depending on the quantum hardware being used. In general, for the garbled circuit post protocol the presence of losses complicates things further, as the secret must itself contain some form of error correction to make sure that the original bitstring is recovered also in lossy frameworks.

Conclusion

As already said in the beginning of the post, TEE-Rust can be found here and is free to play with. Besides providing extensive examples, we also provide an Arduino sketch — to be found here — which simulates the measurement outcomes of a typical QKD setup, with losses etc. You can use this to play with TEE-Rust without having to spend several Kilodollars of QKD equipment. Notice: The Arduino sketch library is not yet open at the time of this publication. It will be opened soon tho, and we include the link anyway for future reference.

That’s all from us for now, we hope that playing with this new toy will be at least as fun as it was implementing it!

Twitter, Facebook

Comments