From b167fade22b71e91b7a9d69f5cc61b36dac4ab3f Mon Sep 17 00:00:00 2001 From: Brian Lawrence Date: Mon, 10 Jun 2024 12:04:50 -0700 Subject: [PATCH] FHE stuff draft --- easy/main.typ | 7 ++ easy/src/fhe0.typ | 59 +++++++++++++ easy/src/fhe2.typ | 172 ++++++++++++++++++++++++++++++++++++++ easy/src/fhe3.typ | 185 +++++++++++++++++++++++++++++++++++++++++ easy/src/lwe.typ | 188 ++++++++++++++++++++++++++++++++++++++++++ easy/src/preamble.typ | 2 + 6 files changed, 613 insertions(+) create mode 100644 easy/src/fhe0.typ create mode 100644 easy/src/fhe2.typ create mode 100644 easy/src/fhe3.typ create mode 100644 easy/src/lwe.typ diff --git a/easy/main.typ b/easy/main.typ index 9ff36a2..95c8cd4 100644 --- a/easy/main.typ +++ b/easy/main.typ @@ -33,3 +33,10 @@ #chapter("src/kzg.typ") #chapter("src/plonk.typ") +#part[Levelled fully homomorphic encryption] +#chapter("src/fhe0.typ") +#chapter("src/lwe.typ") +#chapter("src/fhe2.typ") +#chapter("src/fhe3.typ") + + diff --git a/easy/src/fhe0.typ b/easy/src/fhe0.typ new file mode 100644 index 0000000..0651da6 --- /dev/null +++ b/easy/src/fhe0.typ @@ -0,0 +1,59 @@ +#import "preamble.typ":* + += Introduction to fully homomorphic encryption + + +Fully homomorphic encryption (FHE) lets you encrypt a message, and then +other people can perform arbitrary operations on the encrypted message +without being able to read the message. + +Levelled FHE is a sort of weaker version of FHE. Like FHE, levelled FHE +lets you perform operations on encrypted data. But unlike FHE, there +will be a limit on the number of operations you can perform before the +data must be decrypted. + +Loosely speaking, the encryption procedure will involve some sort of +"noise" or "error." As long as the error is not too big, the message can +be decoded without trouble. But each operation on the encrypted data +will cause the error to grow – and if it grows beyond some maximum error +tolerance, the message will be lost. So there is a limit on how many +operations you can do before the error gets too big. + +As a sort of silly example, imagine your message is a whole number +between 0 and 10 (so it’s one of 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10), and +your "encryption" scheme encodes the message as a real number that is +very close to the message. So if the cyphertext is 1.999832, well then +that means the original message was 2. The decryption procedure is +"round to the nearest integer." + +(You might be thinking: This is some pretty terrible cryptography, +because the message isn’t secure. Anyone can figure out how to round a +number, no secret key required. Yep, you’re right. The actual encryption +#link("https://hackmd.io/mQB8_nWPTm-Kyua7QgNLNw")[scheme] is more +complicated. But it still has this "rounding-off-errors" feature, and +that’s what I want to focus on right now.) + +Now imagine that the "operations" you want to perform are addition. (If +you like, imagine doing the addition modulo 11, so if a number gets too +big, it "wraps around.") Well, every time you add two encrypted numbers +($1.999832 + 2.999701 = 4.999533$), the errors add as well. After too +many operations, the error will exceed $0.5$, and the rounding procedure +won’t give the right answer anymore. + +But as long as you’re careful not to go over the error limit, you can +add cyphertexts with confidence. + +In fact, for our levelled FHE protocol, our message will be a bit: +either 0 or 1; +our operations will be the logic gates AND and NOT. +Any logic circuit can be built out of AND and NOT gates, +so we'll be able to perform arbitrary calculations +within the FHE encryption. + +Our protocol uses a cryptosystem built +from a problem called "learning with errors." +"Learning with errors" is kind of a strange name; +I'd call it "approximate linear algebra modulo $q$." +Anyway, we'll start with the learning-with-errors problem +(@lwe) and how to build cryptography on top of it (@lwe-crypto) +before we get back to levelled FHE. diff --git a/easy/src/fhe2.typ b/easy/src/fhe2.typ new file mode 100644 index 0000000..2bdd41c --- /dev/null +++ b/easy/src/fhe2.typ @@ -0,0 +1,172 @@ +#import "preamble.typ":* + += Public-Key Cryptography from Learning with Errors + +The +learning with errors +problem (@lwe) is one of those "hard problems that you can build cryptography +on." The problem is to solve for constants +$ a_1, dots, a_n in ZZ / (q ZZ), $ given a bunch of +#emph[approximate] equations of the form +$ a_1 x_1 + dots.h + a_n x_n = y + epsilon.alt , $ where each +$epsilon.alt$ is a "small" error (in the linked example, $epsilon.alt$ +is either 0 or 1). + +In @lwe +we saw how even a small case of this problem ($q = 11$, $n = 4$) can be +annoyingly tricky. In the real world, you should imagine that $n$ and +$q$ are much bigger – maybe $n$ is in the range +$100 lt.eq n lt.eq 1000$, and $q$ could be anywhere from $n^2$ to +$2^(sqrt(n))$, say. + +Now let’s see how to turn this into a public-key cryptosystem. We’ll use +the same numbers from the "blue set" in @lwe. In fact, that "blue +set" will be exactly the public key. + +#figure( + align(center)[#table( + columns: 1, + align: (auto,), + table.header([Public Key],), + table.hline(), + [(1, 0, 1, 7) : 2], + [(5, 8, 4, 10) : 9], + [(7, 7, 8, 5) : 3], + [(5, 1, 10, 6) : 3], + [(8, 0, 2, 4) : 1], + [(9, 3, 0, 6) : 9], + [(0, 6, 1, 6) : 9], + [(0, 4, 9, 7) : 5], + [(10, 7, 4, 10) : 10], + [(5, 5, 10, 6) : 8], + [(10, 7, 3, 1) : 9], + [(0, 2, 5, 5) : 6], + [(9, 10, 2, 1) : 2], + [(3, 7, 2, 1) : 5], + [(2, 3, 4, 5) : 3], + [(2, 1, 6, 9) : 3], + )] + , kind: table + ) + +The private key is simply the vector $a$. + +#figure( + align(center)[#table( + columns: 1, + align: (auto,), + table.header([Private Key],), + table.hline(), + [$upright(bold(a))$ = (10, 8, 10, 10)], + )] + , kind: table + ) + +== How to encrypt $mu$? + +Suppose you have a message $m in { 0 , 5 }$. (You’ll see in a moment why +we insist that $mu$ is one of these two values.) The cyphertext to +encrypt $m$ will be a pair $(upright(bold(x)) : y)$, where $x$ is a +vector, $y$ is a scalar, and +$upright(bold(x)) dot.op upright(bold(a)) + epsilon.alt = y + mu$, where +$epsilon.alt$ is "small". + +How to do the encryption? If you’re trying to encrypt, you only have +access to the public key – that list of pairs $(upright(bold(x)) : y)$ +above. You want to make up your own $upright(bold(x))$, for which you +know approximately the value $upright(bold(x)) dot.op upright(bold(a))$. +You could just take one of the vectors $upright(bold(x))$ from the +table, but that wouldn’t be very secure: if I see your cyphertext, I can +find that $upright(bold(x))$ in the table and use it to decrypt $mu$. + +Instead, you are going to combine several rows of the table to get your +vector $upright(bold(x))$. Now you have to be careful: when you combine +rows of the table, the errors will add up. We’re guaranteed that each +row of the table has $epsilon.alt$ either $0$ or $1$. So if you add at +most $4$ rows, then the total $epsilon.alt$ will be at most $4$. Since +$mu$ is either $0$ or $5$ (and we’re working modulo $q = 11$), that’s +just enough to determine $mu$ uniquely. + +So, here’s the method. You choose at random 4 (or fewer) rows of the +table, and add them up to get a pair $(upright(bold(x)) : y_0)$ with +$upright(bold(x)) dot.op upright(bold(a)) approx y_0$. Then you take +$y = y_0 - mu$ (mod $q = 11$ of course), and send the message +$(upright(bold(x)) : y)$. + +== An example + +Let’s say you randomly choose the first 4 rows: + +#figure( + align(center)[#table( + columns: 1, + align: (auto,), + table.header([Some rows of public key],), + table.hline(), + [(1, 0, 1, 7) : 2], + [(5, 8, 4, 10) : 9], + [(7, 7, 8, 5) : 3], + [(5, 1, 10, 6) : 3], + )] + , kind: table + ) + +Now you add them up to get the following. | $upright(bold(x)) : y_0$ | | +\- | | (7, 5, 1, 6) : 6 | + +Finally, let’s say your message is $m = 5$. So you set +$y = y_0 - m = 6 - 5 = 1$, and send the cyphertext: | +$upright(bold(x)) : y_0$ | | - | | (7, 5, 1, 6) : 1. | + +== Decryption + +Decryption is easy! The decryptor knows +$ upright(bold(x)) dot.op upright(bold(a)) + epsilon.alt = y + mu $ +where $0 lt.eq epsilon.alt lt.eq 4$. + +Plugging in $upright(bold(x))$ and $upright(bold(a))$, the decryptor +computes $ upright(bold(x)) dot.op upright(bold(a)) = 4 . $ Plugging in +$y = 1$, we see that $ 4 + epsilon.alt = 1 + mu . $ + +Now it’s a simple "rounding" problem. We know that $epsilon.alt$ is +small and positive, so $1 + mu$ is either $4$ or … a little more. (In +fact, it’s one of $4 , 5 , 6 , 7 , 8$.) On the other hand, since $mu$ is +0 or 5, well, $1 + mu$ had better be 1 or 6… so the only possibility is +that $1 + mu = 6$, and $mu = 5$. + +== How does this work in general? + +In practice, $n$ and $q$ are often much larger. Maybe $n$ is in the +hundreds, and $q$ could be anywhere from "a little bigger than $n$" to +"almost exponentially large in $n$," say $q = 2^(sqrt(n))$. In fact, to +do FHE, we’re going to want to take $q$ pretty big, so you should +imagine that $q approx 2^(sqrt(n))$. + +For security, the encryption algorithm shouldn’t just take add up 3 or 4 +rows of the public key. In fact we want the encryption algorithm to add +at least $log (q^n) = n log q$ rows – to be safe, maybe make that number +a little bigger, say $m = 2 n log q$. Of course, for this to work, the +public key has to have at least $m$ rows. + +So in practice, the public key will have $m = 2 n log q$ rows, and the +encryption algorithm will be "select some subset of the rows at random, +and add them up". + +Of course, combining $m$ rows will have the effect of multiplying the +error by $m$ – so if the initial $epsilon.alt$ was bounded by $1$, then +the error in the cyphertext will be at most $m$. But remember that $q$ +is exponentially large compared to $m$ and $n$ anyway, so a mere factor +of $m$ isn’t going to scare us! + +Now we could insist that the message is just a single bit – either $0$ +or $⌊q / 2⌋$. Or we could allow the message to be any multiple of some +constant $r$, where $r$ is bigger than the error bound (right now that’s +$m$) – which allows you to encode a message space of size $q \/ r$ +rather than just a single bit. + +When we do FHE, we’re going to apply many operations to a cyphertext, +and each is going to cause the error to grow. We’re going to have to put +some effort into keeping the error under control – and then, when the +error inevitably grows beyond the permissible bound, we’ll need a +special technique ("bootstrapping") to refresh the cyphertext and start +anew. diff --git a/easy/src/fhe3.typ b/easy/src/fhe3.typ new file mode 100644 index 0000000..96515f0 --- /dev/null +++ b/easy/src/fhe3.typ @@ -0,0 +1,185 @@ += Levelled Fully Homomorphic Encryption from Learning with Errors + + +== The main idea: Approximate eigenvalues + +If you haven’t already, this might be a good time to go back and read +about the +#link("https://notes.0xparc.org/notes/learning-with-errors-exercise")[learning with errors] +problem and how you can use it to do +#link("https://hackmd.io/mQB8_nWPTm-Kyua7QgNLNw")[public-key cryptography];. + +You should at least understand the vague idea: We’re going pick some +large integer $q$ (in practice $q$ could be anywhere from a few thousand +to $2^1000$), and do "approximate linear algebra" modulo $q$. In other +words, we’ll do linear algebra, where all our calculations are done +modulo $q$ – but we’ll also allow the calculations to have a small +"error" $epsilon.alt$, which will typically be much, much smaller than +$q$. + +Here’s the setup. Our #emph[secret key] will be a vector +\$\\mathbf{v} = (v\_1, \\ldots, v\_n) \\in (\\ZZ / q \\ZZ)^n\$ – a +vector of length $n$, where the entries are integers modulo $q$. Suppose +we want to encode a message $mu$ that’s just a single bit, let’s say +$mu in { 0 , 1 }$. Our cyphertext will be a square $n$-by$n$ matrix $C$ +such that $ C upright(bold(v)) approx mu upright(bold(v)) . $ Now if we +assume that $upright(bold(v))$ has at least one "big" entry (say $v_i$), +then decryption is easy: Just compute the $i$-th entry of +$C upright(bold(v))$, and determine whether it is closer to $0$ or to +$v_i$. + +With a bit of effort, it’s possible to make this into a public-key +cryptosystem. The main idea is to release a +#link("https://hackmd.io/mQB8_nWPTm-Kyua7QgNLNw")[table] of vectors +$upright(bold(x))$ such that +$upright(bold(x)) dot.op upright(bold(v)) approx 0$, and use that as a +public key. Given $mu$ and the public key, you can find a matrix $C_0$ +such that $C_0 upright(bold(v)) approx 0$ – then take +$C = C_0 + mu \* upright(I d)$, where $upright(I d)$ is the identity +matrix. And $C_0$ can be built row-by-row… but we won’t get into the +details here. + +Indeed homomorphic encryption is already interesting without the +public-key feature. If you assume the person encrypting the data knows +$upright(bold(v))$, it’s easy (linear algebra, again) to find $C$ such +that $C upright(bold(v)) approx mu upright(bold(v))$. + +To make homomorphic encryption work, we need to explain how to operate +on $mu$. We’ll describe three operations: addition, NOT, and +multiplication (aka AND). + +Addition is simple: Just add the matrices. If +$C_1 upright(bold(v)) approx mu_1 upright(bold(v))$ and +$C_2 upright(bold(v)) approx mu_2 upright(bold(v))$, then +$ (C_1 + C_2) upright(bold(v)) = C_1 upright(bold(v)) + C_2 upright(bold(v)) approx mu_1 upright(bold(v)) + mu_2 upright(bold(v)) = (mu_1 + mu_2) upright(bold(v)) . $ +Of course, addition on bits isn’t a great operation, because if you add +$1 + 1$, you get $2$, and $2$ isn’t a legitimate bit anymore. So we +won’t really use this. + +Negation of a bit (NOT) is equally simple, though. If $mu in { 0 , 1 }$ +is a bit, then its negation is simply $1 - mu$. And if $C$ is a +cyphertext for $mu$, then $upright(I d) - C$ is a cyphertext for +$1 - mu$, since +$ (upright(I d) - C) upright(bold(v)) = upright(bold(v)) - C upright(bold(v)) approx (1 - mu) upright(bold(v)) . $ + +Multiplication is also a good operation on bits – it’s just AND. To +multiply two bits, you just multiply (matrix multiplication) the +cyphertexts: +$ C_1 C_2 upright(bold(v)) approx C_1 (mu_2 upright(bold(v))) = mu_2 C_1 upright(bold(v)) approx mu_2 mu_1 upright(bold(v)) = mu_1 mu_2 upright(bold(v)) . $ + +(At this point you might be concerned about this symbol $approx$ and +what happens to the size of the error. That’s an important issue, and +we’ll come back to it.) + +Anyway, once you have AND and NOT, you can build arbitrary logic gates – +and this is what we mean when we say you can perform arbitrary +calculations on your encrypted bits, without ever learning what those +bits are. At the end of the calculation, you can send the resulting +cyphertexts back to be decrypted. + +== A constraint on the secret key $upright(bold(v))$ and the "Flatten" operation + +In order to make the error estimates work out, we’re going to need to +make it so that all the cyphertext matrices $C$ have "small" entries. In +fact, we will be able to make it so that all entries of $C$ are either +$0$ or $1$. + +To make this work, we will assume our secret key $upright(bold(v))$ has +the special form +$ upright(bold(v)) = (a_1 , 2 a_1 , 4 a_1 , dots.h , 2^k a_1 , a_2 , 2 a_2 , 4 a_2 , dots.h , 2^k a_2 , dots.h , a_r , 2 a_r , 4 a_r , dots.h , 2^k a_r) , $ +where $k = ⌊log_2 q⌋$. + +To see how this helps us, try the following puzzle. Assume $q = 11$ (so +all our vectors have entries modulo 11), and $r = 1$, so our secret key +has the form $ upright(bold(v)) = (a_1 , 2 a_1 , 4 a_1 , 8 a_1) . $ You +know $upright(bold(v))$ has this form, but you don’t know the specific +value of $a_1$. + +Now suppose I give you the vector +$ upright(bold(x)) = (9 , 0 , 0 , 0) . $ I ask you for another vector +$ "Flatten" (upright(bold(x))) = upright(bold(x)) prime , $ where +$upright(bold(x)) prime$ has to have the following two properties: \* +$upright(bold(x)) prime dot.op upright(bold(v)) = upright(bold(x)) dot.op upright(bold(v))$, +and \* All the entries of $upright(bold(x)) prime$ are either 0 or 1. + +And you have to find this vector $upright(bold(x)) prime$ without +knowing $a_1$. + +The solution is to use binary expansion: take +$upright(bold(x)) prime = (1 , 0 , 0 , 1)$. You should check for +yourself to see why this works – it boils down to the fact that +$(1 , 0 , 0 , 1)$ is the binary expansion of $9$. + +How would you flatten a different vector, like +$ upright(bold(x)) = (9 , 3 , 1 , 4) ? $ I’ll leave this as an exercise +to you! As a hint, remember we’re working with numbers modulo 11 – so if +you come across a number that’s bigger than 11 in your calculation, it’s +safe to reduce it mod 11. + +Similarly, if you know $upright(bold(v))$ has the form +$ upright(bold(v)) = (a_1 , 2 a_1 , 4 a_1 , dots.h , 2^k a_1 , a_2 , 2 a_2 , 4 a_2 , dots.h , 2^k a_2 , dots.h , a_r , 2 a_r , 4 a_r , dots.h , 2^k a_r) , $ +and you are given some matrix $C$ with coefficients in +\$\\ZZ / q \\ZZ\$, then you can compute another matrix $"Flatten" (C)$ +such that: \* $"Flatten" (C) upright(bold(v)) = C upright(bold(v))$, and +\* All the entries of $"Flatten" (C)$ are either 0 or 1. + +The $"Flatten"$ process is essentially the same binary-expansion process +we used above to turn $upright(bold(x))$ into $upright(bold(x)) prime$, +applied to each $k + 1$ entries of each row of the matrix $C$. + +So now, using this $"Flatten"$ operation, we can insist that all of our +cyphertexts $C$ are matrices with coefficients in ${ 0 , 1 }$. For +example, to multiply two messages $mu_1$ and $mu_2$, we first multiply +the corresponding cyphertexts, then flatten the resulting product: +$ "Flatten" (C_1 C_2) . $ + +Of course, revealing that the secret key $upright(bold(v))$ has this +special form will degrade security. This cryptosystem is as secure as an +LWE problem on vectors of length $r$, not $n$. So we need to make $n$ +bigger, say $n approx r log q$, to get the same level of security. + +== Error analysis + +Now let’s compute more carefully what happens to the error when we add, +negate, and multiply bits. Suppose +$ C_1 upright(bold(v)) = mu_1 upright(bold(v)) + epsilon.alt_1 , $ where +$epsilon.alt$ is some vector with all its entries bounded by a bound +$B$. (And similarly for $C_2$ and $mu_2$.) + +When we add two cyphertexts, the errors add: +$ (C_1 + C_2) upright(bold(v)) = (mu_1 + mu_2) upright(bold(v)) + (epsilon.alt_1 + epsilon.alt_2) . $ +So the error on the sum will be bounded by $2 B$. + +Negation is similar to addition – in fact, the error won’t change at +all. + +Multiplication is more complicated, and this is why we insisted that all +cyphertexts have entries in ${ 0 , 1 }$. We compute +$ C_1 C_2 upright(bold(v)) = C_1 (mu_2 upright(bold(v)) + epsilon.alt_2) = mu_1 mu_2 upright(bold(v)) + (mu_2 epsilon.alt_1 + C_1 epsilon.alt_2) . $ + +Now since $mu_2$ is either $0$ or $1$, we know that $mu_2 epsilon.alt_1$ +is a vector with all entries bounded by $B$. What about +$C_1 epsilon.alt_2$? Here you have to think for a second about matrix +multiplication: when you multiply an $n$-by-$n$ matrix by a vector, each +entry of the product comes as a sum of $n$ different products. Now we’re +assuming that $C_1$ is a $0 - 1$ matrix, and all entries of +$epsilon.alt_2$ are bounded by $B$… so the product has all entries +bounded by $n B$. Adding this to the error for $mu_2 epsilon.alt_1$, we +get that the total error in the product $C_1 C_2 upright(bold(v))$ is +bounded by $(n + 1) B$. + +In summary: We can start with cyphertexts having a very small error (if +you think carefully about this +#link("https://hackmd.io/mQB8_nWPTm-Kyua7QgNLNw")[protocol];, you will +see that the error is bounded by approximately $n log q$). Every +addition operation will double the error bound; every multiplication +("and" gate) will multiply it by $(n + 1)$. And you can’t allow the +error to exceed $q \/ 2$ – otherwise the message cannot be decrypted. So +you can perform calculations of up to approximately $log_n q$ steps. (In +fact, it’s a question of #emph[circuit depth];: you can start with many +more than $log_n q$ input bits, but no bit can follow a path of length +greater than $log_n q$ AND gates.) + +This gives us a #emph[levelled] fully homomorphic encryption protocol. +Next we’ll see a trick called "bootstrapping," which lets us turn this +into FHE. diff --git a/easy/src/lwe.typ b/easy/src/lwe.typ new file mode 100644 index 0000000..3741a58 --- /dev/null +++ b/easy/src/lwe.typ @@ -0,0 +1,188 @@ +#import "preamble.typ":* + += A hard problem: learning with errors + + +Many cryptographic protocols rely on some sort of "hard problem" +-- a computationally infeasible challenge +whose difficulty makes the protocol secure. +It is hard to factor a composite number (like $6177$) +into prime factors ($6177 = 71*87$); +the challenge of factoring gives us +#link("https://en.wikipedia.org/wiki/RSA_(cryptosystem)", "RSA"). +The challenge of the discrete logarithm problem +(@discretelog) +gives us elliptic curve cryptography, +KZG polynomial commitments (@kzg), and so forth. + +Our protocol for levelled FHE relies on a different hard problem. +The problem is to solve systems of linear equations. +Except the equations are only approximately true -- +they permit a small "error" -- +and instead of solving for rational or real numbers, +you're solving for integers modulo $q$. + +Here’s a concrete example of a LWE problem and how one might attack it +"by hand." This exercise will make the inherent difficulty of the +problem quite intuitive, but also give us some ideas on how one might +write a LWE solver in practice to attack small LWE +problems. + +#problem[ +We are working over $bb(F)_11$, and there is some secret vector +$a = (a_1 , dots.h , a_4)$. There are two sets of claims. Each claim +"$(x_1 , dots.h , x_4) : y$" purports the relationship + +$ + y = a_1 x_1 + a_2 x_2 + a_3 x_3 + a_4 x_4 + epsilon, #h(0.3in) epsilon in {0,1}. +$ + +One of the sets of claims is "genuine" and comes from a consistent set +of $a_i$, while the other set is "fake" and has randomly generated $y$ +values. Tell them apart and find the correct secret vector +$(a_1 , dots.h , a_4)$. + +#figure( + align(center)[#table( + columns: 2, + align: (auto,auto,), + table.header([Blue Set], [Red Set],), + table.hline(), + [(1, 0, 1, 7) : 2], [(5, 4, 5, 2) : 2], + [(5, 8, 4, 10) : 9], [(7, 7, 7, 8) : 5], + [(7, 7, 8, 5) : 3], [(6, 8, 2, 2) : 0], + [(5, 1, 10, 6) : 3], [(10, 4, 4, 3) : 1], + [(8, 0, 2, 4) : 1], [(1, 10, 8, 6) : 6], + [(9, 3, 0, 6) : 9], [(2, 7, 7, 4) : 4], + [(0, 6, 1, 6) : 9], [(8, 6, 6, 9) : 1], + [(0, 4, 9, 7) : 5], [(10, 6, 1, 6) : 9], + [(10, 7, 4, 10) : 10], [(3, 1, 10, 9) : 7], + [(5, 5, 10, 6) : 8], [(2, 4, 10, 3) : 7], + [(10, 7, 3, 1) : 9], [(10, 4, 6, 4) : 2], + [(0, 2, 5, 5) : 6], [(8, 5, 7, 2) : 2], + [(9, 10, 2, 1) : 2], [(4, 7, 0, 0) : 8], + [(3, 7, 2, 1) : 5], [(0, 3, 0, 0) : 0], + [(2, 3, 4, 5) : 3], [(8, 3, 2, 7) : 8], + [(2, 1, 6, 9) : 3], [(4, 6, 6, 3) : 2], + )] + , kind: table + ) +] + +#solution[ +We start with some helpful notation. Define an #strong[information +vector] +$ + (x_1 , x_2 , x_3 , x_4 lr(|y|) S), +$ +where $S subset F_11$, to +mean the statement +#quote[$sum a_i x_i = y + s$, where $s in S$.] +In +particular, a given purported approximation +$ + (x_1 , x_2 , x_3 , x_4) : y +$ +in the LWE protocol corresponds to the +information vector +$ + (x_1 , x_2 , x_3 , x_4 lr(|y|) { 0 , - 1 }). +$ +The +benefit of this notion is that we can take linear combinations of them. +Specifically, + +#proposition[ +If $(X_1 lr(|y_1|) S_1)$ and +$(X_2 lr(|y_2|) S_2)$ are information vectors (where $X_i$ are vectors), +then + +$ (alpha X_1 + beta X_2 lr(|alpha y_1 + beta y_2|) alpha S_1 + beta S_2) , $ + +where $alpha S = { alpha s \| s in S }$ and +$S + T = { s + t \| s in S , t in T }$. +] + +We can observe the following: + ++ If we obtain two vectors $(X lr(|y|) S_1)$ and $(X lr(|y|) S_2)$, then + we have the information (assuming the vectors are accurate) + $(X lr(|y|) S_1 sect S_2)$. So if we are lucky enough, say, to have + $lr(|S_1 sect S_2|) = 1$, then we have found an actual equation with + no error. ++ As we linearly combine vectors, their "error part" $S$ gets bigger + exponentially. So we can only add vectors very few times, ideally just + 1 or 2 times, before they start being unusable. + +With these heuristics, we are ready to solve this problem. + +#todo[Is it better with the equations displayed, or no?] + +=== Red Set + +First, we show that the "Red set" is inconsistent. Our main strategy +will be to make vectors with many $0$’s in the same places. + ++ Our eyes are drawn to the juicy-looking + $ (0 , 3 , 0 , 0 lr(|0|) { 0 , - 1 }), $ which immediately gives + $ a_2 in { 0 , 7 } $. ++ $ (4 , 7 , 0 , 0 lr(|8|) { 0 , - 1 }) $ gives + $ 4 a_1 + 7 a_2 in { 7 , 8 }. $ Since $ 7 a_2 in { 0 , 5 }, $ + $ 4 a_1 in { 7 , 8 } - { 0 , 5 } = { 7 , 8 , 2 , 3 }, $ and + $ a_1 in { 10 , 2 , 6 , 9 }. $ ++ $ (10 , 4 , 4 , 3 lr(|1|) { 0 , - 1 }) + (7 , 7 , 7 , 8 lr(|5|) { 0 , - 1 }) = (6 , 0 , 0 , 0 lr(|6|) { 0 , - 1 , - 2 }), $ + which is nice because it has 3 zeroes! This gives + $ a_1 in { 1 , 8 , 10 } $. Combining with (2), we conclude that + $ a_1 = 10 $. ++ We can reuse $ (4 , 7 , 0 , 0) : 8. $ Since we knew from (2) that + $ 4 a_1 + 7 a_2 in { 7 , 8 }, $ we can substitute $ a_1 = 10 $ to get + $ 7 a_2 in { 0 , 1 } $. This forces $ a_2 = 0 $ because of (1). + +At this point, basically any isolation of the first two variables would +force a contradiction. For example, we can compute + +$ (8 , 6 , 6 , 9 lr(|1|) { 0 , - 1 }) + (5 , 4 , 5 , 2 lr(|2|) { 0 , - 1 }) = (2 , 10 , 0 , 0 lr(|3|) { 0 , - 1 , - 2 }) . $ + +Since $ 2 a_1 + 10 a_2 = 9$, but $3 + { 0 , - 1 , - 2 } = { 1 , 2 , 3 } $, +we have a contradiction. + +=== Blue Set + +This is slightly harder because we don’t have really nice vectors like +$(0 , 3 , 0 , 0)$, but still very doable. First, we try to isolate two +of the variables. For example, we can compute + +- $10 (10 , 7 , 4 , 10 lr(|10|) { 0 , - 1 }) - 9 (10 , 7 , 3 , 1 lr(|9|) { 0 , - 1 }) = (0 , 0 , 1 , 9 lr(|1|) { 1 , 0 , - 1 })$ +- $5 (5 , 5 , 10 , 6 lr(|8|) { 0 , - 1 }) + 9 (7 , 7 , 8 , 5 lr(|3|) { 0 , - 1 }) = (0 , 0 , 1 , 9 lr(|1|) { 0 , 2 , 6 , 8 })$ + +By looking at the intersection, we can conclude that +$(0 , 0 , 1 , 9 lr(|1|) { 0 })$. Equivalently, $a_3 + 9 a_4 = 1$ or +$a_3 = 1 + 2 a_4$. Now, we can compute + +- $(2 , 1 , 6 , 9 lr(|3|) { 0 , - 1 }) + (9 , 10 , 2 , 1 lr(|2|) { 0 , - 1 }) = (0 , 0 , 8 , 10 lr(|5|) { 0 , - 1 , - 2 })$. + This says that, using $a_3 = 1 + 2 a_4$, + + $ 8 a_3 + 10 a_4 = 8 + 4 a_4 in 5 + { 0 , - 1 , - 2 } . $ + + Solving, we know that $a_4 in 2 + { 0 , - 3 , - 6 } = { 2 , 7 , 10 }$. + +- $- 2 (0 , 2 , 5 , 5 lr(|6|) { 0 , - 1 }) + (0 , 4 , 9 , 7 lr(|5|) { 0 , - 1 }) = (0 , 0 , 10 , 8 lr(|4|) { - 1 , 0 , 1 , 2 })$. + Substituting for $a_3$ as before, we obtain + + $ 10 + 6 a_4 in { 3 , 4 , 5 , 6 } . $ + + Solving, we know that $a_4 in { 8 , 10 , 1 , 3 }$. + +The combination proves that $a_4 = 10$; in turn, we deduce $a_3 = 10$ by +substitution. We omit some details in the remaining algebra (which can +be done in various ways, as any triple of equations will set up 3 +"almost equations" in the 2 remaining unknowns): + ++ $(1 , 0 , 1 , 7 lr(|2|) { 0 , - 1 })$ gives $a_1 in { 9 , 10 }$. ++ $(0 , 6 , 1 , 6 lr(|9|) { 0 , - 1 })$ gives $a_2 in { 8 , 10 }$. ++ $(2 , 3 , 4 , 5 lr(|3|) { 0 , - 1 })$ gives + $2 a_1 + 3 a_2 in { 0 , 1 }$. + +This is enough to conclude that $a_1 = 10$ and $a_2 = 8$, giving the +answer $(10 , 8 , 10 , 10)$. +] \ No newline at end of file diff --git a/easy/src/preamble.typ b/easy/src/preamble.typ index fe6ecc1..dcb3378 100644 --- a/easy/src/preamble.typ +++ b/easy/src/preamble.typ @@ -54,6 +54,7 @@ #let todo = thmbox("todo", "TODO", fill: rgb("#ddaa77")).with(numbering: none) #let proof = thmproof("proof", "Proof") +#let solution = thmproof("proof", "Solution") #let assumption = thmbox("main", "Assumption", fill: rgb("#eeeeaa"), base_level: 1) #let goal = thmbox("main", "Goal", fill: rgb("#eeeeaa"), base_level: 1) @@ -119,6 +120,7 @@ it v(-0.5em) } + show quote: set align(center) // Section headers set heading(numbering: "1.1")