Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Concrete 0.2.0 is the first version of the new Concrete Library. It is based on experimental features in concrete-core
through the intermediate libraries (concrete-integer
, concrete-shortint
). It is published with a temporary dependency concrete-core-experimental
. Future versions of Concrete 0.2 will be based on a public version of concrete-core
.
To use concrete
in your project, you first need to add it as a dependency in your Cargo.toml
:
concrete
exposes different cargo features
to customize the types and features used.
concrete
types
This crate exposes 3 kinds of data types. Each kind is enabled by activating its corresponding feature in the TOML line. Each kind may have multiple types:
By enabling the serde
feature, the different data types and keys exposed by the crate can be serialized / deserialized.
Copy this if you would like to enable all features:
As concrete
relies on concrete-core
, concrete
is only supported on x86_64 Linux
and x86_64 macOS
.
Windows users can use concrete
through the WSL
.
macOS users who have Apple Silicon (arm64
) devices can use concrete
by compiling using the nightly
toolchain
First, install the needed Rust toolchain:
Then, you can either:
Manually specify the toolchain to use in each of the cargo commands:
For example:
Or override the toolchain to use for the current project:
To check the toolchain that Cargo will use by default, you can use the following command:
concrete
is a Rust crate (library) meant to abstract away the details of Fully Homomorphic Encryption (FHE) to enable non-cryptographers to build applications that use FHE.
FHE is a powerful cryptographic tool, which allows computation to be performed directly on encrypted data without needing to decrypt it first.
Concrete 0.2 is a complete rewrite of the Concrete library. Previous release (0.1.x) was cryptography oriented while the new 0.2 version is developer oriented. There is no backward compatibility.
Concrete Library implements Zama’s variant of Fully Homomorphic Encryption over the Torus (TFHE). TFHE is based on Learning With Errors (LWE), a well studied cryptographic primitive believed to be secure even against quantum computers.
In cryptography, a raw value is called a message (also sometimes called a cleartext), an encoded message is called a plaintext and an encrypted plaintext is called a ciphertext.
Using FHE in a Rust program with Concrete consists in:
generating a client key and a server key using secure parameters:
client key encrypts/decrypts data and must be kept secret
server key is used to perform operations on encrypted data and could be public (also called evaluation key)
encrypting plaintexts using the client key to produce ciphertexts
operating homomorphically on ciphertexts with the server key
decrypting the resulting ciphertexts into plaintexts using the client key
This crate provides different types which are the counterparts of native Rust types (such as bool
, u8
, u16
) in the FHE domain.
With concrete
crate, our goal is to let any developer without any prior cryptographic knowledge to build his own FHE application. To reach that goal, some of the complexity is hidden from the user.
Aside from the advanced customization options offered directly by concrete
, an advanced user could also have a look at the underlying libraries.
concrete
is built as a framework of libraries, but we greatly suggest to any user to start building applications with the concrete
crate.
In its current state, concrete
crate is built on top of 3 primitive crate types: respectively, concrete-boolean
for boolean type, concrete-shortint
for the integers from 2 to 7 bits, and concrete-int
for the integer from 4 to 16 bits. Cryptographic operations will be handled by concrete-core
.
We have summarized the relation between all concrete
crates with the following diagram:
Kind | Cargo Feature | Type(s) |
---|---|---|
⭐️ | 🗣 | 📁
The idea of homomorphic encryption is that you can compute on ciphertexts while not knowing messages encrypted in them. A scheme is said to be fully homomorphic, meaning any program can be evaluated with it, if at least two of the following operations are supported (is a plaintext and is the corresponding ciphertext):
homomorphic univariate function evaluation:
homomorphic addition:
homomorphic multiplication:
Zama's variant of TFHE is fully homomorphic and deals with fixed-precision numbers as messages. It implements homomorphic addition and function evaluation via Programmable Bootstrapping. You can read more about Zama's TFHE variant in the .
If you would like to know more about the problems that FHE solves, we suggest you review our .
Booleans
booleans
ShortInts
shortints
Integers
integers
Due to their nature, branching operations like if else
statements are not possible.
The different types exposed by this crate overloads operators.
There are two types of operators, binary operators and unary operators. Binary operators like +
work with two values, while unary operators like !
work with one value.
In other words:
Concrete is an FHE library based on TFHE.
It is interesting to understand some basics about TFHE, in order to apprehend where the limitations are coming from both in terms of precision (number of bits used to represent the plaintext values) and execution time (why TFHE operations are slower than native operations).
Although there are many kinds of ciphertexts in TFHE, all the encrypted values in concrete are mainly stored as LWE ciphertexts.
The security of TFHE relies on LWE which stands for Learning With Errors. The problem is believed to be secure against quantum attacks.
An LWE Ciphertext is a collection of 32-bits or 64-bits unsigned integers. Before encrypting a message in an LWE ciphertext, one needs to first encode it as a plaintext. This is done by shifting the message to the most significant bits of the unsigned integer type used.
Then, a little random value called noise is added to the least significant bits. This noise (also called error for Learning With Errors) is crucial to the security of the ciphertext.
To go from a plaintext to a ciphertext one needs to encrypt the plaintext using a secret key.
A LWE ciphertext, is composed of two parts:
The mask of a fresh ciphertext (one that is the result of an encryption and not an operation such as ciphertext addition) is a list of n
uniformly random values.
The body is computed as follows:
Now that the encryption scheme is defined, to illustrate why it is slower to compute over encrypted data, let us show the example of the addition between ciphertexts.
To add two ciphertexts, we must add their $mask$ and $body$ as done below.
In FHE, there are two types of operations that can be applied to ciphertexts:
leveled operations, which increase the noise in the ciphertext
bootstrapped operations, which reduce the noise in the ciphertext
In FHE, the noise must be tracked and managed in order to guarantee the correctness of the computation.
Bootstrapping operations are used across the computation to decrease the noise in the ciphertexts, preventing it from tampering the message. The rest of the operations are called leveled because they do not need bootstrapping operations and thus are most of the time really fast.
The following sections explain the concept of noise and padding in ciphertexts, which are core to how Concrete enables efficient homomorphic operations.
For it to be secure, LWE requires random noise to be added to the message at encryption time.
In TFHE, this random noise is drawn from a Centered Normal Distribution parameterized by a standard deviation. This standard deviation is a security parameter. With all other security parameters set, the larger the standard deviation is, the more secure the encryption is.
In Concrete, the noise is encoded in the least significant bits of the plaintexts. Each leveled computation will increase the noise, and thus if too many computations are done, the noise will eventually overflow onto the significant data bits of the message and lead to an incorrect result.
The figure below illustrates this problem in case of an addition, where an extra bit of noise is incurred as a result.
Concrete manages the noise for you, by performing bootstrapping operations to reset the noise when needed.
Since encoded values have a fixed precision, operating on them can sometime produce results that are outside the original interval. To avoid losing precision or wrapping around the interval, Concrete uses the additional bits by defining bits of padding on the most significant bits.
As an example, consider adding two ciphertexts. Adding two values could en up outside the range of either ciphertexts, and thus necessitate a carry, which would then be carried onto the first padding bit. In the figure below, each plaintext over 32 bits has one bit of padding on its left (i.e., the most significant bit). After the addition, the padding bit is no longer available, as it has been used in order for the carry. This is referred to as consuming bits of padding. Since no padding is left, there is no guarantee that additional additions would yield correct results.
Concrete manages this for you.
Concrete 0.2.0 proposes predefined types to represent the encryption of unsigned integers, called FheUint
. In the case where input message size is small enough, the message is simply encrypted with one ciphertext.
By means of example, the type FheUint3
, which is used to represent messages encoded over 3 bits, will contain only one ciphertext. When the input message size is larger, many ciphertexts are used. The idea is to split the input message following a radix decomposition, whose decomposition basis is dependent on the chosen parameter set.
As FHE types exposed by this crate are not (since they are bigger than native types and contain data on the heap), the operators (whether binary or unary) are overloaded on both owned values (T
) and references / borrowed values (&T
) to eliminate the need to clone
the values each time they are used.
By default, the cryptographic parameters provided by Concrete 0.2.0 ensure at least 128 bits of security. The security has been evaluated using the latest versions of the Lattice Estimator () with red_cost_model = reduction.RC.BDGL16
.
For all sets of parameters, the error probability when computing a univariate function over one ciphertext is . Note that univariate functions might be performed when arithmetic functions are computed (for instance, the multiplication of two ciphertexts).
An LWE secret key is a list of n
random integers: . is called the
The mask
The body
To add ciphertexts, it is sufficient to add their masks and bodies. Instead of just adding 2 integers, one needs to add elements. The addition is an intuitive example to show the slowdown of FHE computation compared to plaintext computation but other operations are far more expensive (e.g. the computation of a lookup table using the Programmable Bootstrapping)
If you would like to know more about TFHE, you can find more information in our .
For instance, in the type FheUint12
, now representing messages encoded over 12 bits, 6 ciphertexts are involved. Each one of them contains the encryption of messages encoded over 2 bits. Before being encrypted, the message over 12 bits is split into 6 smaller messages over 2 bits such that . In other words, the message is first decomposed over the basis (for 2 bits), and then each piece is separately encrypted.
By defining a new dynamic type (see this ), Concrete offers the possibility to use a different representation than the radix one, called the CRT representation. This is based on the famous , which defines a way to represent integer values using smaller moduli. For instance, let be the original message modulus. Then, computing arithmetic operations over the moduli leads to the same result (thanks to the CRT theorem) than directly computing over modulus . Let and . Then, in the CRT representation, becomes ), and becomes . Then, the product is simply obtained with . Recovering the result modulus is done by computing the inverse CRT. The main advantage of this representation lies in the possibility to compute each arithmetic operation in parallel on each modulus blocks.
As explained in the Introduction, most types are meant to be shared with the server that performs the computations.
The easiest way to send these data to a server is to use the serialization
and deserialization
features. concrete
uses the serde framework. Serde's Serialize
and Deserialize
functions are implemented on Concrete's types.
To be able to serialize our data, we need to pick a data format. For our use case, bincode is a good choice, mainly because it is a binary format.
In the concrete-example-client-server GitHub repository, we have a more complete example project of an application that is composed of a client and a server.
This example demonstrates the use of the following aspects:
Building Client Server architecture
Communicating via TcpStream
Using serialization to exchange encrypted data
Using serialization to save a client key locally and save processing time
Creating a generic function for use with different Fhe
types
Simple multithreading on the server side to handle multiple clients at the same time
Communication is achieved via a tcp connection. The server is the listener, so it creates a TcpListener
that listens for incoming connections on localhost
port 8080
.
When a client initiates a connection, the main server thread calls the handle_client
function in a new thread (and also moves the tcp connection to this new thread).
If we did not create another thread, a client connected to the server would have to wait for the previous client to finish and end its connection before proceeding.
The first thing that the server does is receive and deserialize the ServerKey
sent by the client. Then, it immediately calls set_server_key
.
Once the key is set, the function starts an infinite loop
.
The first step of the loop is the server receiving a "token" sent from the client, to know if the client wishes to stop the connection.
The second step is rather simple: The server expects the client to send 3 FheUint3
s; the server deserializes them and performs a fhe_computation
on them; and, finally, the server serializes the results and sends them to the client.
The last step is similar to the previous one. The difference is that the server expects and uses FheUint16
.
The fhe_computation
is a generic function defined as:
The client code has a more code, as it does a bit more than just communicating with the server. It interacts via the standard input with a user and manages the keys, as well.
First, the client generates the ClientKey
and the ServerKey
. It uses the custom function key_gen
to do so. The key_gen
function's goal is to save the file on disk and reuse the saved keys to avoid regenerating them each time the client process starts, saving a lot of time.
Next, the tcp connection is initiated, and the ServerKey
is sent to the server.
Once the key was successfully sent, the client does the same thing as the server: it enters an infinite loop
.
The first step of the loop is to send the value 1
to the server to tell it we want to continue.
Then, the client reads from the standard input 3 numbers that must fit in 3 bits. These numbers are then encrypted, serialized, and sent to the server.
Next, the client reads the result returned by the server and deserializes, decrypts, and prints the result on the standard output.
This is done again, but this time for numbers that can fit into 16 bits.
Finally, the clients ask the user if it wishes to perform further computations. If that is the case, the client will send the value 0
to the server, letting it know that the connection can end.
In this example, we are going to build a small function that homomorphically computes a parity bit.
We will first write a non-generic function. Then, we will write it using generics to be able to use the function with both FheBool
s and normal bool
s.
Our function that computes the parity bit will take 2 parameters:
A slice of boolean
A mode (Odd
or Even
)
This function will return a boolean that will be either true
or false
so that the sum of booleans (in the input + the returned one) is either an Odd
or Even
number depending on the requested mode.
Since we are going to use booleans, we must enable the booleans
feature in our Cargo.toml.
First, we need to define the function as well as the verification function.
The way to find the parity bit is to first initialize it to false, then
XOR
it with all the bits, one after the other and add negation depending on the requested mode.
We also define a validation function, that simply sums together the number of the bit set within the input with the computed parity bit and checks that the sum is an even or odd number, depending on the mode.
We can now call it, but first we have to do the mandatory configuration steps:
Now we want to make our compute_parity_bit
function generic so that we can use it with both FheBool
and bool
.
Writing a generic function that accepts FHE
types as well as clear types can help test the function to see if it is correct. If the function is generic, that means we can run it with clear data, allowing the use of print-debugging or a debugger to spot errors.
However, writing generic functions that use operator overloading for our FHE types can be a bit trickier than normal, since, as explained in our Generic Bounds How To, FHE
types are not copy. Therefore, you will need to use the reference &
, even though you wouldn't normally use it when using native types, which are all Copy
.
This will make the generic bounds a bit trickier at first.
Our function has the following signature:
To make it generic, we can start by doing:
We now have to write the generic bounds: the where
clause.
In our function, we use the following operators:
!
(trait: Not
)
^
(trait: BitXor
)
We can add them to our where
, which would look like:
However, the compiler will complain:
fhe_bit
is a reference to a BoolType
(&BoolType
) since it is borrowed from the fhe_bits
slice when we iterate over its elements. We can try to change the BitXor
bounds to what the compiler suggests by requiring &BoolType
to implement BitXor
and not BoolType
.
The compiler is still not happy:
The way to fix this is to use Higher-Rank Trait Bounds
as shown in the Generic Bounds How To:
The final code will look like this:
Here is a complete example that uses this function for both clear and FHE values:
If you ever need to use a debugger, you can tell Cargo to generate debug info by adding in your TOML
release
modeDue to their nature, FHE types are slower than native types, so it is recommended to always build and run your project in release mode (cargo build --release
, cargo run --release
).
Another option that may improve performances is to enable fat
link time optimizations:
You should compare the run time with and without LTO to see if it improves performances.
With FHE types, the more precision you have, the more the computations are expensive. Therefore, it is important to choose the smallest type that can represent all your values.
concrete
gives the ability to create dynamic
types, that is, types that are created and customized at runtime to better fit your needs and try to gain performances. This feature can be a great option if for example, you only need 10 bits of precision and that concrete
does not expose an integer type with exactly 10 bits.
These methods return an encryptor
, which is the object you'll need to use to create values of your new type.
Types created dynamically still benefit from overloaded operators.
Creating a 10-bit integer by combining five 2-bit ShortInts
Another example using the CRT decomposition instead of the Radix one to represent 16-bit integers.
You can see our .
In the section, we explained that operators are overloaded to work with references and non-references.
If you wish to write generic functions which use operators with mixed reference and non-reference, it might get tricky at first to specify the trait . This page should serve as a cookbook to help you.
operation | trait bound |
---|
The for<'a>
syntax is something called , often shortened as HRTB
Creating a dynamic type is done by using the add_*
methods of the .
Kind | Builder method |
---|
|
|
|
|
|
|
|
|
There are two ways to contribute to Concrete tools in general:
you can open issues to report bugs and typos and to suggest ideas
you can ask to become an official contributor by emailing hello@zama.ai. Only approved contributors can send pull requests, so please make sure to get in touch before you do!
In this tutorial, we are going to build a data type that represents a Latin string in FHE while implementing the to_lower
and to_upper
functions.
The allowed characters in a Latin string are:
Uppercase letters: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
Lowercase letters: a b c d e f g h i j k l m n o p q r s t u v w x y z
For the code point of the letters, we will use the ascii
codes. In ascii:
The uppercase letters are in the range [65, 90]
The lowercase letters are in the range [97, 122]
lower_case
= upper_case
+ 32 <=> upper_case
= lower_case
- 32
For this type, we will use the FheUint8
type.
Our type will hold the encrypted characters as a Vec<FheUint8>
, as well as the encrypted constant 32
to implement our functions that change the case.
In the FheLatinString::encrypt
function, we have to make a bit of data validation:
The input string can only contain ascii letters (no digit, no special characters)
The input string cannot mix lower and upper case letters
These two points are to work around a limitation of FHE, which is that we cannot create branches, meaning our function cannot use conditional statements. For example, we can not check if the 'char' is a letter and uppercase to modify it to lowercase, like in the example below.
With these preconditions checked, implementing to_lower
and to_upper
is rather simple.
As we will be using the FheUint8
type, the integers
feature must be activated:
Booleans |
ShortInts |
Integers |
Native homomorphic booleans support common boolean operations.
The list of supported operations is:
Native small homomorphic integer types (e.g., FheUint3 or FheUint4) allow to easily compute various operations. In general, computing over encrypted data is as easy as computing over clear data, since the same operation symbol is used. For instance, the addition between two ciphertexts is done using the symbol +
between two FheUint. Similarly, many operations can be computed between a clear value (i.e. a scalar) and a ciphertext.
In Rust native types, any operation is modular. In Rust, u8
, computations are done modulus 2^8. The similar idea is applied for FheUintX, where operations are done modulus 2^X. For instance, in the type FheUint3, operations are done modulus 8.
Small homomorphic integer types support all common arithmetic operations, meaning +
, -
, x
, /
, mod
.
The division operation implements a subtlety: since data is encrypted, it might be possible to compute a division by 0. In this case, the division is tweaked so that dividing by 0 returns 0.
The list of supported operations is:
A simple example on how to use these operations:
Small homomorphic integer types support some bitwise operations.
The list of supported operations is:
A simple example on how to use these operations:
Small homomorphic integer types support comparison operations.
However, due to some Rust limitations, this is not possible to overload the comparison symbols because of the inner definition of the operations. To be precise, Rust expects to have a boolean as output, whereas a ciphertext encrypted the result is returned when using homomorphic types.
So instead of using symbols for the comparisons, you will need to use the different methods. These methods follow the same naming that the 2 standard Rust trait
A simple example on how to use these operations:
Shortints type also support the computation of univariate functions, which deep down uses TFHE's programmable bootstrapping.
A simple example on how to use these operations:
Using the shortint types offers the possibility to evaluate bivariate functions, i.e., functions that takes two ciphertexts as input.
In what follows, a simple code example:
In the same vein, native homomorphic types supports modular operations. At the moment, integers are more limited than shortint, but operations will be added soon.
Homomorphic integer types support arithmetic operations.
The list of supported operations is:
A simple example on how to use these operations:
Homomorphic integer types support some bitwise operations.
The list of supported operations is:
A simple example on how to use these operations:
As for shortints, homomorphic integers support the evaluation of univariate functions.
Here, an example on how to use this operation:
Bivariate function evaluations are now supported by integers.
If the precision is too large, this operation might fail due to an out of memory error
An example of this operation:
The basic steps for using concrete are the following:
importing concrete
configuring and creating keys
setting server key
encrypting data
computing over encrypted data
decrypting data
Here is the full example that we will walk through:
concrete
uses traits
to have a consistent API for creating FHE types and enable users to write generic functions. However, to be able to use associated functions and methods of a trait, the trait has to be in scope.
To make it easier for users, we use the prelude
'pattern'. That is, all concrete
important traits are in a prelude
module that you glob import. With this, there is no need to remember or know the traits to import.
The first step in your Rust code building is the creation of the configuration.
The configuration is used to declare which type you will use or not use, as well as enabling you to use custom crypto-parameters for these types for more advanced usage / testing.
In our example, we are interested in using 8-bit unsigned integers with default parameters. As per the table on the Getting Started page, we need to enable the integers
feature.
Next in our code, we create a config by first creating a builder with all types deactivated. Then, we enable the uint8
type with default parameters.
As the names try to convey, the client_key
is meant to stay private and not leave the client whereas the server_key
can be made public and sent to a server for it to enable FHE computations.
This function will move the server key to an internal state of the crate, allowing us to manage the details and give you, the user, a simpler interface.
Encrypting data is done via the encrypt
associated function of the [FheEncrypt] trait.
Types exposed by this crate will implement at least one of [FheEncrypt] or [FheTryEncrypt], to allow enryption.
Computations should be as easy as normal Rust to write, thanks to operator overloading.
The decryption is done by using the decrypt
method. (This method comes from the [FheDecrypt] trait).
name | symbol | type |
---|---|---|
name | symbol | type |
---|---|---|
name | symbol | type |
---|---|---|
name | symbol | type |
---|---|---|
name | symbol | type |
---|---|---|
Creating a configuration is done using the type.
The command returns a client key and a server key.
The next step is to call .
&
Binary
|
Binary
^
Binary
!
Unary
+
Binary
-
Binary
*
Binary
/
Binary
%
Binary
!
Unary
&
Binary
|
Binary
^
Binary
>>
Binary
<<
Binary
+
Binary
-
Binary
*
Binary
!
Unary
&
Binary
|
Binary
^
Binary
>>
Binary
<<
Binary