Skip to content

Commit aa1d7dc

Browse files
committed
feat(shortint): add PRF primitive with re-randomization
- required for some maths properties we need - limited API surface in shortint to have the minimum amount of functions to maintain - factorized the PRF inputs generation to share between rerand and non rerand version - seed creation returns the data hashed to represent the output random bits distribution so that it can be hashed for rerand as well - added test checking as many properties as possible verifying the rerand variant is strictly equivalent to the non rerand variant except for the bit content of the LWE - integer wrapping and HL to follow
1 parent 25d24bb commit aa1d7dc

5 files changed

Lines changed: 630 additions & 88 deletions

File tree

tfhe/src/high_level_api/integers/oprf.rs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -591,9 +591,12 @@ mod test {
591591
};
592592
use crate::prelude::FheDecrypt;
593593
use crate::shortint::oprf::test::test_uniformity;
594-
use crate::shortint::parameters::test_params::TEST_PARAM_MESSAGE_2_CARRY_2_PBS_KS_GAUSSIAN_2M128;
594+
use crate::shortint::parameters::test_params::{
595+
TEST_PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M128,
596+
TEST_PARAM_MESSAGE_2_CARRY_2_PBS_KS_GAUSSIAN_2M128,
597+
};
595598
use crate::shortint::parameters::PARAM_MESSAGE_2_CARRY_2_KS32_PBS_TUNIFORM_2M128;
596-
use crate::{generate_keys, set_server_key, ConfigBuilder, FheUint8, Seed};
599+
use crate::{generate_keys, set_server_key, ConfigBuilder, FheInt8, FheUint8, Seed};
597600
use num_bigint::BigUint;
598601
use rand::{thread_rng, Rng};
599602
use rayon::iter::{IntoParallelIterator, ParallelIterator};
@@ -890,6 +893,32 @@ mod test {
890893
assert!(result_bounded < (1 << 3));
891894
}
892895

896+
#[test]
897+
fn test_oprf_bounded_zero() {
898+
let config = ConfigBuilder::with_custom_parameters(
899+
TEST_PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M128,
900+
)
901+
.use_dedicated_oprf_key(true)
902+
.build();
903+
904+
let (client_key, server_key) = generate_keys(config);
905+
set_server_key(server_key);
906+
907+
// Do not use static seed in production
908+
let ct_unsigned_bounded = FheUint8::generate_oblivious_pseudo_random_bounded(Seed(0), 0);
909+
assert!(ct_unsigned_bounded.is_trivial());
910+
let result_unsigned_bounded: u8 = ct_unsigned_bounded.decrypt(&client_key);
911+
// PRF with 0 bits is equivalent to modulo 1 meaning only 0 is generated
912+
assert_eq!(result_unsigned_bounded, 0);
913+
914+
// Do not use static seed in production
915+
let ct_signed_bounded = FheInt8::generate_oblivious_pseudo_random_bounded(Seed(0), 0);
916+
assert!(ct_signed_bounded.is_trivial());
917+
let result_signed_bounded: i8 = ct_signed_bounded.decrypt(&client_key);
918+
// PRF with 0 bits is equivalent to modulo 1 meaning only 0 is generated
919+
assert_eq!(result_signed_bounded, 0);
920+
}
921+
893922
#[cfg(feature = "gpu")]
894923
mod gpu {
895924
use super::*;
@@ -904,6 +933,29 @@ mod test {
904933
use rayon::prelude::{IntoParallelRefIterator, ParallelSlice};
905934
use rayon::ThreadPoolBuilder;
906935

936+
#[test]
937+
fn test_oprf_bounded_zero() {
938+
for setup_fn in crate::high_level_api::integers::unsigned::tests::gpu::GPU_SETUP_FN {
939+
let client_key = setup_fn();
940+
941+
// Do not use static seed in production
942+
let ct_unsigned_bounded =
943+
FheUint8::generate_oblivious_pseudo_random_bounded(Seed(0), 0);
944+
assert!(ct_unsigned_bounded.is_trivial());
945+
let result_unsigned_bounded: u8 = ct_unsigned_bounded.decrypt(&client_key);
946+
// PRF with 0 bits is equivalent to modulo 1 meaning only 0 is generated
947+
assert_eq!(result_unsigned_bounded, 0);
948+
949+
// Do not use static seed in production
950+
let ct_signed_bounded =
951+
FheInt8::generate_oblivious_pseudo_random_bounded(Seed(0), 0);
952+
assert!(ct_signed_bounded.is_trivial());
953+
let result_signed_bounded: i8 = ct_signed_bounded.decrypt(&client_key);
954+
// PRF with 0 bits is equivalent to modulo 1 meaning only 0 is generated
955+
assert_eq!(result_signed_bounded, 0);
956+
}
957+
}
958+
907959
#[test]
908960
fn test_oprf_gpu() {
909961
for setup_fn in crate::high_level_api::integers::unsigned::tests::gpu::GPU_SETUP_FN {

tfhe/src/high_level_api/integers/signed/base.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,27 @@ where
912912
self.ciphertext.on_cpu().decrypt_trivial()
913913
}
914914

915+
/// Returns true if the ciphertext is a trivial encryption
916+
///
917+
/// # Example
918+
///
919+
/// ```rust
920+
/// use tfhe::prelude::*;
921+
/// use tfhe::{generate_keys, set_server_key, ConfigBuilder, FheInt16};
922+
///
923+
/// let (client_key, server_key) = generate_keys(ConfigBuilder::default());
924+
/// set_server_key(server_key);
925+
///
926+
/// let non_trivial = FheInt16::encrypt(1i16, &client_key);
927+
/// assert!(!non_trivial.is_trivial());
928+
///
929+
/// let trivial = FheInt16::encrypt_trivial(2i16);
930+
/// assert!(trivial.is_trivial());
931+
/// ```
932+
pub fn is_trivial(&self) -> bool {
933+
self.ciphertext.on_cpu().is_trivial()
934+
}
935+
915936
/// Reverse the bit of the signed integer
916937
///
917938
/// # Example

tfhe/src/integer/gpu/server_key/radix/oprf.rs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ where
176176
target_sks: &CudaServerKey,
177177
streams: &CudaStreams,
178178
) -> CudaUnsignedRadixCiphertext {
179+
assert!(target_sks.message_modulus.0.is_power_of_two());
179180
let message_bits_count = target_sks.message_modulus.0.ilog2() as u64;
180181
let range_bits_count = message_bits_count * num_blocks;
181182
assert!(range_bits_count > 0);
@@ -291,6 +292,7 @@ where
291292
target_sks: &CudaServerKey,
292293
streams: &CudaStreams,
293294
) -> CudaSignedRadixCiphertext {
295+
assert!(target_sks.message_modulus.0.is_power_of_two());
294296
let message_bits_count = target_sks.message_modulus.0.ilog2() as u64;
295297
let range_bits_count = message_bits_count * num_blocks;
296298
assert!(range_bits_count > 0);
@@ -388,9 +390,10 @@ where
388390
result
389391
}
390392

391-
// Core private implementation that calls the OPRF backend.
392-
// This function contains the main logic for both bounded and unbounded generation.
393-
//
393+
/// Core private implementation that calls the OPRF backend.
394+
/// This function contains the main logic for both bounded and unbounded generation.
395+
///
396+
/// Caller must ensure total_random_bits is non 0 otherwise this function will panic.
394397
fn generate_multiblocks_oblivious_pseudo_random(
395398
&self,
396399
result: &mut CudaRadixCiphertext,
@@ -412,14 +415,18 @@ where
412415
let polynomial_size = bootstrapping_key.polynomial_size();
413416
let in_lwe_size = input_lwe_dimension.to_lwe_size();
414417
let message_bits_count = target_sks.message_modulus.0.ilog2();
418+
let carry_bits_count = target_sks.carry_modulus.0.ilog2();
419+
let bits_per_block = message_bits_count + carry_bits_count + 1;
415420

416-
let seeded = create_random_from_seed_modulus_switched(
421+
let (seeded, _rle_info) = create_random_from_seed_modulus_switched(
417422
seed,
418423
in_lwe_size,
419424
polynomial_size,
420425
&[total_random_bits],
421-
message_bits_count as u64,
426+
message_bits_count.into(),
427+
bits_per_block.into(),
422428
);
429+
423430
let h_seeded_lwe_list: Vec<u64> = seeded
424431
.into_iter()
425432
.flat_map(|(seeded, _bits)| {
@@ -471,6 +478,13 @@ where
471478
}
472479
}
473480

481+
/// # Panics
482+
///
483+
/// Panics if:
484+
/// - `target_sks.message_modulus` is not a power of 2
485+
/// - `excluded_upper_bound` is a power of 2 use
486+
/// [`Self::par_generate_oblivious_pseudo_random_unsigned_integer_bounded`] instead
487+
/// - `excluded_upper_bound.ilog2() + 1` is greater than the output bit count
474488
pub fn par_generate_oblivious_pseudo_random_unsigned_custom_range(
475489
&self,
476490
seed: impl OprfSeed,
@@ -484,7 +498,13 @@ where
484498
target_sks.message_modulus.0.is_power_of_two(),
485499
"Message modulus must be a power of two"
486500
);
487-
let message_bits_count = target_sks.message_modulus.0.ilog2() as u64;
501+
assert!(
502+
target_sks.carry_modulus.0.is_power_of_two(),
503+
"Carry modulus must be a power of two"
504+
);
505+
let message_bits_count: u64 = target_sks.message_modulus.0.ilog2().into();
506+
let carry_bits_count: u64 = target_sks.carry_modulus.0.ilog2().into();
507+
let bits_per_block = message_bits_count + carry_bits_count + 1;
488508

489509
assert!(
490510
!excluded_upper_bound.is_power_of_two(),
@@ -528,12 +548,13 @@ where
528548
.iter_as::<u64>()
529549
.collect::<Vec<_>>();
530550

531-
let seeded = create_random_from_seed_modulus_switched(
551+
let (seeded, _rle_info) = create_random_from_seed_modulus_switched(
532552
seed,
533553
in_lwe_size,
534554
polynomial_size,
535555
&[num_input_random_bits],
536556
message_bits_count,
557+
bits_per_block,
537558
);
538559

539560
let h_seeded_lwe_list: Vec<u64> = seeded

tfhe/src/shortint/ciphertext/re_randomization.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ use crate::core_crypto::entities::{LweCiphertext, LweCompactCiphertextList, Plai
1313
use crate::core_crypto::prelude::lwe_compact_ciphertext_list_add_assign;
1414
use crate::shortint::ciphertext::NoiseLevel;
1515
use crate::shortint::key_switching_key::KeySwitchingKeyMaterialView;
16+
use crate::shortint::oprf::{OprfSeed, RandomBitsRleLeBytes};
17+
use crate::shortint::public_key::compact::TFHE_PKE_DOMAIN_SEPARATOR;
1618
use crate::shortint::{Ciphertext, CompactPublicKey, PBSOrder};
1719

1820
use rayon::prelude::*;
@@ -106,6 +108,22 @@ impl From<blake3::Hasher> for ReRandomizationSeedHasher {
106108
/// This type cannot be cloned or copied, as a seed should only be used once.
107109
pub struct ReRandomizationSeed(pub(crate) XofSeed);
108110

111+
impl ReRandomizationSeed {
112+
pub(crate) fn new_prf_rerand_seed(
113+
hash_algo: ReRandomizationHashAlgo,
114+
prf_seed: impl OprfSeed,
115+
random_bits_rle_bytes: &RandomBitsRleLeBytes,
116+
) -> Self {
117+
let mut seed_gen = ReRandomizationSeedGen::new_prf_rerand_seed_gen(
118+
hash_algo,
119+
prf_seed,
120+
random_bits_rle_bytes,
121+
);
122+
123+
seed_gen.next_seed()
124+
}
125+
}
126+
109127
/// The context that will be hashed and used to generate unique [`ReRandomizationSeed`].
110128
///
111129
/// At this level, the context will directly hash any data passed to it.
@@ -226,6 +244,27 @@ pub struct ReRandomizationSeedGen {
226244
}
227245

228246
impl ReRandomizationSeedGen {
247+
pub(crate) fn new_prf_rerand_seed_gen(
248+
hash_algo: ReRandomizationHashAlgo,
249+
prf_seed: impl OprfSeed,
250+
random_bits_rle_bytes: &RandomBitsRleLeBytes,
251+
) -> Self {
252+
const PRF_RERAND_DOMAIN_SEPARATOR: [u8; XofSeed::DOMAIN_SEP_LEN] = *b"PRF_RRND";
253+
254+
let mut context = ReRandomizationContext::new_with_hasher(
255+
TFHE_PKE_DOMAIN_SEPARATOR,
256+
ReRandomizationSeedHasher::new(hash_algo, PRF_RERAND_DOMAIN_SEPARATOR),
257+
);
258+
259+
let prf_seed = prf_seed.into_bytes();
260+
let prf_seed = prf_seed.as_ref();
261+
262+
context.add_bytes(prf_seed);
263+
context.add_bytes(random_bits_rle_bytes.get());
264+
265+
context.finalize()
266+
}
267+
229268
pub fn next_seed(&mut self) -> ReRandomizationSeed {
230269
let current_seed_index = self.next_seed_index;
231270
self.next_seed_index += 1;

0 commit comments

Comments
 (0)