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:
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 equalsecretSize
+securitySize
;orderings
andbitmask
are required to have lengthtotalSize
;bitmask
is required to contain precisely $8\cdot$secretSize
$0$s and $8\cdot$securitySize
$1$s;security0
andsecurity1
are required to have lengthsecuritySize
.
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:
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:
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 aResult
type as it checks that all the requirements around size and composition of thebitstring
vector are met;default
instantiates the struct setting everything to $0$ (the sizes, the length of the vectors, etc.);from_plaintext
works likenew
, but acceptsVec<u8>
as inputs instead of the more verboseSecretBox<Vec<u8>>
. It is basically a wrapper aroundnew
;- We also implement a
diagnostics
function which works only when thedebug
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
andDebug
for pretty printing and displaying reasons;Zeroize
andZeroizeOnDrop
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:
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 aResult
type as it checks that all the requirements around size and composition of thebitstring
vector are met;default
instantiates the struct setting everything to $0$;from_plaintext
is the usual wrapper aroundnew
;- As before, we also implement
diagnostics
, available when thedebug
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:
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;
- Then, if
- 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.
- It follows that if
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!