Skip to content

Commit 9180bc6

Browse files
committed
feat(all): add seeded public encryption for compact lists
This allows to create CompactCiphertextList and ProvenCompactCiphertextList using a seed, so that the encryption can be reproduced * Follows NIST submission: - Create XofSeed from some seed bytes - Then init a NoiseRandomGenerator from the XofSeed - Use the gnerator to do the public encryption - When a zk proof is needed, for each chunk create the seed for the zk-proof by taking the next 16 bytes of noise_random_generator. This is custom to tfhe-rs as NIST submission does not cover this case * JS API + tests included * Backward compatibility tests Backward compatibility tests are included, as since this produces seeded data, we need to be able to guarantee backward compatibility.
1 parent 7d08b16 commit 9180bc6

File tree

34 files changed

+991
-156
lines changed

34 files changed

+991
-156
lines changed

tests/backward_compatibility/high_level_api.rs

Lines changed: 189 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ use tfhe_backward_compat_data::load::{
3232
use tfhe_backward_compat_data::{
3333
DataKind, HlBoolCiphertextTest, HlCiphertextTest, HlClientKeyTest, HlCompressedKVStoreTest,
3434
HlCompressedSquashedNoiseCiphertextListTest, HlCompressedXofKeySetTest,
35-
HlHeterogeneousCiphertextListTest, HlPublicKeyTest, HlServerKeyTest, HlSignedCiphertextTest,
36-
HlSquashedNoiseBoolCiphertextTest, HlSquashedNoiseSignedCiphertextTest,
37-
HlSquashedNoiseUnsignedCiphertextTest, TestMetadata, TestType, Testcase, ZkPkePublicParamsTest,
35+
HlHeterogeneousCiphertextListTest, HlPublicKeyTest, HlSeededCompactCiphertextListTest,
36+
HlServerKeyTest, HlSignedCiphertextTest, HlSquashedNoiseBoolCiphertextTest,
37+
HlSquashedNoiseSignedCiphertextTest, HlSquashedNoiseUnsignedCiphertextTest, TestMetadata,
38+
TestType, Testcase, ZkPkePublicParamsTest, ZkProofAuxiliaryInfo,
3839
};
3940
use tfhe_versionable::Unversionize;
4041

@@ -249,44 +250,206 @@ pub fn test_hl_heterogeneous_ciphertext_list_elements<CtList: CiphertextList>(
249250
key: &ClientKey,
250251
test: &HlHeterogeneousCiphertextListTest,
251252
) -> Result<(), String> {
252-
for idx in 0..(list.len()) {
253-
match test.data_kinds[idx] {
253+
verify_expanded_values(&list, &test.clear_values, &test.data_kinds, key)
254+
}
255+
256+
fn verify_expanded_values<CtList>(
257+
list: &CtList,
258+
clear_values: &[i64],
259+
data_kinds: &[DataKind],
260+
key: &ClientKey,
261+
) -> Result<(), String>
262+
where
263+
CtList: CiphertextList,
264+
{
265+
if clear_values.len() != data_kinds.len() {
266+
return Err(format!(
267+
"Number of clear values ({}) is not the same if the number of data kind ({})",
268+
clear_values.len(),
269+
data_kinds.len(),
270+
));
271+
}
272+
273+
if clear_values.len() != list.len() {
274+
return Err(format!(
275+
"Number of clear values ({}) is not the same if the number of values in the expander({})",
276+
clear_values.len(),
277+
list.len(),
278+
));
279+
}
280+
281+
for (idx, (value, kind)) in clear_values.iter().zip(data_kinds.iter()).enumerate() {
282+
match kind {
254283
DataKind::Bool => {
255-
let ct: FheBool = list.get(idx).unwrap().unwrap();
284+
let ct: FheBool = list
285+
.get(idx)
286+
.map_err(|e| format!("Failed to get bool at index {idx}: {e}"))?
287+
.ok_or_else(|| format!("No value at index {idx}"))?;
256288
let clear = ct.decrypt(key);
257-
if clear != (test.clear_values[idx] != 0) {
289+
if clear != (*value != 0) {
258290
return Err(format!(
259-
"Invalid decrypted cleartext:\n Expected :\n{:?}\nGot:\n{:?}",
260-
clear, test.clear_values[idx]
291+
"Invalid decrypted bool at index {idx}: expected {:?}, got {clear:?}",
292+
*value != 0
261293
));
262294
}
263295
}
264296
DataKind::Signed => {
265-
let ct: FheInt8 = list.get(idx).unwrap().unwrap();
297+
let ct: FheInt8 = list
298+
.get(idx)
299+
.map_err(|e| format!("Failed to get signed at index {idx}: {e}"))?
300+
.ok_or_else(|| format!("No value at index {idx}"))?;
266301
let clear: i8 = ct.decrypt(key);
267-
if clear != test.clear_values[idx] as i8 {
302+
if clear != *value as i8 {
268303
return Err(format!(
269-
"Invalid decrypted cleartext:\n Expected :\n{:?}\nGot:\n{:?}",
270-
clear,
271-
(test.clear_values[idx] as i8)
304+
"Invalid decrypted signed at index {idx}: expected {:?}, got {clear:?}",
305+
*value as i8
272306
));
273307
}
274308
}
275309
DataKind::Unsigned => {
276-
let ct: FheUint8 = list.get(idx).unwrap().unwrap();
310+
let ct: FheUint8 = list
311+
.get(idx)
312+
.map_err(|e| format!("Failed to get unsigned at index {idx}: {e}"))?
313+
.ok_or_else(|| format!("No value at index {idx}"))?;
277314
let clear: u8 = ct.decrypt(key);
278-
if clear != test.clear_values[idx] as u8 {
315+
if clear != *value as u8 {
279316
return Err(format!(
280-
"Invalid decrypted cleartext:\n Expected :\n{:?}\nGot:\n{:?}",
281-
clear, test.clear_values[idx]
317+
"Invalid decrypted unsigned at index {idx}: expected {:?}, got {clear:?}",
318+
*value as u8
282319
));
283320
}
284321
}
285-
};
322+
}
286323
}
287324
Ok(())
288325
}
289326

327+
/// Shared core for seeded compact ciphertext list backward compat tests.
328+
/// When `zk_proof_info` is `Some`, operates in ZK (proven) mode; otherwise plain mode.
329+
#[allow(clippy::too_many_arguments)]
330+
fn test_hl_seeded_compact_list_core<T: TestType>(
331+
dir: &Path,
332+
test: &T,
333+
format: DataFormat,
334+
key_filename: &str,
335+
public_key_filename: &str,
336+
clear_values: &[i64],
337+
data_kinds: &[DataKind],
338+
seed: &[u8],
339+
zk_proof_info: Option<&ZkProofAuxiliaryInfo>,
340+
) -> Result<TestSuccess, TestFailure> {
341+
#[cfg(not(feature = "zk-pok"))]
342+
if zk_proof_info.is_some() {
343+
return Ok(test.success(format));
344+
}
345+
346+
let key_file = dir.join(key_filename);
347+
let key = ClientKey::unversionize(
348+
load_versioned_auxiliary(key_file).map_err(|e| test.failure(e, format))?,
349+
)
350+
.map_err(|e| test.failure(e, format))?;
351+
352+
let server_key = key.generate_server_key();
353+
set_server_key(server_key);
354+
355+
let pubkey_file = dir.join(public_key_filename);
356+
let pubkey = CompactPublicKey::unversionize(
357+
load_versioned_auxiliary(pubkey_file).map_err(|e| test.failure(e, format))?,
358+
)
359+
.map_err(|e| test.failure(e, format))?;
360+
361+
let mut builder = CompactCiphertextList::builder(&pubkey);
362+
for (value, kind) in clear_values.iter().zip(data_kinds.iter()) {
363+
match kind {
364+
DataKind::Unsigned => {
365+
builder.push(*value as u8);
366+
}
367+
DataKind::Signed => {
368+
builder.push(*value as i8);
369+
}
370+
DataKind::Bool => {
371+
builder.push(*value != 0);
372+
}
373+
}
374+
}
375+
376+
if let Some(_proof_info) = zk_proof_info {
377+
#[cfg(feature = "zk-pok")]
378+
{
379+
use tfhe::zk::ZkComputeLoad;
380+
381+
let crs_file = dir.join(&*_proof_info.params_filename);
382+
let crs = CompactPkeCrs::unversionize(
383+
load_versioned_auxiliary(crs_file).map_err(|e| test.failure(e, format))?,
384+
)
385+
.map_err(|e| test.failure(e, format))?;
386+
387+
let pregenerated: ProvenCompactCiphertextList =
388+
load_and_unversionize(dir, test, format)?;
389+
390+
let rebuilt = builder
391+
.build_with_proof_packed_seeded(
392+
&crs,
393+
_proof_info.metadata.as_bytes(),
394+
ZkComputeLoad::Proof,
395+
seed,
396+
)
397+
.map_err(|e| test.failure(e, format))?;
398+
399+
if pregenerated != rebuilt {
400+
return Err(test.failure(
401+
"Seeded proven compact list rebuilt from values/seed does not match pregenerated",
402+
format,
403+
));
404+
}
405+
406+
let expanded = pregenerated
407+
.verify_and_expand(&crs, &pubkey, _proof_info.metadata.as_bytes())
408+
.map_err(|e| test.failure(e, format))?;
409+
verify_expanded_values(&expanded, clear_values, data_kinds, &key)
410+
.map_err(|e| test.failure(e, format))?;
411+
}
412+
} else {
413+
let pregenerated: CompactCiphertextList = load_and_unversionize(dir, test, format)?;
414+
415+
let rebuilt = builder.build_packed_seeded(seed).unwrap();
416+
417+
if pregenerated != rebuilt {
418+
return Err(test.failure(
419+
"Seeded compact list rebuilt from values/seed does not match pregenerated",
420+
format,
421+
));
422+
}
423+
424+
let expanded = pregenerated.expand().map_err(|e| test.failure(e, format))?;
425+
verify_expanded_values(&expanded, clear_values, data_kinds, &key)
426+
.map_err(|e| test.failure(e, format))?;
427+
}
428+
429+
Ok(test.success(format))
430+
}
431+
432+
/// Test seeded compact ciphertext list: loads pregenerated list, rebuilds from
433+
/// stored values/seed, asserts they match via PartialEq.
434+
pub fn test_hl_seeded_compact_ciphertext_list(
435+
dir: &Path,
436+
test: &HlSeededCompactCiphertextListTest,
437+
format: DataFormat,
438+
) -> Result<TestSuccess, TestFailure> {
439+
test_hl_seeded_compact_list_core(
440+
dir,
441+
test,
442+
format,
443+
&test.key_filename,
444+
&test.public_key_filename,
445+
&test.clear_values,
446+
&test.data_kinds,
447+
&test.seed,
448+
test.proof_info.as_ref(),
449+
)
450+
}
451+
452+
/// Test seeded proven compact ciphertext list
290453
/// Test HL client key: loads the key and checks the parameters using the values stored in
291454
/// the test metadata.
292455
pub fn test_hl_clientkey(
@@ -778,21 +941,20 @@ pub fn test_hl_compressed_squashed_noise_ciphertext_list(
778941
}
779942

780943
for i in 0..list.len() {
781-
let decrypted = match test.data_kinds[i] {
944+
let decrypted: i64 = match test.data_kinds[i] {
782945
DataKind::Unsigned => {
783946
let ct: SquashedNoiseFheUint = list.get(i).unwrap().unwrap();
784947
let clear: u64 = ct.decrypt(&key);
785-
clear
948+
clear as i64
786949
}
787950
DataKind::Signed => {
788951
let ct: SquashedNoiseFheInt = list.get(i).unwrap().unwrap();
789-
let clear: i64 = ct.decrypt(&key);
790-
clear as u64
952+
ct.decrypt(&key)
791953
}
792954
DataKind::Bool => {
793955
let ct: SquashedNoiseFheBool = list.get(i).unwrap().unwrap();
794956
let clear: bool = ct.decrypt(&key);
795-
clear as u64
957+
clear as i64
796958
}
797959
};
798960

@@ -953,6 +1115,9 @@ impl TestedModule for Hl {
9531115
TestMetadata::HlCompressedXofKeySet(test) => {
9541116
test_hl_compressed_xof_key_set_test(test_dir.as_ref(), test, format).into()
9551117
}
1118+
TestMetadata::HlSeededCompactCiphertextList(test) => {
1119+
test_hl_seeded_compact_ciphertext_list(test_dir.as_ref(), test, format).into()
1120+
}
9561121
_ => {
9571122
println!("WARNING: missing test: {:?}", testcase.metadata);
9581123
TestResult::Skipped(testcase.skip())

tfhe-zk-pok/src/curve_api.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -446,9 +446,9 @@ impl PairingGroupOps<bls12_446::Zp, bls12_446::G1, bls12_446::G2> for bls12_446:
446446

447447
// These are just ZSTs that are not actually produced and are only used for their
448448
// associated types. So it's ok to derive "NotVersioned" for them.
449-
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, NotVersioned)]
449+
#[derive(Debug, Copy, Clone, PartialEq, serde::Serialize, serde::Deserialize, NotVersioned)]
450450
pub struct Bls12_381;
451-
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, NotVersioned)]
451+
#[derive(Debug, Copy, Clone, PartialEq, serde::Serialize, serde::Deserialize, NotVersioned)]
452452
pub struct Bls12_446;
453453

454454
impl Curve for Bls12_381 {

tfhe-zk-pok/src/proofs/pke.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ impl<G: Curve> PublicParams<G> {
272272
}
273273
}
274274

275-
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Versionize)]
275+
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Versionize)]
276276
#[serde(bound(
277277
deserialize = "G: Curve, G::G1: serde::Deserialize<'de>, G::G2: serde::Deserialize<'de>",
278278
serialize = "G: Curve, G::G1: serde::Serialize, G::G2: serde::Serialize"
@@ -325,7 +325,7 @@ impl<G: Curve> Proof<G> {
325325

326326
/// These fields can be pre-computed on the prover side in the faster Verifier scheme. If that's the
327327
/// case, they should be included in the proof.
328-
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Versionize)]
328+
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Versionize)]
329329
#[serde(bound(
330330
deserialize = "G: Curve, G::G1: serde::Deserialize<'de>, G::G2: serde::Deserialize<'de>",
331331
serialize = "G: Curve, G::G1: serde::Serialize, G::G2: serde::Serialize"

tfhe-zk-pok/src/proofs/pke_v2/hashes.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ impl PkeV2HashConfig {
105105
/// List of hash config that were used for a given version of this crate
106106
///
107107
/// This is stored in the proof so that we only support a specific subset of all possible config.
108-
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, Versionize)]
108+
#[derive(Default, Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Versionize)]
109109
#[versionize(PkeV2SupportedHashConfigVersions)]
110110
pub enum PkeV2SupportedHashConfig {
111111
V0_4_0 = 0,

tfhe-zk-pok/src/proofs/pke_v2/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ impl<G: Curve> PublicParams<G> {
351351

352352
/// This represents a proof that the given ciphertext is a valid encryptions of the input messages
353353
/// with the provided public key.
354-
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Versionize)]
354+
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Versionize)]
355355
#[serde(bound(
356356
deserialize = "G: Curve, G::G1: serde::Deserialize<'de>, G::G2: serde::Deserialize<'de>",
357357
serialize = "G: Curve, G::G1: serde::Serialize, G::G2: serde::Serialize"
@@ -428,7 +428,7 @@ impl<G: Curve> Proof<G> {
428428

429429
/// These fields can be pre-computed on the prover side in the faster Verifier scheme. If that's the
430430
/// case, they should be included in the proof.
431-
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Versionize)]
431+
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, Versionize)]
432432
#[versionize(ComputeLoadProofFieldsVersions)]
433433
pub(crate) struct ComputeLoadProofFields<G: Curve> {
434434
pub(crate) C_hat_h3: G::G2,

0 commit comments

Comments
 (0)