The BGV fully homomorphic encryption scheme
This is a sister blogpost to the previous one about a similar scheme (BFV) and it's part of the series that covers fully homomorphic encryption techniques and applications.
Introduction
In this blogpost we will focus on the encryption, decryption, relinearization and the noise analysis of the Brakerski, Gentry, Vaikuntanathan (BGV) fully homomorphic encryption scheme. A Python implementation of the aforementioned will be provided.
DISCLAIMER: This toy implementation is not meant to be secure or optimized for efficiency. It's for educational purposes only, and for us to better understand the inner workings.
What to expect from this blogpost?
- We briefly describe the motivations of fully homomorphic encryption.
- We present the BGV scheme.
- We offer a Python implementation for the BGV scheme, for educational purposes.
Now, let's make a short, informal, tour before starting. Fully homomorphic encryption (FHE) allows us to perform computation on encrypted data. While there are a few cryptographic schemes to achieve this, in this blogpost we explore the BGV scheme.
Firstly, this scheme allows us to encrypt messages into ciphertexts. Intuitively, a message is hidden in a ciphertext if there is no way to get to retrieve the original message from its encryption. To an attacker that sees a ciphertext, that ciphertext should look like "random garbage". In order to achieve this we hide the message by "masking" it with some noise. We say it's hard for an attacker to retrieve a message from its ciphertext if doing so is at least as hard as solving a computationally unfeasable problem. The problem the security of the BGV scheme is based on is called the Ring Learning with Errors problem.
Secondly, the scheme allows us to perform arithmetic operations (additions, multiplications) using noisy ciphertexts. However, we will encounter some problems along the way, such as the noise increasing after computations, or the size of the resulting ciphertext increasing. We'll see how to handle these using modulus switching (for the former) and relinearization (for the latter).
This post will get a bit mathematical, so before starting we introduce some notation. Any other notation will be defined when introduced.
Notations:
- - messages.
- - ciphertexts. If ciphertexts have more components, we denote the components with indices:
- - Security parameter.
- - the set of integers from
- - Ring.
- Ex: - the integers.
- Ex: - the quotient polynomial ring with integer coefficients: .
- - Ring modulo the ideal generated by .
- Ex: - the integers mod .
- Ex: - the quotient polynomial ring with coefficients being integers in .
- - , coefficients centered in . When applying to a polynomial we mean to apply it to all its coefficients individually. When applying to a vector of elements, we mean to apply it to each element individually. So . We give the similar treatment to reduction operation
- - Noise distribution, parametrized by (usually Gaussian).
- For simplicity, when we say we sample a polynomial from some distribution (or a polynomial comes from some distribution) we mean that its coefficients are sampled independently from that distribution.
- - An element sampled from the distribution
- - An element sampled uniformly from a set
- Ex: An element sampled uniformly from the ring
- denotes the multiplication of 2 elements from the ring
- denotes the multiplication of coefficients of by some scalar .
Fully Homomorphic Encryption (FHE)
Before defining fully homomorphic encryption, let's look at some applications of it to get a better intuition:
1. Outsourcing storage and computations
- A company wants to use a cloud provider to store data and do computational heavy lifting.
- Task: The company wants to use the information (for example: to do machine learning, extract statistical properties) without locally retrieving it (defeating the purpose of using the cloud company services). Furthermore, the company doesn't want the cloud provider to see the sensitive data.
- Solution: Using homomorphic encryption, the company can encrypt and send the data to the cloud provider and then the cloud provider can process the information (as requested by the company) in the encrypted form, without learning anything about the data.
- Ex: finance, medical apps, consumer privacy in ads.
2. Private queries
- A client wants to retrieve a query against a large database held by a provider.
- Task: The client wants to retrieve a query without the database provider learning the query.
- Solution: Using homomorphic encryption the client can encrypt the index of the record and then the database can use this encrypted index to fetch (the comparison operation can be done using FHE) and return the encrypted result to the client. Finally, the client can decrypt the record.
3. Private set intersection
- A client and a server each has a set of elements. The client wants to know which elements from its set are in the server's set.
- Task: The client wants to find out the intersection of its elements and the server's, without the server learning anything about the client's elements and vice versa.
- Solution (Very simplified): They translate the comparison operation into a subtraction (). The client encrypts its elements and sends them to the server. Then, the server evalutes these subtractions homomorphically. Because the result of the subtraction is an encrypted result, the server learns nothing. Then the server sends the comparison results back and the client decrypts the results. We have a longer, more in-depth blogpost about it here. Happy reading!
- Examples: Client: user contacts, Server: whatsapp contacts; Client: own passwords, Server: database with breached passwords.
Intuition
- A user has an arithmetic function and some inputs .
- He wants to compute but instead of computing it directly, he wants to encrypt and do the computation on the ciphertext, obtaining a result which eventually decrypts to .
- It's helpful to view the functions as tree of operations. For example (taken from the BFV blogpost):
Homomorphic Encryption -- Definition
Let be a tuple of procedures.
Let be a function in a family of functions. This function can also be written as a circuit from a family of circuits .
- - key generation, is a security parameter, is a functionality parameter (degree of the polynomial or depth of the circtuit).
- - Encryption of a message -- known as fresh ciphertexts.
- - Evaluate the function on the ciphertexts -- known as evaluated ciphertexts.
Correctness
We require the (FHE) scheme to correctly decrypt both fresh and evaluated ciphertexts:
In the image above, the user with the laptop is interested evaluating a certain circuit on its data . Therefore it sends its data, encrypted, to the cloud so that the cloud computes the circuit. By correctness, the user indeed gets the circuit applied on its data .
Circuits?
The two important operations we care about are addition and multiplication, because with them we can construct any arithmetic operation! Homomorphic encryption schemes started out by encrypting bits, so the equivalent operations for additon and multiplication on bits, xor
and and
are used.
To convey the fact that a circuit supports both xor
and and
usually a nand
gate is used, because nand
gates are composed by 1 multiplication and 1 addition and they are logically complete (you can build any circuit with them).
By extending to multiple bits, with the gates we discussed above you can represent any computation using boolean circuits.
HE can be classified based on the type of functions that it supports:
- Partially homomorphic encryption
Given , and you can do limited operations (either additions or multiplications) - Somewhat (leveled) homomorphic encryption
Given you can compute where is a polynomial of a limited degree. One can do limited number of multiplications (Circuits of a maximum depth). - Fully homomorphic encryption
One can do unlimited multiplications and additions.
The community put a lot of effort into the analysing FHE schemes: finding good parameters, finding software and hardware optimizations, coming up with possible attacks and adjusting the schemes against them. An interesting read is the homomorphic encryption standard, which compiles good practices and techniques for developing and using existing schemes.
Ring Learning With Errors (RLWE)
In this section we'll focus on briefly introducing the Ring learning with errors (RLWE) problem. The problem is very complex and we'll only attempt to cover the basic blocks for building an intuition. For the interested reader more details can be found in this paper.
Quick recap on working with polynomials
This section is taken from the BFV blogpost, but we'll repeat it here to save you a click. For this subsection we denote with .
The HE scheme we are going to describe deals with adding and multiplying polynomials. Here we present a quick example of how to work with polynomials, so that we all have the same starting point. If you already know how to do this, you can skip it.
First thing, let's add and multiply polynomials modulo some polynomial . This "modulo " thing simply means that we add and multiply the polynomials in the usual way, but we take the remainders of the results when divided by . When we do these additions and multiplications , we sometimes say in a fancy way that we are working in the ring of reduced polynomials.
Let's take . If we look at , then . Therefore, when taking the reminder we get . For faster computations you can use this trick: when making , simply replace by , by and so on.
Let's consider two polynomials and . Then:
Here nothing special happened. Let's multiply them:
Now let's go one step further and see how we perform these operations of polynomials not just modulo , but also modulo some integer . As you might expect, the coefficients of these polynomials are also reduced modulo , so they always take integer values from to This time, we say that we are working the ring of reduced polynomials .
Let's take the previous example, , , and consider . We can think of and as already taken . If we take them further modulo , then and . Moreover,
and
To give a powerful visual intuition of polynomials in quotient rings, consider the following polynomial and its visual representation. For this grade 3 polynomial we have four pentagons, for each coefficient. Each pentagon represents . The value of the coefficient is colored with green:
The security of the BGV scheme
Like we said in the introduction, our ciphertexts will be masked with some noise to look as random as possible in order to hide any information an attacker may extract about the message.
In fact it can be proven that the BGV scheme is secure as long as nobody can solve the Ring Learning With Errors (RLWE) problem described below. The parameters of the scheme must be chosen such that there are no known algorithms to solve the underlying RLWE problem in feasable time.
Formally, the idea of "ciphertexts looking like garbage" is called ciphertext indistinguishability. This concept can be formulated using a game between a challenger and an adversary:
- The challenger receives two messages from the adversary. Then, the challenger encrypts them into .
- Then, the challenger secretly flips a bit and sends the ciphertext to the adversary.
- The adversary must guess the bit . After running an efficient algorithm, it outputs a bit . It wins if .
If there exists at least one adversary that can win the game with a probability significantly larger than 1/2 (a coinflip) using a computationally efficient algorithm (something that can run in our universe's lifetime), the challenger's algorithm is considered insecure. When we prove the security of a scheme, we actually show that if an adversary had such an algorithm that can win the security game, he can take that algorithm and use it to solve a problem that is considered computationally hard.
We will now describe the RLWE problem.
Consider the quotient polynomial ring . This ring has polynomials with degree at most and integer coefficients in .
The RLWE problem - decision
The problem asks to distinguish between the following two distributions:
- with , .
- with , , and for some "small" secret sampled before.
The RLWE problem also has a search version, where given pairs , you need to find . In practice, the decision version is more widely used when proving the security of different schemes.
More details about the RLWE problem can be found in this paper. In essence, by adding some small noise , from a Gaussian distribution, we make our secret unrecoverable even if we have access to many RLWE samples.
The original BGV paper presents the algorithms (encryption, decryption, etc) of a HE scheme based on a more general problem, the General Learning with Errors (GLWE) problem. For simplicity, we work with the RLWE case in this blogpost.
The BGV encryption scheme
Generate the following parameters
- - degree of . is chosen as a power of 2.
- - ciphertext modulus.
- - plainext modulus, .
- - the noise distribution, a discrete Gaussian.
We consider . This ring has polynomials with degree at most and integer coefficients in .
Note:
- All polynomial operations are considered unless otherwise specified, to ease notation.
- When we talk about "small" polynomials, we intuitively think about them having small coefficients.
- Polynomials that come from the noise distribution will be colored with green (ex: ) and the ones that will come from the secret key distribution will be colored with red (Ex: ).
We will have one secret key , a public key with two components , a ciphertext with 2 components The messsage will come from the ring .
SecretKeyGen(params) -> sk
- Draw from a secret key distribution which outputs "small" elements with overwhelming probability. In practice , with the probability of sampling specified as a parameter and the probabilities of sampling or being equal.
- Return the secret key .
PubKeyGen(sk, params) -> (pk0, pk1)
- Draw a random element and the noise .
- Return the public key: .
Notice that the public key looks like a RLWE sample, but the error is multiplied by the plaintext modulus .
Encrypt(m, pk, params) -> (c0, c1)
- Draw the noise and a "small" polynomial in the same way as the secret.
- Compute
- Compute
- Return .
The ciphertext components are elements in .
Decrypt(c, sk, params)
- Compute
- Return .
Correctness (Click for math)
Let's unroll the equations to check the correctness. All operation are to ease notation, unless we specify otherwise.
The public key is:
The public key hides the secret as a RLWE sample.
The encryption equations unrolled are:
The decryption equation unrolled is:
The decryption is correct as long as the error we add to does not wrap the coefficients of arond the modulus . The noise analysis will be offered later, in its own section.
Now, we reduce the above and we get:
FHE Operations
Here we will explore the homomorphic properties of the scheme. We refer to the two input message and ciphertext pairs with and to the resulting message-ciphertext pair with .
The goal is to find two functions, , that work on ciphertexts, for addition and multiplication, such that:
- when we have and
- when we have and
Recall that and encrypt and i.e.:
FHE Addition
The image on the left performs the addition before the encryption, with the messages in clear. This is not what we want. The image on the right is the desirable outcome, where we encrypt the messages, then perform addition on the encrypted messages.
We define add
:
add(c, c') -> c*
- return
Correctness (click for math)
And by using Decrypt(c*, sk, params)
we have
Again, by reducing everything we have
with the same remark that for the decryption to be correct the error that we add to must not wrap around .
The resulting ciphertext is not fresh anymore (just encrypted), and has a bigger noise than any of the previous ciphertexts that were inputs in the addition operation. In general, using non-fresh ciphertexts to perform future operations will carry over the noise from all the previous operations.
We'll analyse how the noise grows in a dedicated section at the end.
FHE Multiplication
As in the addition case, we want the multiplication to be done on the encrypted messages.
Before describing how we multiply two ciphertexts we need to reinterpret the decryption equation: as a linear equation: in .
If we look at adding two such linear equations in we obtain another linear equation in :
However, when multiplying two linear equations in we get a quadratic equation () in :
We can define mul
in the following way:
mul(c, c') -> c*
- return
Remarks
- Similarly, as in addition, the noise of the resulting ciphertext increases.
- We increase the number of ciphertext components by 1. This is not sustainable.
- Our decryption equation is linear, however after
mul
we have a quadratic decryption equation with 3 ciphertexts that doesn't play well with our idea of linear decryption equation.
To solve the remarks the concept of relinearization is introduced.
Relinearization
The concept was explained in the BFV blogpost as well.
Intuition: We want to transform the quadratic equation in into some other linear equation in some other secret key .
In order to do this we need to give some extra information (a "hint") about the key that will help us get .
Consider the hint to be the pair:
This is very similar to how the public key is generated and we can use the PubKeyGen -> (pk0, pk1)
to generate it:
Intuitively, we use a RLWE sample to hide .
Notice that:
The easiest way to get the term from the above equation is to multiply it by . However, is a random element in and it will yield some large noise . So we have to do something smarter.
Key switching v1
Key switching is a variant of relinearization. We start with a ciphertext .
Idea:
Split the ciphertext into multiple "small" ciphertexts (for now think of small as the ciphertext polynomials having small coefficents). We can do this by decomposing the ciphertext coefficients into a small base .
Intuition:
To build an intuition, we can take the most common bases used in representing numbers:
- Base 10:
- Base 2:
Programmatically, this can be done by dividing the integer by the base repeatedly and collecting the remainders.
In Python code:
def int2base(x: int, base: int) -> List[int]:
digits = []
while x > 0:
q, r = divmod(x, base)
digits.append(r)
x = q
return digits
print(int2base(123, 10))
# [3, 2, 1]
print(int2base(12, 2))
# [0, 0, 1, 1]
A ciphertext component is a polynomial of degree less than , with coefficients in , which we denote as follows:
We want to write the coefficients in base . Since any coefficient is less than , this decomposition has at most terms. For example where is the corresponding digit in the decomposition. Using these digits we can construct the polynomials :
Then using these polynomials we can decompose the original polynomial :
The polynomials are elements of and for a reasonable small they have small coefficients (). For each one of them we generate the hints:
with and .
Now we go back to our ciphertext , the result of EvalMul
. We decompose in the base and we get:
Then we construct the new ciphertext with the two components:
Using the above relations, we obtain the following linear equation:
(the terms containing from and from cancel out, is reconstructed and we are left with an error term).
Because the relinearization error is a multiple of , it goes away when we decrypt and reduce as long as the relinearization error does not wrap around .
Remarks
- We need to relinearize after a multiplication.
- A smaller means we have a smaller noise, but more relinearization keys are needed.
- is chosen based on what we want to optimize. We usually want to choose to introduce a noise around the size of the noise in the ciphertexts. Once the noise grows we can use or something else. We can also choose a big at the start (which introduces lots of starting noise) and we don't need to change it later.
Modulus switching
Task: You want to reduce the noise that accumulates in the ciphertext, after performing homomorphic operations. The modulus switching technique allows us to decrease the noise, to ensure the decryption is still correct after performing homomorphic operations. More analysis is provided in a dedicated section below.
Idea: The technique uses two moduli with and . This involves changing the ring where the ciphertexts live, , with a ring , that is parametrized by the smaller modulus. When you perform this change, the coefficient ring is "approximated" by some ring , in the intuitive sense that the numbers become smaller (the numbers are scaled down by some factor). As we will see this means that the noise will be smaller in absolute value. Although, the noise will relatively stay the same, since , we mostly care about the magnitude of the noise, especially in multiplications.
You can perform this repeatedly with many moduli with decreasing values to lower the noise after each multiplication.
The easiest way to do this is to scale down the ciphertext's components by and smartly round in a way to keep the decryption equation correct. To ensure this, we want the scaling to keep the following propriety: , where where .
The two requirements that we want to have are
- The decryption should be correct (decryption mod should be the same as decryption mod ): .
- The noise scales down.
Rounding
In order for the decryption to be correct we must adjust before scaling by . To do this we add a small correction term per component. We require to:
- Only influence the error, hence (it will disappear when decrypting)
- Make the ciphertext to be divisible by :
One easy choice to set the correction terms such that both properties hold is where is the inverse of modulo .
In the end, we define the components of the modulus switched ciphertext:
Correctness (Click for math)
Now, let's check the decryption equation is actually correct. For a :
When doing we mention that is the same as in under some conditions (see lemma 1 from here).
In the end we decrypt instead of . In practice this can be solved by multiplying by before encryption or after decryption, or to take (however such a can be harder to find).
Key switching v2
Now that we know about modulus switching we introduce a second version to do key switching in BGV. Recall that the key switching technique is used in relinearization, for decreasing the size of the ciphertext obtained in multiplication.
Idea: Given two moduli with and , we start with elements in and we make a detour in a ring with a bigger modulus where we perform our relinearization. Then we perform modulus switching to go back to our small ring .
We generate the hint:
Recall that is the result of the mul
. Then we compute the components of the new ciphertext :
Lastly, we scale them both using modulus switching: with , like before.
The relinearization equation looks like this:
Because the relinearization error is a multiple of (remember that we chose to be multiples of too, to not influence the error), it will go away when we decrypt and reduce as long as the relinearization error does not wrap around .
Correctness (click for math)
Noise analysis
The main motivation of analysing the noise growth is because we want to set the parameters of the scheme based on it.
Relevant papers:
- Ilia Iliashenko thesis
- CS15
- Evaluating the effectiveness of heuristic worst-case noise analysis in FHE
- GHS
- Finding and Evaluating Parameters for BGV
Measuring Noise
When doing operations (such as addition, multiplication) with noisy ciphertexts, the total noise accumulates. In the end we want our decryption to be correct. However, this can only happen if the accumulated noise does not exceed some bound.
Intuition: We want to "measure" this accumulated noise by looking at "how much" a polynomial can impact an operation it's involved in. For example, we can analyze the impact of the polynomials which represent the RLWE noise or of the secret . We can represent the polynomial's impact with a number.
We can "measure" the impact of the polynomials using the:
- Infinity norm: , which is the value of the biggest coefficient in absolute value.
- Canonical norm: . In order to define it we use the so called canonical embedding of the polynomial.
While the infinity norm is easy to understand, the canonical embedding is a bit more mathematically involved. Instead of searching for a way to measure the impact of a polynomial , we transfer the polynomial in another space, such as , and we search for a better measure there.
Recall that we work with polynomials from the ring with a power of .
Canonical embedding
We have the quotient polynomial of which has the complex roots with .
Then we take a polynomial :
and we evaluate in every root . This leads to a vector . We call this vector the canonical embedding of .
The canonical embedding is defined as
Canonical norm
Since now we have obtained a new vector in we can look at its infinity norm and define the canonical norm .
In practice, the canonical norm is used to compute the noise bounds, because it provides a better decryption noise bound.
- There is a relationship between these norms: for some constant , which is when is a power of (see this). With this inequality in mind, it suffices to assure correct decryption by setting the canonical norm of the noise to be less than .
- When doing multiplications, the noise bound offered by the canonical norm increases slower than the bound offered by the infinity norm. We have these two inequalities:
where is the so-called expansion factor of the ring . For more details, see this.
Noise bounds
Suppose we choose with coefficients sampled independently from one of the distributions (discrete Gaussian, random, ternary). Let be the variance of each coefficient in . In Ilia Iliashenko's thesis and in the Finding and Evaluating Parameters for BGV paper it's shown that we can choose a number such that
holds with overwhelming probability. In practice, we set .
When analysing the noise growth we'll make use of the following proprieties. For two polynomials with variances and some constant we have:
- ,
- ,
- .
For the distributions that we work with we have the coefficient variances:
- Discrete Gaussian distribution with standard deviation .
- Ternary distribution: .
- Uniform distribution with integer coefficients in .
Noise for operations
We now have the building blocks (starting values and operations) to analyse the noise for each operation. The goal of analysing the noise buildup is to choose the scheme parameters such that the scheme properties (fhe, decryption) hold and the scheme remains secure. Following the paper Finding and Evaluating Parameters for BGV we have:
Fresh ciphertext noise
In order to make sure the decryption is correct we must make sure the error
does not wrap around the modulus (i.e ). The message is thought to come uniformly from .
Recall that
- The errors come from a discrete Gaussian => .
- The secret and come from a ternary distribution => .
- The message comes from and we assume that the coefficients are uniformly distributed in => .
Hence:
The parameters for the scheme and distributions, , are chosen such that the decryption is correct.
Addition
For two ciphertexts encrypting we have the corresponding errors . The error after ciphertext addition is .
If we use the decryption equation we have
We have with and (because both ). By replacing in the equation above we obtain:
We see that the noise grows linearly.
Finally, using the canonical norm we have the following:
Multiplication
Similarly for multiplication, if we use the decryption equation we have:
If we look at the error term we see that in the leading term we multiply the noises, therefore the noise grows quadratically.
Using the canonical norm, we have:
Modulus switching
For modulus switch we scale the noise down and then we add the small error that comes with the correction terms .
Recall that
- We set . We assume the terms are uniformly distributed mod , then multiplied by => .
Using the canonical norm we get:
We can see that the noise is scaled down by almost .
Key switching - Base decomposition
In relinearization we deal with . Here we decompose in base and hide the hints about using RingLWE-like samples involving errors . Remember from key switching technique using base decomposition that we have a relinearization error: . The variance of any term from the sum is .
Recall that
- The errors come from a discrete Gaussian distribution => .
- We can assume that the polynomials in base behave like random polynomials extracted from => .
If we use the canonical norm:
Key switching - Modulus switch
In the relinearisation version that employs modulus switching, we have the following error term: .
Recall that:
- The ciphertext component comes from => .
- The errors comes from a discrete Gaussian => .
- The secret comes from a ternary distribution => .
- We set . We assume the terms are uniformly distributed mod , then multiplied by => .
The variance of this error term is:
Now we can bound:
Code
And now that we have discussed all the ingredients and spices of the BGV encryption scheme, it's time to bake it, sorry, implement it.
We provide a proof of concept implementation, in Python. The aim of the code is to mirror the equations easily. This means that if we see in the theory part we prefer to see a * s + e
in code instead of polyadd(polymul(a, s), e)
.
DISCLAIMER: The code should be used for educational purposes only, and never in production.
We represent polynomials using a class: QuotientRingPoly
. Every polynomial is defined by three attributes:
coef: np.ndarray
- Vector of the polynomial's coefficients.coef[-1]
is the free coefficient,coef[0]
is the leading coefficient. It's always padded to haven
elements.coef_modulus: int
- Modulus of the ring where its coefficients live in.poly_modulus: int | np.ndarray
- Modulus of the polynomial ring where it belongs:X^n + 1
QuotientRingPoly
has the ._reduce(self)
method which reduces any polynomial to an element in the ring.
We overloaded the operators +, -, *, //, %
:
- Operators work between two
QuotientRingPoly
using the default polynomial operation rules. - Operators work between a
QuotientRingPoly
and anint | float
coefficient-wise.
The attributes coef
and coef_modulus
can be assigned too. After assigning, the _reduce()
method will be called. This is helpful to easily change the ring of the polynomial.
Remarks
- To work with big integers, we use the python built in
int
class. To use this with numpy we setdtype = object
for every array creation. - We have helper functions
roundv
andintv
(round vector, int vector) that vectorize theround
andint
operations, allowing us to help with rounding and converting vector elements to python's builtinint
class.
Resources
- BGV paper - Original paper
- BV paper
- FHE standard
- CS15 - for comparisons between FHE schemes
- Ilia Iliashenko thesis - for noise analysis
- Evaluating the effectiveness of heuristic worst-case noise analysis in FHE
- GHS
- Finding and Evaluating Parameters for BGV
- Bitdefender BFV blogpost
- Inferati blogpost