Explaining Yao's Garbled Circuits

The protocol so fun you have to implement it! Like I did recently.

Yao’s Garbled Circuits is a Cryptographic scheme that allows two parties with secret inputs to evaluate an arbitrary function on those inputs, without revealing them to each other. As far as I can tell, the protocol was first described orally by Andrew Yao in 1986, but the first written description was in the subsequent How to Play Any Mental Game paper, by Goldreich, Micali, and Wigderson. But, I’m not an academic historian, so take this with a grain of salt, and feel free to correct me on Twitter if you know better.

I first heard about this scheme last summer, and like so many things in Cryptography, it seemed quite mysterious to me at the time. And just like so many of those things, it turned out to be a lot simpler than I expected; hopefully this post can impart a bit of that feeling to you as well.

MPC in a Nutshell

Garbled Circuits are a special case of a more general idea called (secure) Multi-Party Computation (MPC).

The premise is that you have a group of parties P1,,PnP_1, \ldots, P_n. Each party PiP_i has their own secret input xix_i. The parties want to compute some function f(x1,,xn)f(x_1, \ldots, x_n) on their inputs, learning the result yy.

1.png

The simplest way to do this would be for the parties to share their inputs with each other. Each party would know all of the inputs x1,,xnx_1, \ldots, x_n, and thus be able to compute y=f(x1,,xn)y = f(x_1, \ldots, x_n) on their own.

The problem with this naive approach is that each party would like to keep their input xix_i secret. The difficulty in MPC is not in computing the function ff among multiple parties, but rather in keeping all of the inputs hidden while performing that computation.

Example: The Millionaire Problem

One of the classic examples of a situation where MPC is useful is Yao’s Millionaire Problem.

The premise is that several millionaires want to know which of them is the richest. They could do this by revealing their exact wealth to each other, but they’d like to each keep that amount secret. In other words, they’d like to learn who has the most money, without revealing the amount of money that person, or any other member of the group has.

This is a problem which can be solved through an MPC protocol. The inputs x1,,xnx_1, \ldots, x_n will be set to the wealth of each party, and the function ff to compute will be:

f(x1,,xn):=arg maxixi f(x_1, \ldots, x_n) := \argmax_{i} x_i

which returns the index jj of the largest amount xjx_j.

An MPC protocol for ff would let the millionaires collaborate by exchanging messages, eventually learning the result f(x1,,xn)f(x_1, \ldots, x_n), but no other information about the inputs.

This is a somewhat artificial example, but there are plenty of more realistic applications of MPC. Another example people like to give is that of the Danish Sugar Beet Auction, where MPC was used to organize an auction for sugar beet production contracts without revealing individual bids.

Another interesting application of MPC is allowing companies to collaborate to train Machine Learning models on private data, without sharing that data with each other. For example, several hospitals could collaborate to train a model for identifying cancer, without having sensitive health information leave the individual hospitals.

Our Specific Setting

So far we’ve seen MPC in the case with an arbitrary number of parties, computing some arbitrary function ff. Garbled Circuits is a technique for doing MPC when there are only two parties. We also assume that the participants are semi-honest. They might be curious about what the input of the other party is, but they won’t cheat by misbehaving, and deviating from what the protocol asks them to do.

Boolean Circuits

This technique nonetheless works with an arbitrary function ff, although we need to assume a more concrete representation for that function. We assume that ff can be represented as a boolean circuit. You can think of this circuit as a bunch of wires connected to boolean gates like &,\&, \oplus, etc. A more formal definition would model this circuit as a graph. The nodes would be the inputs or gates of the circuit, and the edges would be the wires connecting gates together.

2.png

I think the most useful representation for this post is related to the idea of graphs, but not exactly the same. I like to think of a circuit as a collection of labeled wires. Each wire either comes from an input value, or from the output of another gate, like in this example:

3.png

You can also model this idea as a little programming language, involving variables, reading input, and boolean operators:

let a = input(0)
let b = input(1)
let c = a & b
let d = c | b
return (c, d)

But models are less important than what you do with them, so let’s mosey on and have a look at that.

Walking on Wires

The Garbled Circuits protocol is asymmetric: each of the two parties does something different. One of the parties is what I’ll call the garbler: their job is to obfuscate the circuit and the input values, and hand that mess over to the other party. I’ll call this other party the evaluator: their job is to take this garbled circuit, and evaluate it, producing the final result. They then share this result with the garbler, and everybody is happy.

A single gate

To illustrate the essentials, let’s consider the case where the parties have secret bits x0x_0 and x1x_1, respectively, and want to compute a boolean function f(x0,x1)f(x_0, x_1) on these inputs.

We can think of this function as a lookup table:

F:=f(0,0)f(0,1)f(1,0)f(1,1) F := \begin{vmatrix} f(0, 0) & f(0, 1) \cr f(1, 0) & f(1, 1) \cr \end{vmatrix}

Now, if you had the inputs x0x_0 and x1x_1, then you could evaluate f(x0,x1)f(x_0, x_1) by simply looking up the entry F[x0,x1]F[x_0, x_1]. Of course, each party only has one of the inputs, so this doesn’t work.

Now, this next idea is a bit of a reach, so bear with me. The garbler will generate 4 encryption keys k0a,k1a,k0b,k1bk^a_0, k^a_1, k^b_0, k^b_1, one for each wire, and each of the two possible values that wire can have. Then, they create an encrypted version of the table FF:

F:=Enc((k0a,k0b),f(0,0))Enc((k0a,k1b),f(0,1))Enc((k1a,k0b),f(1,0))Enc((k1a,k1b),f(1,1)) F' := \begin{vmatrix} \text{Enc}((k^a_0, k^b_0), f(0, 0)) & \text{Enc}((k^a_0, k^b_1), f(0, 1)) \cr \text{Enc}((k^a_1, k^b_0), f(1, 0)) & \text{Enc}((k^a_1, k^b_1), f(1, 1)) \cr \end{vmatrix}

Now, if you were to magically have the keys kx0ak^a_{x_0} and kx1bk^b_{x_1}, then those keys wouldn’t tell you any information about x0x_0 and x1x_1, since they look as random as any other pair of keys, but they would let you evaluate f(x0,x1)f(x_0, x_1), using FF'.

You would use your pair of keys to try and decrypt each entry. Eventually, you would hit the entry encrypted with kx0ak^a_{x_0} and kx1bk^b_{x_1}, and would thus read out f(x0,x1)f(x_0, x_1).

Now, one problem is that since the layout of FF' is the same as that of FF, we know which input we’ve decrypted simply by observing where our decryption is successful. To get around this, we can simply shuffle the table around. The easiest way to do this is to randomly swap both rows, and randomly swap both columns.

In order to avoid having to try and decrypt all 4 entries, we can also attach a pointer bit to our keys, which indicates where we should look. For example, we might receive (ka,1)(k^a, 1), and (kb,0)(k^b, 0), which tells us to look at the entry F[1,0]F[1, 0] to use these keys. But, we don’t know that (ka,1)(k^a, 1) actually corresponds to (k1a,1)(k^a_1, 1), because it’s possible that we decided to flip the two rows in the table, and we actually have (k0a,1)(k^a_0, 1).

4.png

Thus, if we can somehow deliver the garbled table FF' along with kx0ak^a_{x_0} and kx1bk^b_{x_1} to the evaluator, then they’ll be able to tell us the result, all without them learning the garbler’s input x0x_0.

Now, preparing and sending the table FF' isn’t very difficult.

The same goes for the garbler’s input key kx0ak^a_{x_0}. All they have to do is just send it along. k0ak^a_0 isn’t any more recognizable than k1ak^a_1, and our shuffling and pointer bit flipping help make that the case.

Now, for kx1bk^b_{x_1}, we’re in a bit of a pickle. The garbler knows k0bk^b_0 and k1bk^b_1, and the evaluator knows x1x_1. We’d like for the evaluator to somehow learn kx1bk^b_{x_1}, without learning the other key, and without the garbler learning x1x_1. It seems we’re at a bit of an impasse, at least without a neat little tool.

Detour: Oblivious Transfer

The tool we need is something called Oblivious Transfer. The idea is that a sender has two messages m0m_0 and m1m_1, the receiver has a bit bb, and the two run an Oblivious Transfer protocol so that:

Like many protocols in Cryptography, you can think of this ideal protocol as a magical box with a trusted third party, such as a friendly gnome, inside:

5.png

For our purposes, all you really need to know is that you can instantiate this protocol without any magic. The details don’t really matter for Garbled Circuits. If you’re curious, I’d recommend taking a gander at The Simplest Protocol for Oblivious Transfer, which is one way, among many, of instantiating this construction.

Wrapping up the simple case

Now, notice that we can directly use the Oblivious Transfer protocol for our previous dilemma. The garbler has k0bk^b_{0} and k1bk^b_1 as their two messages, and the evaluator has x1x_1, and would like to end up with just kx1bk^b_{x_1}. This is the exact setup Oblivious Transfer was designed to solve.

Let’s recap our scheme so far. To evaluate f(x0,x1)f(x_0, x_1), with the garbler having x0x_0, and the evaluator having x1x_1, first the garbler generates random keys k0a,k1a,k0b,k1bk^a_0, k^a_1, k^b_0, k^b_1 (including their corresponding pointer bits), along with the encrypted table:

F:=Enc((k0a,k0b),f(0,0))Enc((k0a,k1b),f(0,1))Enc((k1a,k0b),f(1,0))Enc((k1a,k1b),f(1,1)) F' := \begin{vmatrix} \text{Enc}((k^a_0, k^b_0), f(0, 0)) & \text{Enc}((k^a_0, k^b_1), f(0, 1)) \cr \text{Enc}((k^a_1, k^b_0), f(1, 0)) & \text{Enc}((k^a_1, k^b_1), f(1, 1)) \cr \end{vmatrix}

They then randomly flip the rows and columns of this table, and also the corresponding pointer bits of the keys.

Then, for kak^a, they simply send kx0ak^a_{x_0} to the evaluator. For kbk^b, they run an Oblivious Transfer using k0bk^b_{0} and k1bk^b_1, with the evaluator receiving kx1bk^b_{x_1}.

Using kx0ak^a_{x_0} and kx1bk^b_{x_1}, the evaluator can decrypt the entry for f(x0,x1)f(x_0, x_1), and then send the result back to the garbler.

Everything’s Connected

Now, this protocol only works with a function {0,1}2{0,1}\{0, 1\}^2 \to \{0, 1\}; in other words, a single gate. Our circuit is likely to have more than one gate. To support this, we need a way to chain multiple gate evaluations together.

Notice that the output of our function didn’t really matter in the single gate case. Sure, we said it was just a boolean in {0,1}\{0, 1\}, but it could have easily been an integer, or a string. As long as we can encrypt it, it doesn’t really matter.

So, we could have the output of the function be another key, which would then be used as the input to a different gate.

To illustrate, let’s say we have two input wires aa and bb, connected to some gate ff, producing an output wire cc:

6.png

Like with each of the input wires, the output wire cc has two keys k0ck^c_0 and k1ck^c_1 associated with it. Instead of thinking of our gate ff as producing a boolean output, we can instead have it produce one of these keys as its output, giving us the following lookup table:

F:=kf(0,0)ckf(0,1)ckf(1,0)ckf(1,1)c F := \begin{vmatrix} k^c_{f(0, 0)} & k^c_{f(0, 1)} \cr k^c_{f(1, 0)} & k^c_{f(1, 1)} \cr \end{vmatrix}

And then we just shove this lookup table into the previous setup we had, and everything should just work. Given the correct inputs kx0ak^a_{x_0} and kx1bk^b_{x_1}, the evaluator learns kf(x0,x1)ck^c_{f(x_0, x_1)}, which they can then use as an input to the next gate they need to evaluate.

Bird’s Eye View

We’ve seen all of the individual pieces, so let’s take a little step back and look at how everything fits together. Once again, I think looking at our circuit as a collection of wires makes the most sense.

For every wire ww, there will be two keys, k0wk^w_0 and k1wk^w_1, for each of the two possible values that wire can take. These keys also contain a random pointer bit (with k1wk^w_1 having the opposite of k0wk^w_0) which tells us which part of the encrypted lookup table to use when using that key.

A wire obtains its value in one of only two ways:

  1. It’s connected to one of the inputs
  2. It’s connected to the output of a gate

For the first case, we have two ways of getting the key to the evaluator. If the input xx for that wire ww belongs to the garbler, then they can just send kxwk^w_x to the evaluator. If the input belongs to the evaluator, then an Oblivious Transfer needs to be conducted, using k0wk^w_0, k1wk^w_1 as messages, along with the evaluator’s bit xx.

In the second case, where the wire is the output of some gate, the evaluator will have received the keys for the two inputs to that gate, and can use the encrypted table to look the resulting output key up.

Finally, after going through the circuit, the evaluator will end up with a set of output keys, corresponding to the output wires of the circuit.

To turn these keys into a concrete result, we can also use an encrypted table, albeit with just two entries:

O:=Enc(k0w,0)Enc(k1w,1) O := \begin{vmatrix} \text{Enc}(k^w_0, 0)\cr \text{Enc}(k^w_1, 1) \end{vmatrix}

(shuffled appropriately, based on the pointer bit, of course).

To summarize, the garbler generates all of the encryption keys for each wire, and then sends the correct input keys to the evaluator, either by choosing them directly, if the garbler has the input bit for that key, or by running an Oblivious Transfer with the evaluator, if they’re the one with the input bit. The garbler then sends along the encrypted tables for each gate, along with the encrypted output tables. The evaluator can then traverse the circuit, using their encryption keys to get the key contained in each gate table, and then eventually the outputs contained in the output tables. They then send the output back to the garbler, and everyone is happy.

Conclusion

I found this scheme so compelling when I first read about it a month or so ago that I had to implement it; it was only natural that I’d end up writing about it too.

There are probably 100 explanations of Yao’s Garbled Circuits out there, given that it’s one of the earliest MPC schemes, and spawned an entire paradigm of protocols. Another explanation I like is in the Pragmatic MPC book, which has a lot of great material beyond just this scheme.

In a further post, I might explain the nitty gritty details of how my implementation works, although it’s nothing fancy.