Welcome to this tutorial about TFHE-rscore_crypto module.
Setting up TFHE-rs to use the core_crypto module
To use TFHE-rs, it first has to be added as a dependency in the Cargo.toml:
tfhe = { version ="0.8.7", features = ["x86_64-unix"] }
This enables the x86_64-unix feature to have efficient implementations of various algorithms for x86_64 CPUs on a Unix-like system. The 'unix' suffix indicates that the UnixSeeder, which uses /dev/random to generate random numbers, is activated as a fallback if no hardware number generator is available (like rdseed on x86_64 or if the Randomization Services on Apple platforms are not available). To avoid having the UnixSeeder as a potential fallback or to run on non-Unix systems (e.g., Windows), the x86_64 feature is sufficient.
For Apple Silicon, the aarch64-unix or aarch64 feature should be enabled. aarch64 is not supported on Windows as it's currently missing an entropy source required to seed the CSPRNGs used in TFHE-rs.
In short: For x86_64-based machines running Unix-like OSes:
tfhe = { version ="0.8.7", features = ["x86_64-unix"] }
For Apple Silicon or aarch64-based machines running Unix-like OSes:
tfhe = { version ="0.8.7", features = ["aarch64-unix"] }
tfhe = { version ="0.8.7", features = ["x86_64"] }
Commented code to double a 2-bit message in a leveled fashion and using a PBS with the core_crypto module.
As a complete example showing the usage of some common primitives of the core_crypto APIs, the following Rust code homomorphically computes 2 * 3 using two different methods. First using a cleartext multiplication and then using a PBS.
use tfhe::core_crypto::prelude::*;pubfnmain() {// DISCLAIMER: these toy example parameters are not guaranteed to be secure or yield correct// computations// Define the parameters for a 4 bits message able to hold the doubled 2 bits messagelet small_lwe_dimension =LweDimension(742);let glwe_dimension =GlweDimension(1);let polynomial_size =PolynomialSize(2048);let lwe_noise_distribution =Gaussian::from_dispersion_parameter(StandardDev(0.000007069849454709433), 0.0);let glwe_noise_distribution =Gaussian::from_dispersion_parameter(StandardDev(0.00000000000000029403601535432533), 0.0);let pbs_base_log =DecompositionBaseLog(23);let pbs_level =DecompositionLevelCount(1);let ciphertext_modulus =CiphertextModulus::new_native();// Request the best seeder possible, starting with hardware entropy sources and falling back to// /dev/random on Unix systems if enabled via cargo featuresletmut boxed_seeder =new_seeder();// Get a mutable reference to the seeder as a trait object from the Box returned by new_seederlet seeder = boxed_seeder.as_mut();// Create a generator which uses a CSPRNG to generate secret keysletmut secret_generator =SecretRandomGenerator::<ActivatedRandomGenerator>::new(seeder.seed());// Create a generator which uses two CSPRNGs to generate public masks and secret encryption// noiseletmut encryption_generator =EncryptionRandomGenerator::<ActivatedRandomGenerator>::new(seeder.seed(), seeder);println!("Generating keys...");// Generate an LweSecretKey with binary coefficientslet small_lwe_sk =LweSecretKey::generate_new_binary(small_lwe_dimension, &mut secret_generator);// Generate a GlweSecretKey with binary coefficientslet glwe_sk =GlweSecretKey::generate_new_binary(glwe_dimension, polynomial_size, &mut secret_generator);// Create a copy of the GlweSecretKey re-interpreted as an LweSecretKeylet big_lwe_sk = glwe_sk.clone().into_lwe_secret_key();// Generate the bootstrapping key, we use the parallel variant for performance reasonlet std_bootstrapping_key =par_allocate_and_generate_new_lwe_bootstrap_key(&small_lwe_sk,&glwe_sk, pbs_base_log, pbs_level, glwe_noise_distribution, ciphertext_modulus,&mut encryption_generator, );// Create the empty bootstrapping key in the Fourier domainletmut fourier_bsk =FourierLweBootstrapKey::new( std_bootstrapping_key.input_lwe_dimension(), std_bootstrapping_key.glwe_size(), std_bootstrapping_key.polynomial_size(), std_bootstrapping_key.decomposition_base_log(), std_bootstrapping_key.decomposition_level_count(), );// Use the conversion function (a memory optimized version also exists but is more complicated// to use) to convert the standard bootstrapping key to the Fourier domainconvert_standard_lwe_bootstrap_key_to_fourier(&std_bootstrapping_key, &mut fourier_bsk);// We don't need the standard bootstrapping key anymoredrop(std_bootstrapping_key);// Our 4 bits message spacelet message_modulus =1u64<<4;// Our input messagelet input_message =3u64;// Delta used to encode 4 bits of message + a bit of padding on u64let delta = (1_u64<<63) / message_modulus;// Apply our encodinglet plaintext =Plaintext(input_message * delta);// Allocate a new LweCiphertext and encrypt our plaintextlet lwe_ciphertext_in:LweCiphertextOwned<u64> =allocate_and_encrypt_new_lwe_ciphertext(&small_lwe_sk, plaintext, lwe_noise_distribution, ciphertext_modulus,&mut encryption_generator, );// Compute a cleartext multiplication by 2letmut cleartext_multiplication_ct = lwe_ciphertext_in.clone();println!("Performing cleartext multiplication...");lwe_ciphertext_cleartext_mul(&mut cleartext_multiplication_ct,&lwe_ciphertext_in,Cleartext(2), );// Decrypt the cleartext multiplication resultlet cleartext_multiplication_plaintext:Plaintext<u64> =decrypt_lwe_ciphertext(&small_lwe_sk, &cleartext_multiplication_ct);// Create a SignedDecomposer to perform the rounding of the decrypted plaintext// We pass a DecompositionBaseLog of 5 and a DecompositionLevelCount of 1 indicating we want to// round the 5 MSB, 1 bit of padding plus our 4 bits of messagelet signed_decomposer =SignedDecomposer::new(DecompositionBaseLog(5), DecompositionLevelCount(1));// Round and remove our encodinglet cleartext_multiplication_result:u64= signed_decomposer.closest_representable(cleartext_multiplication_plaintext.0) / delta;println!("Checking result...");assert_eq!(6, cleartext_multiplication_result);println!("Cleartext multiplication result is correct! \ Expected 6, got {cleartext_multiplication_result}" );// Now we will use a PBS to compute the same multiplication, it is NOT the recommended way of// doing this operation in terms of performance as it's much more costly than a multiplication// with a cleartext, however it resets the noise in a ciphertext to a nominal level and allows// to evaluate arbitrary functions so depending on your use case it can be a better fit.// Generate the accumulator for our multiplication by 2 using a simple closurelet accumulator:GlweCiphertextOwned<u64> =generate_programmable_bootstrap_glwe_lut( polynomial_size, glwe_dimension.to_glwe_size(), message_modulus asusize, ciphertext_modulus, delta,|x:u64|2* x, );// Allocate the LweCiphertext to store the result of the PBSletmut pbs_multiplication_ct =LweCiphertext::new(0u64, big_lwe_sk.lwe_dimension().to_lwe_size(), ciphertext_modulus, );println!("Computing PBS...");programmable_bootstrap_lwe_ciphertext(&lwe_ciphertext_in,&mut pbs_multiplication_ct,&accumulator,&fourier_bsk, );// Decrypt the PBS multiplication resultlet pbs_multiplication_plaintext:Plaintext<u64> =decrypt_lwe_ciphertext(&big_lwe_sk, &pbs_multiplication_ct);// Round and remove our encodinglet pbs_multiplication_result:u64= signed_decomposer.closest_representable(pbs_multiplication_plaintext.0) / delta;println!("Checking result...");assert_eq!(6, pbs_multiplication_result);println!("Multiplication via PBS result is correct! Expected 6, got {pbs_multiplication_result}" );}
Quick start
The core_crypto module from TFHE-rs is dedicated to the implementation of the cryptographic tools related to TFHE. To construct an FHE application, the and/or modules (based on core_crypto) are recommended.
The core_crypto module offers an API to low-level cryptographic primitives and objects, like lwe_encryption or rlwe_ciphertext. The goal is to propose an easy-to-use API for cryptographers.
The overall code architecture is split in two parts: one for entity definitions and another focused on algorithms. The entities contain the definition of useful types, like LWE ciphertext or bootstrapping keys. The algorithms are then naturally defined to work using these entities.
The API is convenient to add or modify existing algorithms, or to have direct access to the raw data. Even if the LWE ciphertext object is defined, along with functions giving access to the body, it is also possible to bypass these to get directly the element of LWE mask.
For instance, the code to encrypt and then decrypt a message looks like:
use tfhe::core_crypto::prelude::*;// DISCLAIMER: these toy example parameters are not guaranteed to be secure or yield correct// computations// Define parameters for LweCiphertext creationlet lwe_dimension =LweDimension(742);let lwe_noise_distribution =Gaussian::from_dispersion_parameter(StandardDev(0.000007069849454709433), 0.0);let ciphertext_modulus =CiphertextModulus::new_native();// Create the PRNGletmut seeder =new_seeder();let seeder = seeder.as_mut();letmut encryption_generator =EncryptionRandomGenerator::<ActivatedRandomGenerator>::new(seeder.seed(), seeder);letmut secret_generator =SecretRandomGenerator::<ActivatedRandomGenerator>::new(seeder.seed());// Create the LweSecretKeylet lwe_secret_key =allocate_and_generate_new_binary_lwe_secret_key(lwe_dimension, &mut secret_generator);// Create the plaintextlet msg =3u64;let plaintext =Plaintext(msg <<60);// Create a new LweCiphertextletmut lwe =LweCiphertext::new(0u64, lwe_dimension.to_lwe_size(), ciphertext_modulus);encrypt_lwe_ciphertext(&lwe_secret_key,&mut lwe, plaintext, lwe_noise_distribution,&mut encryption_generator,);let decrypted_plaintext =decrypt_lwe_ciphertext(&lwe_secret_key, &lwe);// Round and remove encoding// First create a decomposer working on the high 4 bits corresponding to our encoding.let decomposer =SignedDecomposer::new(DecompositionBaseLog(4), DecompositionLevelCount(1));let rounded = decomposer.closest_representable(decrypted_plaintext.0);// Remove the encodinglet cleartext = rounded >>60;// Check we recovered the original messageassert_eq!(cleartext, msg);