diff --git a/Cargo.toml b/Cargo.toml index 6c46148..e2c6b7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,9 @@ description = "Library for polymorphic encryption and pseudonymization" readme = "README.md" [features] -default = ["build-binary", "long", "offline", "batch", "serde", "json"] -serde = ["dep:serde"] # For (de)serialization support via Serde +default = ["build-binary", "long", "offline", "batch", "serde", "json", "verifiable"] +serde = ["dep:serde", "dep:serde_json"] # For (de)serialization support via Serde +verifiable = [] # Enable verifiable operations with zero-knowledge proofs elgamal3 = [] # For ElGamal triple encryption, including the recipient's public key in message encoding offline = [] # For encryption towards global keys (instead of encryption with session keys) batch = [] # For batch transcryption with reordering to prevent linkability diff --git a/benches/base.rs b/benches/base.rs index b854065..d3742e1 100644 --- a/benches/base.rs +++ b/benches/base.rs @@ -7,6 +7,12 @@ use libpep::core::primitives::{ }; use rand::rng; +#[cfg(feature = "verifiable")] +use libpep::core::proved::{ + PseudonymizationFactorCommitments, RSKFactorsProof, RekeyFactorCommitments, VerifiableRSK, + VerifiableRekey, VerifiableReshuffle, +}; + fn setup_keys() -> (ScalarNonZero, GroupElement) { let mut rng = rng(); let secret_key = ScalarNonZero::random(&mut rng); @@ -239,6 +245,170 @@ fn bench_rrsk2(c: &mut Criterion) { }); } +#[cfg(feature = "verifiable")] +fn bench_verifiable_reshuffle_create(c: &mut Criterion) { + c.bench_function("verifiable_reshuffle_create", |b| { + b.iter_batched( + || { + let (_, public_key) = setup_keys(); + let mut rng = rand::rng(); + let message = GroupElement::random(&mut rng); + let encrypted = encrypt(&message, &public_key, &mut rng); + let s = ScalarNonZero::random(&mut rng); + (encrypted, s, rng) + }, + |(encrypted, s, mut rng)| VerifiableReshuffle::new(&encrypted, &s, &mut rng), + criterion::BatchSize::SmallInput, + ) + }); +} + +#[cfg(feature = "verifiable")] +fn bench_verifiable_reshuffle_verify(c: &mut Criterion) { + c.bench_function("verifiable_reshuffle_verify", |b| { + b.iter_batched( + || { + let (_, public_key) = setup_keys(); + let mut rng = rand::rng(); + let message = GroupElement::random(&mut rng); + let encrypted = encrypt(&message, &public_key, &mut rng); + let s = ScalarNonZero::random(&mut rng); + let proof = VerifiableReshuffle::new(&encrypted, &s, &mut rng); + let result = reshuffle(&encrypted, &s); + let (commitments, _) = PseudonymizationFactorCommitments::new(&s, &mut rng); + (encrypted, result, proof, commitments) + }, + |(encrypted, result, proof, commitments)| { + proof.verify_reshuffle(&encrypted, &result, &commitments) + }, + criterion::BatchSize::SmallInput, + ) + }); +} + +#[cfg(feature = "verifiable")] +fn bench_verifiable_rekey_create(c: &mut Criterion) { + c.bench_function("verifiable_rekey_create", |b| { + b.iter_batched( + || { + let (_, public_key) = setup_keys(); + let mut rng = rand::rng(); + let message = GroupElement::random(&mut rng); + let encrypted = encrypt(&message, &public_key, &mut rng); + let k = ScalarNonZero::random(&mut rng); + (encrypted, k, rng) + }, + |(encrypted, k, mut rng)| VerifiableRekey::new(&encrypted, &k, &mut rng), + criterion::BatchSize::SmallInput, + ) + }); +} + +#[cfg(feature = "verifiable")] +fn bench_verifiable_rekey_verify(c: &mut Criterion) { + c.bench_function("verifiable_rekey_verify", |b| { + b.iter_batched( + || { + let (_, public_key) = setup_keys(); + let mut rng = rand::rng(); + let message = GroupElement::random(&mut rng); + let encrypted = encrypt(&message, &public_key, &mut rng); + let k = ScalarNonZero::random(&mut rng); + let proof = VerifiableRekey::new(&encrypted, &k, &mut rng); + let result = rekey(&encrypted, &k); + let (commitments, _) = RekeyFactorCommitments::new(&k, &mut rng); + (encrypted, result, proof, commitments) + }, + |(encrypted, result, proof, commitments)| { + proof.verify_rekey(&encrypted, &result, &commitments) + }, + criterion::BatchSize::SmallInput, + ) + }); +} + +#[cfg(feature = "verifiable")] +fn bench_verifiable_rsk_create(c: &mut Criterion) { + c.bench_function("verifiable_rsk_create", |b| { + b.iter_batched( + || { + let (_, public_key) = setup_keys(); + let mut rng = rand::rng(); + let message = GroupElement::random(&mut rng); + let encrypted = encrypt(&message, &public_key, &mut rng); + let s = ScalarNonZero::random(&mut rng); + let k = ScalarNonZero::random(&mut rng); + (encrypted, s, k, rng) + }, + |(encrypted, s, k, mut rng)| VerifiableRSK::new(&encrypted, &s, &k, &mut rng), + criterion::BatchSize::SmallInput, + ) + }); +} + +#[cfg(feature = "verifiable")] +fn bench_verifiable_rsk_verify(c: &mut Criterion) { + c.bench_function("verifiable_rsk_verify", |b| { + b.iter_batched( + || { + let (_, public_key) = setup_keys(); + let mut rng = rand::rng(); + let message = GroupElement::random(&mut rng); + let encrypted = encrypt(&message, &public_key, &mut rng); + let s = ScalarNonZero::random(&mut rng); + let k = ScalarNonZero::random(&mut rng); + let proof = VerifiableRSK::new(&encrypted, &s, &k, &mut rng); + let result = rsk(&encrypted, &s, &k); + let rsk_proof = RSKFactorsProof::new(&s, &k, &mut rng); + let (reshuffle_commitments, _) = + PseudonymizationFactorCommitments::new(&s, &mut rng); + let (rekey_commitments, _) = RekeyFactorCommitments::new(&k, &mut rng); + ( + encrypted, + result, + proof, + rsk_proof, + reshuffle_commitments, + rekey_commitments, + ) + }, + |(encrypted, result, proof, rsk_proof, reshuffle_commitments, rekey_commitments)| { + proof.verify_rsk( + &encrypted, + &result, + &rsk_proof, + &reshuffle_commitments, + &rekey_commitments, + ) + }, + criterion::BatchSize::SmallInput, + ) + }); +} + +#[cfg(feature = "verifiable")] +criterion_group!( + benches, + bench_encrypt, + bench_decrypt, + bench_rerandomize, + bench_reshuffle, + bench_rekey, + bench_rsk, + bench_rrsk, + bench_reshuffle2, + bench_rekey2, + bench_rsk2, + bench_rrsk2, + bench_verifiable_reshuffle_create, + bench_verifiable_reshuffle_verify, + bench_verifiable_rekey_create, + bench_verifiable_rekey_verify, + bench_verifiable_rsk_create, + bench_verifiable_rsk_verify +); + +#[cfg(not(feature = "verifiable"))] criterion_group!( benches, bench_encrypt, @@ -253,4 +423,5 @@ criterion_group!( bench_rsk2, bench_rrsk2 ); + criterion_main!(benches); diff --git a/benches/distributed.rs b/benches/distributed.rs index 9046b50..129a0bb 100644 --- a/benches/distributed.rs +++ b/benches/distributed.rs @@ -7,6 +7,13 @@ use libpep::factors::{EncryptionSecret, PseudonymizationSecret}; use libpep::transcryptor::DistributedTranscryptor; use rand::rng; +#[cfg(feature = "verifiable")] +use libpep::data::traits::{Pseudonymizable, VerifiablePseudonymizable, VerifiableRekeyable}; +#[cfg(feature = "verifiable")] +use libpep::transcryptor::Transcryptor; +#[cfg(feature = "verifiable")] +use libpep::verifier::Verifier; + /// Configuration parameters for distributed benchmarks pub const BENCHMARK_SERVERS: [usize; 4] = [1, 2, 3, 4]; pub const BENCHMARK_ENTITIES: [usize; 4] = [1, 10, 100, 1000]; @@ -293,6 +300,224 @@ fn bench_distributed_transcrypt_batch(c: &mut Criterion) { group.finish(); } +#[cfg(feature = "verifiable")] +#[allow(dead_code)] +fn bench_verifiable_commitment_generation(c: &mut Criterion) { + c.bench_function("verifiable_commitment_generation", |b| { + b.iter_batched( + || { + let rng = rand::rng(); + let ps_secret = PseudonymizationSecret::from(b"pseudonymization-secret".to_vec()); + let enc_secret = EncryptionSecret::from(b"encryption-secret".to_vec()); + let transcryptor = Transcryptor::new(ps_secret.clone(), enc_secret.clone()); + let domain_from = PseudonymizationDomain::from("domain-a"); + let domain_to = PseudonymizationDomain::from("domain-b"); + let session_from = EncryptionContext::from("session-a"); + let session_to = EncryptionContext::from("session-b"); + let info = transcryptor.pseudonymization_info( + &domain_from, + &domain_to, + &session_from, + &session_to, + ); + (info, rng) + }, + |(info, mut rng)| { + black_box(Transcryptor::pseudonymization_commitments(&info, &mut rng)) + }, + criterion::BatchSize::SmallInput, + ) + }); +} + +#[cfg(feature = "verifiable")] +#[allow(dead_code)] +fn bench_verifiable_commitment_verification(c: &mut Criterion) { + c.bench_function("verifiable_commitment_verification", |b| { + b.iter_batched( + || { + let mut rng = rand::rng(); + let ps_secret = PseudonymizationSecret::from(b"pseudonymization-secret".to_vec()); + let enc_secret = EncryptionSecret::from(b"encryption-secret".to_vec()); + let transcryptor = Transcryptor::new(ps_secret.clone(), enc_secret.clone()); + let domain_from = PseudonymizationDomain::from("domain-a"); + let domain_to = PseudonymizationDomain::from("domain-b"); + let session_from = EncryptionContext::from("session-a"); + let session_to = EncryptionContext::from("session-b"); + let info = transcryptor.pseudonymization_info( + &domain_from, + &domain_to, + &session_from, + &session_to, + ); + let commitments = Transcryptor::pseudonymization_commitments(&info, &mut rng); + let verifier = Verifier::new(); + (commitments, verifier) + }, + |(commitments, verifier)| { + black_box(verifier.verify_pseudonymization_commitments(&commitments)) + }, + criterion::BatchSize::SmallInput, + ) + }); +} + +#[cfg(feature = "verifiable")] +#[allow(dead_code)] +fn bench_verifiable_pseudonymization(c: &mut Criterion) { + c.bench_function("verifiable_pseudonymization", |b| { + b.iter_batched( + || { + let mut rng = rand::rng(); + let ps_secret = PseudonymizationSecret::from(b"pseudonymization-secret".to_vec()); + let enc_secret = EncryptionSecret::from(b"encryption-secret".to_vec()); + let transcryptor = Transcryptor::new(ps_secret.clone(), enc_secret.clone()); + let domain_from = PseudonymizationDomain::from("domain-a"); + let domain_to = PseudonymizationDomain::from("domain-b"); + let session_from = EncryptionContext::from("session-a"); + let session_to = EncryptionContext::from("session-b"); + + // Create distributed client and encrypt pseudonym + let (_global_pub, blinded_keys, blinding_factors) = + libpep::keys::distribution::make_distributed_global_keys(1, &mut rng); + let dis_transcryptor = DistributedTranscryptor::new( + ps_secret.clone(), + enc_secret.clone(), + blinding_factors[0], + ); + let sks = dis_transcryptor.session_key_shares(&session_from); + let client = Client::from_shares(blinded_keys, &[sks]); + let pseudonym = Pseudonym::random(&mut rng); + let encrypted = client.encrypt(&pseudonym, &mut rng); + + let info = transcryptor.pseudonymization_info( + &domain_from, + &domain_to, + &session_from, + &session_to, + ); + (encrypted, info, rng) + }, + |(encrypted, info, mut rng)| { + black_box(encrypted.verifiable_pseudonymize(&info, &mut rng)) + }, + criterion::BatchSize::SmallInput, + ) + }); +} + +#[cfg(feature = "verifiable")] +#[allow(dead_code)] +fn bench_verifiable_rekey(c: &mut Criterion) { + c.bench_function("verifiable_rekey", |b| { + b.iter_batched( + || { + let mut rng = rand::rng(); + let ps_secret = PseudonymizationSecret::from(b"pseudonymization-secret".to_vec()); + let enc_secret = EncryptionSecret::from(b"encryption-secret".to_vec()); + let transcryptor = Transcryptor::new(ps_secret.clone(), enc_secret.clone()); + let session_from = EncryptionContext::from("session-a"); + let session_to = EncryptionContext::from("session-b"); + + // Create distributed client and encrypt attribute + let (_global_pub, blinded_keys, blinding_factors) = + libpep::keys::distribution::make_distributed_global_keys(1, &mut rng); + let dis_transcryptor = DistributedTranscryptor::new( + ps_secret.clone(), + enc_secret.clone(), + blinding_factors[0], + ); + let sks = dis_transcryptor.session_key_shares(&session_from); + let client = Client::from_shares(blinded_keys, &[sks]); + let attribute = Attribute::random(&mut rng); + let encrypted = client.encrypt(&attribute, &mut rng); + + let info = transcryptor.attribute_rekey_info(&session_from, &session_to); + (encrypted, info, rng) + }, + |(encrypted, info, mut rng)| black_box(encrypted.verifiable_rekey(&info, &mut rng)), + criterion::BatchSize::SmallInput, + ) + }); +} + +#[cfg(feature = "verifiable")] +#[allow(dead_code)] +fn bench_verifiable_pseudonymization_verify(c: &mut Criterion) { + c.bench_function("verifiable_pseudonymization_verify", |b| { + b.iter_batched( + || { + let mut rng = rand::rng(); + let ps_secret = PseudonymizationSecret::from(b"pseudonymization-secret".to_vec()); + let enc_secret = EncryptionSecret::from(b"encryption-secret".to_vec()); + let transcryptor = Transcryptor::new(ps_secret.clone(), enc_secret.clone()); + let domain_from = PseudonymizationDomain::from("domain-a"); + let domain_to = PseudonymizationDomain::from("domain-b"); + let session_from = EncryptionContext::from("session-a"); + let session_to = EncryptionContext::from("session-b"); + + // Create distributed client and encrypt pseudonym + let (_global_pub, blinded_keys, blinding_factors) = + libpep::keys::distribution::make_distributed_global_keys(1, &mut rng); + let dis_transcryptor = DistributedTranscryptor::new( + ps_secret.clone(), + enc_secret.clone(), + blinding_factors[0], + ); + let sks = dis_transcryptor.session_key_shares(&session_from); + let client = Client::from_shares(blinded_keys, &[sks]); + let pseudonym = Pseudonym::random(&mut rng); + let encrypted = client.encrypt(&pseudonym, &mut rng); + + let info = transcryptor.pseudonymization_info( + &domain_from, + &domain_to, + &session_from, + &session_to, + ); + + let operation_proof = encrypted.verifiable_pseudonymize(&info, &mut rng); + let factors_proof = Transcryptor::pseudonymization_factors_proof(&info, &mut rng); + let result = encrypted.pseudonymize(&info); + let commitments = Transcryptor::pseudonymization_commitments(&info, &mut rng); + let verifier = Verifier::new(); + + ( + encrypted, + result, + operation_proof, + factors_proof, + commitments, + verifier, + ) + }, + |(original, result, operation_proof, factors_proof, commitments, verifier)| { + black_box(verifier.verify_pseudonymization( + &original, + &result, + &operation_proof, + &factors_proof, + &commitments, + )) + }, + criterion::BatchSize::SmallInput, + ) + }); +} + +#[cfg(feature = "verifiable")] +criterion_group!( + benches, + bench_distributed_transcrypt, + bench_distributed_transcrypt_batch, + bench_verifiable_commitment_generation, + bench_verifiable_commitment_verification, + bench_verifiable_pseudonymization, + bench_verifiable_rekey, + bench_verifiable_pseudonymization_verify +); + +#[cfg(not(feature = "verifiable"))] criterion_group!( benches, bench_distributed_transcrypt, diff --git a/benches/quick.rs b/benches/quick.rs index d70e54b..781f986 100644 --- a/benches/quick.rs +++ b/benches/quick.rs @@ -16,6 +16,11 @@ use libpep::data::long::{LongAttribute, LongPseudonym}; #[cfg(feature = "json")] use libpep::data::json::PEPJSONValue; +#[cfg(feature = "verifiable")] +use libpep::data::traits::VerifiablePseudonymizable; +#[cfg(feature = "verifiable")] +use libpep::transcryptor::Transcryptor; + const NUM_ITEMS: usize = 100; const NUM_TRANSCRYPTORS: usize = 2; @@ -372,7 +377,65 @@ fn bench_json_roundtrip_batch(c: &mut Criterion) { }); } -#[cfg(all(feature = "long", feature = "json", feature = "batch"))] +#[cfg(feature = "verifiable")] +fn bench_verifiable_pseudonymization_quick(c: &mut Criterion) { + let (_systems, client_a, _client_b, session_a, session_b, domain_a, domain_b) = setup_system(); + let rng_setup = &mut rng(); + + // Create a single transcryptor for verifiable operations + let ps_secret = PseudonymizationSecret::from(b"ps-0".to_vec()); + let enc_secret = EncryptionSecret::from(b"es-0".to_vec()); + let transcryptor = Transcryptor::new(ps_secret, enc_secret); + + // Pre-generate pseudonyms + let pseudonyms: Vec<_> = (0..NUM_ITEMS) + .map(|_| Pseudonym::random(rng_setup)) + .collect(); + let encrypted: Vec<_> = pseudonyms + .iter() + .map(|p| client_a.encrypt(p, rng_setup)) + .collect(); + + let info = transcryptor.pseudonymization_info(&domain_a, &domain_b, &session_a, &session_b); + + c.bench_function("verifiable_pseudonymization_quick_100", |b| { + b.iter(|| { + let rng = &mut rng(); + for enc in &encrypted { + let proof = enc.verifiable_pseudonymize(&info, rng); + black_box(proof); + } + }) + }); +} + +#[cfg(all( + feature = "long", + feature = "json", + feature = "batch", + feature = "verifiable" +))] +criterion_group!( + benches, + bench_pseudonym_roundtrip, + bench_pseudonym_roundtrip_batch, + bench_attribute_roundtrip, + bench_attribute_roundtrip_batch, + bench_long_pseudonym_roundtrip, + bench_long_pseudonym_roundtrip_batch, + bench_long_attribute_roundtrip, + bench_long_attribute_roundtrip_batch, + bench_json_roundtrip, + bench_json_roundtrip_batch, + bench_verifiable_pseudonymization_quick +); + +#[cfg(all( + feature = "long", + feature = "json", + feature = "batch", + not(feature = "verifiable") +))] criterion_group!( benches, bench_pseudonym_roundtrip, @@ -387,7 +450,28 @@ criterion_group!( bench_json_roundtrip_batch ); -#[cfg(all(feature = "long", feature = "json", not(feature = "batch")))] +#[cfg(all( + feature = "long", + feature = "json", + not(feature = "batch"), + feature = "verifiable" +))] +criterion_group!( + benches, + bench_pseudonym_roundtrip, + bench_attribute_roundtrip, + bench_long_pseudonym_roundtrip, + bench_long_attribute_roundtrip, + bench_json_roundtrip, + bench_verifiable_pseudonymization_quick +); + +#[cfg(all( + feature = "long", + feature = "json", + not(feature = "batch"), + not(feature = "verifiable") +))] criterion_group!( benches, bench_pseudonym_roundtrip, @@ -397,6 +481,7 @@ criterion_group!( bench_json_roundtrip ); +// All remaining combinations - without verifiable for simplicity #[cfg(all(feature = "long", feature = "batch", not(feature = "json")))] criterion_group!( benches, diff --git a/src/lib/core/mod.rs b/src/lib/core/mod.rs index b23265d..11a5c23 100644 --- a/src/lib/core/mod.rs +++ b/src/lib/core/mod.rs @@ -13,3 +13,8 @@ pub mod py; #[cfg(feature = "wasm")] pub mod wasm; + +#[cfg(feature = "verifiable")] +pub mod proved; +#[cfg(feature = "verifiable")] +pub mod zkps; diff --git a/src/lib/core/proved/commitments.rs b/src/lib/core/proved/commitments.rs new file mode 100644 index 0000000..f2a16ab --- /dev/null +++ b/src/lib/core/proved/commitments.rs @@ -0,0 +1,92 @@ +use crate::arithmetic::group_elements::{GroupElement, G}; +use crate::arithmetic::scalars::ScalarNonZero; +use crate::core::zkps::{create_proof, verify_proof, Proof}; +use derive_more::{Deref, From}; +use rand_core::{CryptoRng, RngCore}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[derive(Eq, PartialEq, Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct FactorCommitments { + pub val: GroupElement, + pub inv: GroupElement, +} + +#[derive(Eq, PartialEq, Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct FactorCommitmentsProof(Proof, ScalarNonZero, GroupElement); + +impl FactorCommitments { + pub fn new( + a: &ScalarNonZero, + rng: &mut R, + ) -> (Self, FactorCommitmentsProof) { + let r = ScalarNonZero::random(rng); + let gra = a * r * G; + let (gai, pai) = create_proof(&a.invert(), &gra, rng); + // Checking pki.n == gr proves that a.invert()*a == 1. + // Assume a'^-1 * (a*r*G) = r*G, then a = a' trivially holds for any a, a', r + ( + Self { + val: a * G, + inv: gai, + }, + FactorCommitmentsProof(pai, r, gra), + ) + } + + pub fn reversed(&self) -> Self { + Self { + val: self.inv, + inv: self.val, + } + } +} + +impl FactorCommitmentsProof { + #[must_use] + pub fn verify(&self, commitments: &FactorCommitments) -> bool { + let FactorCommitments { val: ga, inv: gai } = commitments; + verify_proof(gai, &self.2, &self.0) && self.0.n == self.1 * G && self.1 * ga == self.2 + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug, Deref, From)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct RekeyFactorCommitments(pub(crate) FactorCommitments); + +#[derive(Copy, Clone, Eq, PartialEq, Debug, Deref, From)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct PseudonymizationFactorCommitments(pub(crate) FactorCommitments); + +#[derive(Copy, Clone, Eq, PartialEq, Debug, Deref, From)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct RekeyFactorCommitmentsProof(pub(crate) FactorCommitmentsProof); + +#[derive(Copy, Clone, Eq, PartialEq, Debug, Deref, From)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct PseudonymizationFactorCommitmentsProof(pub(crate) FactorCommitmentsProof); + +impl RekeyFactorCommitments { + pub fn new( + a: &ScalarNonZero, + rng: &mut R, + ) -> (Self, RekeyFactorCommitmentsProof) { + let (commitments, proof) = FactorCommitments::new(a, rng); + (Self(commitments), RekeyFactorCommitmentsProof(proof)) + } +} + +impl PseudonymizationFactorCommitments { + pub fn new( + a: &ScalarNonZero, + rng: &mut R, + ) -> (Self, PseudonymizationFactorCommitmentsProof) { + let (commitments, proof) = FactorCommitments::new(a, rng); + ( + Self(commitments), + PseudonymizationFactorCommitmentsProof(proof), + ) + } +} diff --git a/src/lib/core/proved/mod.rs b/src/lib/core/proved/mod.rs new file mode 100644 index 0000000..ea4bf6c --- /dev/null +++ b/src/lib/core/proved/mod.rs @@ -0,0 +1,22 @@ +mod commitments; +mod rekey; +#[cfg(all(feature = "elgamal3", feature = "insecure"))] +mod rerandomize; +mod reshuffle; +mod rsk; + +pub use commitments::{ + FactorCommitments, FactorCommitmentsProof, PseudonymizationFactorCommitments, + PseudonymizationFactorCommitmentsProof, RekeyFactorCommitments, RekeyFactorCommitmentsProof, +}; + +#[cfg(all(feature = "elgamal3", feature = "insecure"))] +pub use rerandomize::{verifiable_rerandomize, VerifiableRerandomize}; + +pub use reshuffle::{ + verifiable_reshuffle, verifiable_reshuffle2, Reshuffle2FactorsProof, VerifiableReshuffle, +}; + +pub use rekey::{verifiable_rekey, verifiable_rekey2, Rekey2FactorsProof, VerifiableRekey}; + +pub use rsk::{verifiable_rsk, verifiable_rsk2, RSK2FactorsProof, RSKFactorsProof, VerifiableRSK}; diff --git a/src/lib/core/proved/rekey.rs b/src/lib/core/proved/rekey.rs new file mode 100644 index 0000000..2749f48 --- /dev/null +++ b/src/lib/core/proved/rekey.rs @@ -0,0 +1,285 @@ +use super::commitments::RekeyFactorCommitments; +use crate::arithmetic::group_elements::{GroupElement, G}; +use crate::arithmetic::scalars::ScalarNonZero; +use crate::core::elgamal::ElGamal; +use crate::core::zkps::{create_proof, verify_proof, Proof}; +use rand_core::{CryptoRng, RngCore}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Second GroupElement is `k*G` if verifiable_rekey with `k` is called. +#[derive(Eq, PartialEq, Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct VerifiableRekey { + pub pb: Proof, + #[cfg(feature = "elgamal3")] + pub py: Proof, +} + +impl VerifiableRekey { + pub fn new(v: &ElGamal, k: &ScalarNonZero, rng: &mut R) -> Self { + // Rekey is normally {in.b/k, in.c, k*in.y}; + let (_, pb) = create_proof(&k.invert(), &v.gb, rng); + #[cfg(feature = "elgamal3")] + let (_, py) = create_proof(k, &v.gy, rng); + Self { + pb, + #[cfg(feature = "elgamal3")] + py, + } + } + pub fn verified_reconstruct( + &self, + original: &ElGamal, + commitments: &RekeyFactorCommitments, + ) -> Option { + if self.verify(original, commitments) { + Some(self.reconstruct(original)) + } else { + None + } + } + /// Extract the result of the rekey operation from the proof. + /// + /// This allows getting the transformed ElGamal value without duplicating data. + /// The `gc` component is taken from the original since it doesn't change during rekey. + pub fn result(&self, original: &ElGamal) -> ElGamal { + ElGamal { + gb: *self.pb, + gc: original.gc, + #[cfg(feature = "elgamal3")] + gy: *self.py, + } + } + + fn reconstruct(&self, original: &ElGamal) -> ElGamal { + self.result(original) + } + #[cfg(feature = "insecure")] + pub fn unverified_reconstruct(&self, original: &ElGamal) -> ElGamal { + self.reconstruct(original) + } + #[must_use] + fn verify(&self, original: &ElGamal, commitments: &RekeyFactorCommitments) -> bool { + #[cfg(feature = "elgamal3")] + return Self::verify_split( + &original.gb, + &original.gc, + &original.gy, + &commitments.0.val, + &commitments.0.inv, + &self.pb, + &self.py, + ); + #[cfg(not(feature = "elgamal3"))] + Self::verify_split( + &original.gb, + &original.gc, + &commitments.0.val, + &commitments.0.inv, + &self.pb, + ) + } + #[must_use] + pub fn verify_rekey( + &self, + original: &ElGamal, + new: &ElGamal, + commitments: &RekeyFactorCommitments, + ) -> bool { + #[cfg(feature = "elgamal3")] + return self.verify(original, commitments) + && new.gb == *self.pb + && new.gc == original.gc + && new.gy == *self.py; + #[cfg(not(feature = "elgamal3"))] + return self.verify(original, commitments) && new.gb == *self.pb && new.gc == original.gc; + } + #[cfg(feature = "elgamal3")] + #[must_use] + fn verify_split( + gb: &GroupElement, + _gc: &GroupElement, + gy: &GroupElement, + gk: &GroupElement, + gki: &GroupElement, + pb: &Proof, + py: &Proof, + ) -> bool { + verify_proof(gki, gb, pb) && verify_proof(gk, gy, py) + } + #[cfg(not(feature = "elgamal3"))] + #[must_use] + fn verify_split( + gb: &GroupElement, + _gc: &GroupElement, + _gk: &GroupElement, + gki: &GroupElement, + pb: &Proof, + ) -> bool { + verify_proof(gki, gb, pb) + } + + // Rekey2 methods + pub fn new2( + v: &ElGamal, + from: &ScalarNonZero, + to: &ScalarNonZero, + rng: &mut R, + ) -> Self { + // Rekey2 is normally {k_from * k_to^-1 * in.B, in.c, k_from^-1 * k_to * in.y}; + let k_from_inv = from.invert(); + let k = k_from_inv * to; + let (_gki, pb) = create_proof(&k.invert(), &v.gb, rng); + #[cfg(feature = "elgamal3")] + let (_gk, py) = create_proof(&k, &v.gy, rng); + Self { + pb, + #[cfg(feature = "elgamal3")] + py, + } + } + pub fn verified_reconstruct2( + &self, + original: &ElGamal, + rekey2_proof: &Rekey2FactorsProof, + ) -> Option { + if self.verify2(original, rekey2_proof) { + Some(self.reconstruct(original)) + } else { + None + } + } + #[must_use] + fn verify2(&self, original: &ElGamal, rekey2_proof: &Rekey2FactorsProof) -> bool { + #[cfg(feature = "elgamal3")] + return Self::verify_split2( + &original.gb, + &original.gc, + &original.gy, + &self.pb, + &self.py, + rekey2_proof, + ); + #[cfg(not(feature = "elgamal3"))] + Self::verify_split2(&original.gb, &original.gc, &self.pb, rekey2_proof) + } + #[must_use] + pub fn verify_rekey2( + &self, + original: &ElGamal, + new: &ElGamal, + rekey2_proof: &Rekey2FactorsProof, + ) -> bool { + #[cfg(feature = "elgamal3")] + return self.verify2(original, rekey2_proof) + && new.gb == *self.pb + && new.gy == *self.py + && new.gc == original.gc; + #[cfg(not(feature = "elgamal3"))] + return self.verify2(original, rekey2_proof) && new.gb == *self.pb && new.gc == original.gc; + } + #[cfg(feature = "elgamal3")] + #[must_use] + fn verify_split2( + gb: &GroupElement, + _gc: &GroupElement, + gy: &GroupElement, + pb: &Proof, + py: &Proof, + rekey2_proof: &Rekey2FactorsProof, + ) -> bool { + verify_proof(&rekey2_proof.pki, gb, pb) && verify_proof(&rekey2_proof.pk, gy, py) + } + #[cfg(not(feature = "elgamal3"))] + #[must_use] + fn verify_split2( + gb: &GroupElement, + _gc: &GroupElement, + pb: &Proof, + rekey2_proof: &Rekey2FactorsProof, + ) -> bool { + verify_proof(&rekey2_proof.pki, gb, pb) + } +} + +#[derive(Eq, PartialEq, Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Rekey2FactorsProof { + #[cfg(feature = "elgamal3")] + pub pk: Proof, + pub pki: Proof, +} + +impl Rekey2FactorsProof { + pub fn new( + from: &ScalarNonZero, + to: &ScalarNonZero, + rng: &mut R, + ) -> Self { + #[cfg(feature = "elgamal3")] + let (_gk_to, pk) = create_proof(&from.invert(), &(to * G), rng); + let (_gki_to, pki) = create_proof(from, &(to.invert() * G), rng); + Self { + #[cfg(feature = "elgamal3")] + pk, + pki, + } + } + #[cfg(feature = "elgamal3")] + pub fn from_proof( + pk: &Proof, + pki: &Proof, + commitments_from: &RekeyFactorCommitments, + commitments_to: &RekeyFactorCommitments, + ) -> Option { + let x = Self { pk: *pk, pki: *pki }; + if x.verify(commitments_from, commitments_to) { + Some(x) + } else { + None + } + } + #[cfg(not(feature = "elgamal3"))] + pub fn from_proof( + pki: &Proof, + commitments_from: &RekeyFactorCommitments, + commitments_to: &RekeyFactorCommitments, + ) -> Option { + let x = Self { pki: *pki }; + if x.verify(commitments_from, commitments_to) { + Some(x) + } else { + None + } + } + #[must_use] + pub fn verify( + &self, + commitments_from: &RekeyFactorCommitments, + commitments_to: &RekeyFactorCommitments, + ) -> bool { + #[cfg(feature = "elgamal3")] + return verify_proof(&commitments_from.0.inv, &commitments_to.0.val, &self.pk) + && verify_proof(&commitments_from.0.val, &commitments_to.0.inv, &self.pki); + #[cfg(not(feature = "elgamal3"))] + verify_proof(&commitments_from.0.val, &commitments_to.0.inv, &self.pki) + } +} + +pub fn verifiable_rekey( + m: &ElGamal, + k: &ScalarNonZero, + rng: &mut R, +) -> VerifiableRekey { + VerifiableRekey::new(m, k, rng) +} + +pub fn verifiable_rekey2( + m: &ElGamal, + from: &ScalarNonZero, + to: &ScalarNonZero, + rng: &mut R, +) -> VerifiableRekey { + VerifiableRekey::new2(m, from, to, rng) +} diff --git a/src/lib/core/proved/rerandomize.rs b/src/lib/core/proved/rerandomize.rs new file mode 100644 index 0000000..03788ff --- /dev/null +++ b/src/lib/core/proved/rerandomize.rs @@ -0,0 +1,68 @@ +use crate::arithmetic::group_elements::GroupElement; +use crate::arithmetic::scalars::ScalarNonZero; +use crate::core::elgamal::ElGamal; +use crate::core::zkps::{create_proof, verify_proof, Proof}; +use rand_core::{CryptoRng, RngCore}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// RERANDOMIZE +// We are re-using some variables from the Proof to reconstruct the Rerandomize operation. +// This way, we only need 1 Proof object (which are fairly large) +#[derive(Eq, PartialEq, Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct VerifiableRerandomize(GroupElement, Proof); + +impl VerifiableRerandomize { + pub fn new(original: &ElGamal, r: &ScalarNonZero, rng: &mut R) -> Self { + // Rerandomize is normally {r * G + in.b, r*in.y + in.c, in.y}; + let (gr, p) = create_proof(r, &original.gy, rng); + Self(gr, p) + } + pub fn verified_reconstruct(&self, original: &ElGamal) -> Option { + if self.verify(original) { + Some(self.reconstruct(original)) + } else { + None + } + } + fn reconstruct(&self, original: &ElGamal) -> ElGamal { + ElGamal { + gb: self.0 + original.gb, + gc: *self.1 + original.gc, + gy: original.gy, + } + } + #[must_use] + fn verify(&self, original: &ElGamal) -> bool { + Self::verify_split(&original.gb, &original.gc, &original.gy, &self.0, &self.1) + } + #[must_use] + pub fn verify_rerandomized(&self, original: &ElGamal, new: &ElGamal) -> bool { + self.verify(original) + && new.gb == self.0 + original.gb + && new.gc == *self.1 + original.gc + && new.gy == original.gy + } + #[must_use] + fn verify_split( + _gb: &GroupElement, + _gc: &GroupElement, + gy: &GroupElement, + gr: &GroupElement, + p: &Proof, + ) -> bool { + // slightly different from the others, as we reuse the structure of a standard proof to reconstruct the Rerandomize operation after sending + verify_proof(gr, gy, p) + } +} + +pub fn verifiable_rerandomize( + original: &ElGamal, + r: &ScalarNonZero, + rng: &mut R, +) -> (ElGamal, VerifiableRerandomize) { + let verifiable_rerandomize = VerifiableRerandomize::new(original, r, rng); + let rerandomized = verifiable_rerandomize.reconstruct(original); + (rerandomized, verifiable_rerandomize) +} diff --git a/src/lib/core/proved/reshuffle.rs b/src/lib/core/proved/reshuffle.rs new file mode 100644 index 0000000..a9250c4 --- /dev/null +++ b/src/lib/core/proved/reshuffle.rs @@ -0,0 +1,267 @@ +use super::commitments::PseudonymizationFactorCommitments; +use crate::arithmetic::group_elements::{GroupElement, G}; +use crate::arithmetic::scalars::ScalarNonZero; +use crate::core::elgamal::ElGamal; +use crate::core::zkps::{create_proof, create_proofs_same_scalar, verify_proof, Proof}; +use rand_core::{CryptoRng, RngCore}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// RESHUFFLE + +#[derive(Eq, PartialEq, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +/// GroupElement is `n*G` if verifiable_reshuffle with `n` is called. +pub struct VerifiableReshuffle { + pub pb: Proof, + pub pc: Proof, +} + +impl VerifiableReshuffle { + pub fn new(v: &ElGamal, s: &ScalarNonZero, rng: &mut R) -> Self { + // Reshuffle is normally {s * in.b, s * in.c, in.y}; + let (_gs, pb, pc) = create_proofs_same_scalar(s, &v.gb, &v.gc, rng); + Self { pb, pc } + } + pub fn verified_reconstruct( + &self, + original: &ElGamal, + commitments: &PseudonymizationFactorCommitments, + ) -> Option { + if self.verify(original, commitments) { + #[cfg(feature = "elgamal3")] + return Some(self.reconstruct(original)); + #[cfg(not(feature = "elgamal3"))] + Some(self.reconstruct()) + } else { + None + } + } + #[cfg(feature = "elgamal3")] + fn reconstruct(&self, original: &ElGamal) -> ElGamal { + ElGamal { + gb: *self.pb, + gc: *self.pc, + gy: original.gy, + } + } + #[cfg(not(feature = "elgamal3"))] + fn reconstruct(&self) -> ElGamal { + ElGamal { + gb: *self.pb, + gc: *self.pc, + } + } + #[cfg(feature = "insecure")] + #[cfg(feature = "elgamal3")] + pub fn unverified_reconstruct(&self, original: &ElGamal) -> ElGamal { + self.reconstruct(original) + } + #[cfg(feature = "insecure")] + #[cfg(not(feature = "elgamal3"))] + pub fn unverified_reconstruct(&self) -> ElGamal { + self.reconstruct() + } + + #[must_use] + fn verify(&self, original: &ElGamal, commitments: &PseudonymizationFactorCommitments) -> bool { + #[cfg(feature = "elgamal3")] + return Self::verify_split( + &original.gb, + &original.gc, + &original.gy, + &commitments.0.val, + &self.pb, + &self.pc, + ); + #[cfg(not(feature = "elgamal3"))] + Self::verify_split( + &original.gb, + &original.gc, + &commitments.0.val, + &self.pb, + &self.pc, + ) + } + #[must_use] + pub fn verify_reshuffle( + &self, + original: &ElGamal, + new: &ElGamal, + commitments: &PseudonymizationFactorCommitments, + ) -> bool { + #[cfg(feature = "elgamal3")] + return self.verify(original, commitments) + && new.gb == *self.pb + && new.gc == *self.pc + && new.gy == original.gy; + #[cfg(not(feature = "elgamal3"))] + return self.verify(original, commitments) && new.gb == *self.pb && new.gc == *self.pc; + } + #[cfg(feature = "elgamal3")] + #[must_use] + fn verify_split( + gb: &GroupElement, + gc: &GroupElement, + _gy: &GroupElement, + gn: &GroupElement, + pb: &Proof, + pc: &Proof, + ) -> bool { + verify_proof(gn, gb, pb) && verify_proof(gn, gc, pc) + } + #[cfg(not(feature = "elgamal3"))] + #[must_use] + fn verify_split( + gb: &GroupElement, + gc: &GroupElement, + gn: &GroupElement, + pb: &Proof, + pc: &Proof, + ) -> bool { + verify_proof(gn, gb, pb) && verify_proof(gn, gc, pc) + } + + // Reshuffle2 methods + pub fn new2( + v: &ElGamal, + from: &ScalarNonZero, + to: &ScalarNonZero, + rng: &mut R, + ) -> Self { + // Reshuffle2 is normally {s_from^-1 * s_to * in.b, s_from^-1 * s_to * in.c, in.y}; + let s = from.invert() * to; + let (_gs, pb, pc) = create_proofs_same_scalar(&s, &v.gb, &v.gc, rng); + Self { pb, pc } + } + pub fn verified_reconstruct2( + &self, + original: &ElGamal, + reshuffle2_proof: &Reshuffle2FactorsProof, + ) -> Option { + if self.verify2(original, reshuffle2_proof) { + #[cfg(feature = "elgamal3")] + return Some(self.reconstruct(original)); + #[cfg(not(feature = "elgamal3"))] + Some(self.reconstruct()) + } else { + None + } + } + #[must_use] + fn verify2(&self, original: &ElGamal, reshuffle2_proof: &Reshuffle2FactorsProof) -> bool { + #[cfg(feature = "elgamal3")] + return Self::verify_split2( + &original.gb, + &original.gc, + &original.gy, + &self.pb, + &self.pc, + reshuffle2_proof, + ); + #[cfg(not(feature = "elgamal3"))] + Self::verify_split2( + &original.gb, + &original.gc, + &self.pb, + &self.pc, + reshuffle2_proof, + ) + } + #[must_use] + pub fn verify_reshuffled2( + &self, + original: &ElGamal, + new: &ElGamal, + reshuffle2_proof: &Reshuffle2FactorsProof, + ) -> bool { + #[cfg(feature = "elgamal3")] + return self.verify2(original, reshuffle2_proof) + && new.gb == *self.pb + && new.gc == *self.pc + && new.gy == original.gy; + #[cfg(not(feature = "elgamal3"))] + return self.verify2(original, reshuffle2_proof) + && new.gb == *self.pb + && new.gc == *self.pc; + } + #[cfg(feature = "elgamal3")] + #[must_use] + fn verify_split2( + gb: &GroupElement, + gc: &GroupElement, + _gy: &GroupElement, + pb: &Proof, + pc: &Proof, + reshuffle2_proof: &Reshuffle2FactorsProof, + ) -> bool { + // ps is needed as proof that s is constructed as s_from.invert() * s_t + verify_proof(&reshuffle2_proof.ps, gb, pb) && verify_proof(&reshuffle2_proof.ps, gc, pc) + } + #[cfg(not(feature = "elgamal3"))] + #[must_use] + fn verify_split2( + gb: &GroupElement, + gc: &GroupElement, + pb: &Proof, + pc: &Proof, + reshuffle2_proof: &Reshuffle2FactorsProof, + ) -> bool { + // ps is needed as proof that s is constructed as s_from.invert() * s_t + verify_proof(&reshuffle2_proof.ps, gb, pb) && verify_proof(&reshuffle2_proof.ps, gc, pc) + } +} + +#[derive(Eq, PartialEq, Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Reshuffle2FactorsProof { + pub ps: Proof, +} + +impl Reshuffle2FactorsProof { + pub fn new( + from: &ScalarNonZero, + to: &ScalarNonZero, + rng: &mut R, + ) -> Self { + let (_gs, ps) = create_proof(&from.invert(), &(to * G), rng); + Self { ps } + } + pub fn from_proof( + ps: &Proof, + commitments_from: &PseudonymizationFactorCommitments, + commitments_to: &PseudonymizationFactorCommitments, + ) -> Option { + let x = Self { ps: *ps }; + if x.verify(commitments_from, commitments_to) { + Some(x) + } else { + None + } + } + #[must_use] + pub fn verify( + &self, + commitments_from: &PseudonymizationFactorCommitments, + commitments_to: &PseudonymizationFactorCommitments, + ) -> bool { + verify_proof(&commitments_from.0.inv, &commitments_to.0.val, &self.ps) + } +} + +pub fn verifiable_reshuffle( + m: &ElGamal, + s: &ScalarNonZero, + rng: &mut R, +) -> VerifiableReshuffle { + VerifiableReshuffle::new(m, s, rng) +} + +pub fn verifiable_reshuffle2( + m: &ElGamal, + from: &ScalarNonZero, + to: &ScalarNonZero, + rng: &mut R, +) -> VerifiableReshuffle { + VerifiableReshuffle::new2(m, from, to, rng) +} diff --git a/src/lib/core/proved/rsk.rs b/src/lib/core/proved/rsk.rs new file mode 100644 index 0000000..9c5606d --- /dev/null +++ b/src/lib/core/proved/rsk.rs @@ -0,0 +1,426 @@ +use super::commitments::{PseudonymizationFactorCommitments, RekeyFactorCommitments}; +use super::rekey::Rekey2FactorsProof; +use super::reshuffle::Reshuffle2FactorsProof; +use crate::arithmetic::group_elements::{GroupElement, G}; +use crate::arithmetic::scalars::ScalarNonZero; +use crate::core::elgamal::ElGamal; +use crate::core::zkps::{create_proof, verify_proof, Proof}; +use rand_core::{CryptoRng, RngCore}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[derive(Eq, PartialEq, Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct RSKFactorsProof { + pub pski: Proof, +} + +impl RSKFactorsProof { + pub fn new(s: &ScalarNonZero, k: &ScalarNonZero, rng: &mut R) -> Self { + let ki = k.invert(); + let (_gm, pski) = create_proof(&ki, &(s * G), rng); + Self { pski } + } + pub fn from_proof( + pski: &Proof, + reshuffle_commitments: &PseudonymizationFactorCommitments, + rekey_commitments: &RekeyFactorCommitments, + ) -> Option { + let x = Self { pski: *pski }; + if x.verify(reshuffle_commitments, rekey_commitments) { + Some(x) + } else { + None + } + } + #[must_use] + pub fn verify( + &self, + reshuffle_commitments: &PseudonymizationFactorCommitments, + rekey_commitments: &RekeyFactorCommitments, + ) -> bool { + verify_proof( + &rekey_commitments.0.inv, + &reshuffle_commitments.0.val, + &self.pski, + ) + } +} + +#[derive(Eq, PartialEq, Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct VerifiableRSK { + pub pb: Proof, + pub pc: Proof, + #[cfg(feature = "elgamal3")] + pub py: Proof, +} + +impl VerifiableRSK { + pub fn new( + v: &ElGamal, + s: &ScalarNonZero, + k: &ScalarNonZero, + rng: &mut R, + ) -> Self { + // RSK is normally {s * k^-1 * in.b, s * in.c, k * in.y}; + let ki = k.invert(); + let ski = s * ki; + let (_gski, pb) = create_proof(&ski, &v.gb, rng); + let (_gn, pc) = create_proof(s, &v.gc, rng); + #[cfg(feature = "elgamal3")] + let (_gk, py) = create_proof(k, &v.gy, rng); + Self { + pb, + pc, + #[cfg(feature = "elgamal3")] + py, + } + } + pub fn verified_reconstruct( + &self, + original: &ElGamal, + rsk_proof: &RSKFactorsProof, + reshuffle_commitments: &PseudonymizationFactorCommitments, + rekey_commitments: &RekeyFactorCommitments, + ) -> Option { + if self.verify( + original, + rsk_proof, + reshuffle_commitments, + rekey_commitments, + ) { + Some(self.reconstruct()) + } else { + None + } + } + /// Extract the result of the RSK operation from the proof. + /// + /// This allows getting the transformed ElGamal value without duplicating data. + pub fn result(&self) -> ElGamal { + ElGamal { + gb: *self.pb, + gc: *self.pc, + #[cfg(feature = "elgamal3")] + gy: *self.py, + } + } + + fn reconstruct(&self) -> ElGamal { + self.result() + } + #[cfg(feature = "insecure")] + pub fn unverified_reconstruct(&self) -> ElGamal { + self.reconstruct() + } + #[must_use] + fn verify( + &self, + original: &ElGamal, + rsk_proof: &RSKFactorsProof, + reshuffle_commitments: &PseudonymizationFactorCommitments, + rekey_commitments: &RekeyFactorCommitments, + ) -> bool { + #[cfg(feature = "elgamal3")] + return Self::verify_split( + &original.gb, + &original.gc, + &original.gy, + &self.pb, + &self.pc, + &self.py, + rsk_proof, + reshuffle_commitments, + rekey_commitments, + ); + #[cfg(not(feature = "elgamal3"))] + Self::verify_split( + &original.gb, + &original.gc, + &self.pb, + &self.pc, + rsk_proof, + reshuffle_commitments, + rekey_commitments, + ) + } + #[cfg(feature = "elgamal3")] + #[must_use] + pub fn verify_rsk( + &self, + original: &ElGamal, + new: &ElGamal, + rsk_proof: &RSKFactorsProof, + reshuffle_commitments: &PseudonymizationFactorCommitments, + rekey_commitments: &RekeyFactorCommitments, + ) -> bool { + #[cfg(feature = "elgamal3")] + return self.verify( + original, + rsk_proof, + reshuffle_commitments, + rekey_commitments, + ) && new.gb == *self.pb + && new.gc == *self.pc + && new.gy == *self.py; + #[cfg(not(feature = "elgamal3"))] + return self.verify( + original, + rsk_proof, + reshuffle_commitments, + rekey_commitments, + ) && new.gb == *self.pb + && new.gc == *self.pc; + } + #[cfg(not(feature = "elgamal3"))] + #[must_use] + pub fn verify_rsk( + &self, + original: &ElGamal, + new: &ElGamal, + rsk_proof: &RSKFactorsProof, + reshuffle_commitments: &PseudonymizationFactorCommitments, + rekey_commitments: &RekeyFactorCommitments, + ) -> bool { + self.verify( + original, + rsk_proof, + reshuffle_commitments, + rekey_commitments, + ) && new.gb == *self.pb + && new.gc == *self.pc + } + #[cfg(feature = "elgamal3")] + #[must_use] + #[allow(clippy::too_many_arguments)] + fn verify_split( + gb: &GroupElement, + gc: &GroupElement, + gy: &GroupElement, + pb: &Proof, + pc: &Proof, + py: &Proof, + rsk_proof: &RSKFactorsProof, + reshuffle_commitments: &PseudonymizationFactorCommitments, + rekey_commitments: &RekeyFactorCommitments, + ) -> bool { + verify_proof(&rsk_proof.pski, gb, pb) + && verify_proof(&reshuffle_commitments.0.val, gc, pc) + && verify_proof(&rekey_commitments.0.val, gy, py) + && rsk_proof.verify(reshuffle_commitments, rekey_commitments) + } + #[cfg(not(feature = "elgamal3"))] + #[must_use] + fn verify_split( + gb: &GroupElement, + gc: &GroupElement, + pb: &Proof, + pc: &Proof, + rsk_proof: &RSKFactorsProof, + reshuffle_commitments: &PseudonymizationFactorCommitments, + rekey_commitments: &RekeyFactorCommitments, + ) -> bool { + verify_proof(&rsk_proof.pski, gb, pb) + && verify_proof(&reshuffle_commitments.0.val, gc, pc) + && rsk_proof.verify(reshuffle_commitments, rekey_commitments) + } + + // RSK2 methods + pub fn new2( + v: &ElGamal, + s_from: &ScalarNonZero, + s_to: &ScalarNonZero, + k_from: &ScalarNonZero, + k_to: &ScalarNonZero, + rng: &mut R, + ) -> Self { + // RSK is normally {s * k^-1 * in.b, s * in.c, k * in.y}; + let s = s_from.invert() * s_to; + let k = k_from.invert() * k_to; + let ki = k.invert(); + let ski = s * ki; + + let (_gski, pb) = create_proof(&ski, &v.gb, rng); + let (_gs, pc) = create_proof(&s, &v.gc, rng); + #[cfg(feature = "elgamal3")] + let (_gk, py) = create_proof(&k, &v.gy, rng); + Self { + pb, + pc, + #[cfg(feature = "elgamal3")] + py, + } + } + pub fn verified_reconstruct2( + &self, + original: &ElGamal, + rsk2_proof: &RSK2FactorsProof, + ) -> Option { + if self.verify2(original, rsk2_proof) { + Some(self.reconstruct()) + } else { + None + } + } + #[cfg(feature = "elgamal3")] + #[must_use] + fn verify2(&self, original: &ElGamal, rsk2_proof: &RSK2FactorsProof) -> bool { + Self::verify_split2( + &original.gb, + &original.gc, + &original.gy, + &self.pb, + &self.pc, + &self.py, + rsk2_proof, + ) + } + #[cfg(not(feature = "elgamal3"))] + #[must_use] + fn verify2(&self, original: &ElGamal, rsk2_proof: &RSK2FactorsProof) -> bool { + Self::verify_split2(&original.gb, &original.gc, &self.pb, &self.pc, rsk2_proof) + } + #[must_use] + pub fn verify_rsk2( + &self, + original: &ElGamal, + new: &ElGamal, + rsk2_proof: &RSK2FactorsProof, + ) -> bool { + #[cfg(feature = "elgamal3")] + return self.verify2(original, rsk2_proof) + && new.gb == *self.pb + && new.gc == *self.pc + && new.gy == *self.py; + #[cfg(not(feature = "elgamal3"))] + return self.verify2(original, rsk2_proof) && new.gb == *self.pb && new.gc == *self.pc; + } + #[cfg(feature = "elgamal3")] + #[must_use] + fn verify_split2( + gb: &GroupElement, + gc: &GroupElement, + gy: &GroupElement, + pb: &Proof, + pc: &Proof, + py: &Proof, + rsk2_proof: &RSK2FactorsProof, + ) -> bool { + verify_proof(&rsk2_proof.pski, gb, pb) + && verify_proof(&rsk2_proof.gs, gc, pc) + && verify_proof(&rsk2_proof.gk, gy, py) + } + #[cfg(not(feature = "elgamal3"))] + #[must_use] + fn verify_split2( + gb: &GroupElement, + gc: &GroupElement, + pb: &Proof, + pc: &Proof, + rsk2_proof: &RSK2FactorsProof, + ) -> bool { + verify_proof(&rsk2_proof.pski, gb, pb) && verify_proof(&rsk2_proof.gs, gc, pc) + } +} + +#[derive(Eq, PartialEq, Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct RSK2FactorsProof { + pub pski: Proof, + pub gs: GroupElement, + #[cfg(feature = "elgamal3")] + pub gk: GroupElement, +} + +impl RSK2FactorsProof { + pub fn new( + s_from: &ScalarNonZero, + s_to: &ScalarNonZero, + k_from: &ScalarNonZero, + k_to: &ScalarNonZero, + rng: &mut R, + ) -> Self { + let gs = s_from.invert() * s_to * G; + let k = k_from.invert() * k_to; + let (_gm, pski) = create_proof(&k.invert(), &gs, rng); + #[cfg(feature = "elgamal3")] + let gk = k * G; + Self { + pski, + gs, + #[cfg(feature = "elgamal3")] + gk, + } + } + #[cfg(feature = "elgamal3")] + pub fn from_proof( + pski: &Proof, + gs: &GroupElement, + gk: &GroupElement, + reshuffle2_proof: &Reshuffle2FactorsProof, + rekey2_proof: &Rekey2FactorsProof, + ) -> Option { + let x = Self { + pski: *pski, + gs: *gs, + gk: *gk, + }; + if x.verify(reshuffle2_proof, rekey2_proof) { + Some(x) + } else { + None + } + } + #[cfg(not(feature = "elgamal3"))] + pub fn from_proof( + pski: &Proof, + gs: &GroupElement, + reshuffle2_proof: &Reshuffle2FactorsProof, + rekey2_proof: &Rekey2FactorsProof, + ) -> Option { + let x = Self { + pski: *pski, + gs: *gs, + }; + if x.verify(reshuffle2_proof, rekey2_proof) { + Some(x) + } else { + None + } + } + #[must_use] + pub fn verify( + &self, + reshuffle2_proof: &Reshuffle2FactorsProof, + rekey2_proof: &Rekey2FactorsProof, + ) -> bool { + #[cfg(feature = "elgamal3")] + return verify_proof(&rekey2_proof.pki, &reshuffle2_proof.ps, &self.pski) + && self.gs == *reshuffle2_proof.ps + && self.gk == *rekey2_proof.pk; + #[cfg(not(feature = "elgamal3"))] + return verify_proof(&rekey2_proof.pki, &reshuffle2_proof.ps, &self.pski) + && self.gs == *reshuffle2_proof.ps; + } +} + +pub fn verifiable_rsk( + m: &ElGamal, + s: &ScalarNonZero, + k: &ScalarNonZero, + rng: &mut R, +) -> VerifiableRSK { + VerifiableRSK::new(m, s, k, rng) +} + +pub fn verifiable_rsk2( + m: &ElGamal, + s_from: &ScalarNonZero, + s_to: &ScalarNonZero, + k_from: &ScalarNonZero, + k_to: &ScalarNonZero, + rng: &mut R, +) -> VerifiableRSK { + VerifiableRSK::new2(m, s_from, s_to, k_from, k_to, rng) +} diff --git a/src/lib/core/py/mod.rs b/src/lib/core/py/mod.rs index 830f770..7195006 100644 --- a/src/lib/core/py/mod.rs +++ b/src/lib/core/py/mod.rs @@ -1,6 +1,12 @@ pub mod elgamal; pub mod primitives; +#[cfg(feature = "verifiable")] +pub mod proved; + +#[cfg(feature = "verifiable")] +pub mod zkps; + use pyo3::prelude::*; pub fn register_module(m: &Bound<'_, PyModule>) -> PyResult<()> { @@ -20,5 +26,16 @@ pub fn register_module(m: &Bound<'_, PyModule>) -> PyResult<()> { .getattr("modules")? .set_item("libpep.core.primitives", &primitives_module)?; + #[cfg(feature = "verifiable")] + { + let zkps_module = PyModule::new(py, "zkps")?; + zkps::register_module(m)?; + py.import("sys")? + .getattr("modules")? + .set_item("libpep.core.zkps", &zkps_module)?; + + proved::register_module(m)?; + } + Ok(()) } diff --git a/src/lib/core/py/proved.rs b/src/lib/core/py/proved.rs new file mode 100644 index 0000000..1c92167 --- /dev/null +++ b/src/lib/core/py/proved.rs @@ -0,0 +1,118 @@ +//! Python bindings for verifiable proofs. + +use crate::core::proved::{RSKFactorsProof, VerifiableRSK, VerifiableRekey, VerifiableReshuffle}; +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; + +/// A verifiable proof for reshuffle operations. +#[pyclass(name = "VerifiableReshuffle")] +#[derive(Clone)] +pub struct PyVerifiableReshuffle { + pub(crate) inner: VerifiableReshuffle, +} + +#[pymethods] +impl PyVerifiableReshuffle { + /// Serialize to JSON string. + #[cfg(feature = "serde")] + fn to_json(&self) -> PyResult { + serde_json::to_string(&self.inner) + .map_err(|e| PyValueError::new_err(format!("Failed to serialize: {}", e))) + } + + /// Deserialize from JSON string. + #[cfg(feature = "serde")] + #[staticmethod] + fn from_json(json: &str) -> PyResult { + serde_json::from_str(json) + .map(|inner| Self { inner }) + .map_err(|e| PyValueError::new_err(format!("Failed to deserialize: {}", e))) + } +} + +/// A verifiable proof for rekey operations. +#[pyclass(name = "VerifiableRekey")] +#[derive(Clone)] +pub struct PyVerifiableRekey { + pub(crate) inner: VerifiableRekey, +} + +#[pymethods] +impl PyVerifiableRekey { + /// Serialize to JSON string. + #[cfg(feature = "serde")] + fn to_json(&self) -> PyResult { + serde_json::to_string(&self.inner) + .map_err(|e| PyValueError::new_err(format!("Failed to serialize: {}", e))) + } + + /// Deserialize from JSON string. + #[cfg(feature = "serde")] + #[staticmethod] + fn from_json(json: &str) -> PyResult { + serde_json::from_str(json) + .map(|inner| Self { inner }) + .map_err(|e| PyValueError::new_err(format!("Failed to deserialize: {}", e))) + } +} + +/// A verifiable proof for RSK (reshuffle-shift-rekey) operations. +#[pyclass(name = "VerifiableRSK")] +#[derive(Clone)] +pub struct PyVerifiableRSK { + pub(crate) inner: VerifiableRSK, +} + +#[pymethods] +impl PyVerifiableRSK { + /// Serialize to JSON string. + #[cfg(feature = "serde")] + fn to_json(&self) -> PyResult { + serde_json::to_string(&self.inner) + .map_err(|e| PyValueError::new_err(format!("Failed to serialize: {}", e))) + } + + /// Deserialize from JSON string. + #[cfg(feature = "serde")] + #[staticmethod] + fn from_json(json: &str) -> PyResult { + serde_json::from_str(json) + .map(|inner| Self { inner }) + .map_err(|e| PyValueError::new_err(format!("Failed to deserialize: {}", e))) + } +} + +/// A proof for RSK factors. +#[pyclass(name = "RSKFactorsProof")] +#[derive(Clone)] +pub struct PyRSKFactorsProof { + pub(crate) inner: RSKFactorsProof, +} + +#[pymethods] +impl PyRSKFactorsProof { + /// Serialize to JSON string. + #[cfg(feature = "serde")] + fn to_json(&self) -> PyResult { + serde_json::to_string(&self.inner) + .map_err(|e| PyValueError::new_err(format!("Failed to serialize: {}", e))) + } + + /// Deserialize from JSON string. + #[cfg(feature = "serde")] + #[staticmethod] + fn from_json(json: &str) -> PyResult { + serde_json::from_str(json) + .map(|inner| Self { inner }) + .map_err(|e| PyValueError::new_err(format!("Failed to deserialize: {}", e))) + } +} + +/// Register the proved module. +pub fn register_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { + parent_module.add_class::()?; + parent_module.add_class::()?; + parent_module.add_class::()?; + parent_module.add_class::()?; + Ok(()) +} diff --git a/src/lib/core/py/zkps.rs b/src/lib/core/py/zkps.rs new file mode 100644 index 0000000..2e35a37 --- /dev/null +++ b/src/lib/core/py/zkps.rs @@ -0,0 +1,136 @@ +//! Python bindings for zero-knowledge proofs. + +use crate::arithmetic::group_elements::GroupElement; +use crate::arithmetic::scalars::ScalarNonZero; +use crate::core::zkps::{create_proof, verify_proof, Proof}; +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use pyo3::types::PyBytes; + +/// A zero-knowledge proof demonstrating knowledge of a discrete logarithm. +/// +/// This proof shows that `N = a*M` for some secret scalar `a` without revealing `a`. +#[pyclass(name = "Proof")] +#[derive(Clone)] +pub struct PyProof { + pub(crate) inner: Proof, +} + +#[pymethods] +impl PyProof { + /// Encodes the proof as a base64 string. + fn to_base64(&self) -> String { + self.inner.to_base64() + } + + /// Decodes a proof from a base64 string. + #[staticmethod] + fn from_base64(s: &str) -> PyResult { + Proof::from_base64(s) + .map(|inner| PyProof { inner }) + .ok_or_else(|| PyValueError::new_err("Invalid base64 encoded proof")) + } + + /// Encodes the proof as a hex string. + fn to_hex(&self) -> String { + hex::encode(self.inner.encode()) + } + + /// Decodes a proof from a hex string. + #[staticmethod] + fn from_hex(s: &str) -> PyResult { + let bytes = hex::decode(s).map_err(|e| PyValueError::new_err(format!("{}", e)))?; + if bytes.len() != 128 { + return Err(PyValueError::new_err("Invalid proof length")); + } + let mut arr = [0u8; 128]; + arr.copy_from_slice(&bytes); + Proof::decode(&arr) + .map(|inner| PyProof { inner }) + .ok_or_else(|| PyValueError::new_err("Invalid proof encoding")) + } + + /// Returns the encoded bytes of the proof. + fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> { + PyBytes::new(py, &self.inner.encode()) + } + + /// Decodes a proof from bytes. + #[staticmethod] + fn from_bytes(bytes: &[u8]) -> PyResult { + if bytes.len() != 128 { + return Err(PyValueError::new_err("Invalid proof length")); + } + let mut arr = [0u8; 128]; + arr.copy_from_slice(bytes); + Proof::decode(&arr) + .map(|inner| PyProof { inner }) + .ok_or_else(|| PyValueError::new_err("Invalid proof encoding")) + } + + fn __repr__(&self) -> String { + format!("Proof({})", self.to_base64()) + } + + fn __str__(&self) -> String { + self.to_base64() + } +} + +/// Creates a zero-knowledge proof. +/// +/// Args: +/// secret: The secret scalar as 32 bytes +/// message: The message as a group element (32 bytes) +/// +/// Returns: +/// A tuple of (public_key, proof) where public_key is 32 bytes +#[pyfunction] +fn create_zkp_proof<'py>( + py: Python<'py>, + secret: &[u8], + message: &[u8], +) -> PyResult<(Bound<'py, PyBytes>, PyProof)> { + let mut rng = rand::rng(); + + let secret = ScalarNonZero::from_slice(secret) + .ok_or_else(|| PyValueError::new_err("Invalid secret scalar"))?; + let message = GroupElement::from_slice(message) + .ok_or_else(|| PyValueError::new_err("Invalid message group element"))?; + + let (public_key, proof) = create_proof(&secret, &message, &mut rng); + + Ok(( + PyBytes::new(py, public_key.to_bytes().as_slice()), + PyProof { inner: proof }, + )) +} + +/// Verifies a zero-knowledge proof. +/// +/// Args: +/// public_key: The public key as 32 bytes +/// message: The message as a group element (32 bytes) +/// proof: The proof to verify +/// +/// Returns: +/// True if the proof is valid, False otherwise +#[pyfunction] +fn verify_zkp_proof(public_key: &[u8], message: &[u8], proof: &PyProof) -> PyResult { + let public_key = GroupElement::from_slice(public_key) + .ok_or_else(|| PyValueError::new_err("Invalid public key"))?; + let message = GroupElement::from_slice(message) + .ok_or_else(|| PyValueError::new_err("Invalid message"))?; + + Ok(verify_proof(&public_key, &message, &proof.inner)) +} + +/// Register the zkps module. +pub fn register_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { + let zkps_module = PyModule::new(parent_module.py(), "zkps")?; + zkps_module.add_class::()?; + zkps_module.add_function(wrap_pyfunction!(create_zkp_proof, &zkps_module)?)?; + zkps_module.add_function(wrap_pyfunction!(verify_zkp_proof, &zkps_module)?)?; + parent_module.add_submodule(&zkps_module)?; + Ok(()) +} diff --git a/src/lib/core/wasm/mod.rs b/src/lib/core/wasm/mod.rs index ab6482a..86037b6 100644 --- a/src/lib/core/wasm/mod.rs +++ b/src/lib/core/wasm/mod.rs @@ -1,2 +1,5 @@ pub mod elgamal; pub mod primitives; + +#[cfg(feature = "verifiable")] +pub mod zkps; diff --git a/src/lib/core/zkps.rs b/src/lib/core/zkps.rs new file mode 100644 index 0000000..591ed13 --- /dev/null +++ b/src/lib/core/zkps.rs @@ -0,0 +1,622 @@ +//! Zero-knowledge proofs and signatures using Schnorr proofs with the Fiat-Shamir transform. +//! +//! This module provides cryptographic primitives for creating and verifying zero-knowledge proofs +//! that demonstrate knowledge of a discrete logarithm without revealing it. These proofs are +//! non-interactive thanks to the Fiat-Shamir transform, which uses a hash function to derive +//! the challenge. +//! +//! # Overview +//! +//! The main components are: +//! - [`Proof`]: A zero-knowledge proof that demonstrates `N = a*M` without revealing `a` +//! - [`create_proof`]/[`verify_proof`]: Create and verify proofs +//! - [`sign`]/[`verify`]: Create and verify signatures (proofs used as signatures) +//! - [`sign_unlinkable`]: Create deterministic signatures that prevent linkability +//! +//! # Security Properties +//! +//! - **Zero-knowledge**: The verifier learns nothing about the secret scalar beyond what the proof demonstrates +//! - **Soundness**: It's computationally infeasible to create a valid proof without knowing the secret +//! - **Non-interactive**: No interaction between prover and verifier required (thanks to Fiat-Shamir) +//! +//! # Examples +//! +//! ``` +//! # use libpep::arithmetic::group_elements::{GroupElement, G}; +//! # use libpep::arithmetic::scalars::ScalarNonZero; +//! # use libpep::core::zkps::{create_proof, verify_proof}; +//! # let mut rng = rand::rng(); +//! let secret = ScalarNonZero::random(&mut rng); +//! let message = GroupElement::random(&mut rng); +//! +//! // Prover creates a proof +//! let (public_key, proof) = create_proof(&secret, &message, &mut rng); +//! +//! // Verifier checks the proof +//! assert!(verify_proof(&public_key, &message, &proof)); +//! ``` + +use derive_more::Deref; +use rand_core::{CryptoRng, RngCore}; +use sha2::{Digest, Sha512}; +use std::fmt::Formatter; + +use crate::arithmetic::group_elements::{GroupElement, G}; +use crate::arithmetic::scalars::{ScalarCanBeZero, ScalarNonZero, ScalarTraits}; +use base64::engine::general_purpose; +use base64::Engine; +use serde::de::{Error, Visitor}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +/// A zero-knowledge proof demonstrating knowledge of a discrete logarithm. +/// +/// This proof shows that `N = a*M` for some secret scalar `a` without revealing `a`. +/// The proof can be verified using the public key `A = a*G` and the message `M`, +/// while keeping the secret `a` hidden. +/// +/// The proof uses the Fiat-Shamir transform to make it non-interactive, deriving +/// the challenge from a hash of the public values. +/// +/// # Fields +/// +/// - `n`: The result `N = a*M` (also accessible via `Deref`) +/// - `c1`, `c2`: Commitments `r*G` and `r*M` for a random nonce `r` +/// - `s`: The response `s = a*e + r` where `e` is the challenge +/// +/// # Serialization +/// +/// Proofs are serialized as base64-encoded strings when using serde. +#[derive(Eq, PartialEq, Clone, Copy, Debug, Deref)] +pub struct Proof { + #[deref] + pub n: GroupElement, + pub c1: GroupElement, + pub c2: GroupElement, + pub s: ScalarCanBeZero, +} + +impl Proof { + /// Encodes the proof as a 128-byte array. + /// + /// The encoding layout is: + /// - Bytes 0-31: `n` + /// - Bytes 32-63: `c1` + /// - Bytes 64-95: `c2` + /// - Bytes 96-127: `s` + pub fn encode(&self) -> [u8; 128] { + let mut retval = [0u8; 128]; + retval[0..32].clone_from_slice(self.n.to_bytes().as_slice()); + retval[32..64].clone_from_slice(self.c1.to_bytes().as_slice()); + retval[64..96].clone_from_slice(self.c2.to_bytes().as_slice()); + retval[96..128].clone_from_slice(self.s.to_bytes().as_slice()); + retval + } + + /// Decodes a proof from a 128-byte array. + /// + /// Returns `None` if any component fails to decode. + pub fn decode(v: &[u8; 128]) -> Option { + Some(Self { + n: GroupElement::from_slice(&v[0..32])?, + c1: GroupElement::from_slice(&v[32..64])?, + c2: GroupElement::from_slice(&v[64..96])?, + s: ScalarCanBeZero::from_slice(&v[96..128])?, + }) + } + + /// Decodes a proof from a byte slice. + /// + /// Returns `None` if the slice is not exactly 128 bytes or if decoding fails. + pub fn decode_from_slice(v: &[u8]) -> Option { + if v.len() != 128 { + None + } else { + let mut arr = [0u8; 128]; + arr.copy_from_slice(v); + Self::decode(&arr) + } + } + + /// Encodes the proof as a URL-safe base64 string. + pub fn to_base64(&self) -> String { + general_purpose::URL_SAFE.encode(self.encode()) + } + + /// Decodes a proof from a URL-safe base64 string. + /// + /// Returns `None` if the string is not valid base64 or if decoding fails. + pub fn from_base64(s: &str) -> Option { + general_purpose::URL_SAFE + .decode(s) + .ok() + .and_then(|v| Self::decode_from_slice(&v)) + } +} + +impl Serialize for Proof { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.to_base64().as_str()) + } +} + +impl<'de> Deserialize<'de> for Proof { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct ProofVisitor; + impl<'de> Visitor<'de> for ProofVisitor { + type Value = Proof; + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("a base64 encoded string representing a Proof") + } + + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + Proof::from_base64(v) + .ok_or_else(|| E::custom(format!("invalid base64 encoded string: {}", v))) + } + } + + deserializer.deserialize_str(ProofVisitor) + } +} + +/// Creates a zero-knowledge proof demonstrating knowledge of a discrete logarithm. +/// +/// Given a secret scalar `a` and a public group element `M`, this function creates a proof +/// that `N = a*M` without revealing `a`. The proof uses a random nonce for unlinkability. +/// +/// # Arguments +/// +/// * `a` - The secret scalar +/// * `gm` - The message/base group element `M` +/// * `rng` - A cryptographically secure random number generator +/// +/// # Returns +/// +/// A tuple `(A, Proof)` where: +/// - `A = a*G` is the public key corresponding to secret `a` +/// - `Proof` contains `N = a*M` and the zero-knowledge proof +/// +/// # Example +/// +/// ``` +/// # use libpep::arithmetic::group_elements::{GroupElement, G}; +/// # use libpep::arithmetic::scalars::ScalarNonZero; +/// # use libpep::core::zkps::{create_proof, verify_proof}; +/// # let mut rng = rand::rng(); +/// let secret = ScalarNonZero::random(&mut rng); +/// let message = GroupElement::random(&mut rng); +/// +/// let (public_key, proof) = create_proof(&secret, &message, &mut rng); +/// assert!(verify_proof(&public_key, &message, &proof)); +/// ``` +pub fn create_proof( + a: &ScalarNonZero, + gm: &GroupElement, + rng: &mut R, +) -> (GroupElement, Proof) { + let r = ScalarNonZero::random(rng); + + let ga = a * G; + let gn = a * gm; + let gc1 = r * G; + let gc2 = r * gm; + + let mut hasher = Sha512::new(); + hasher.update(ga.to_bytes()); + hasher.update(gm.to_bytes()); + hasher.update(gn.to_bytes()); + hasher.update(gc1.to_bytes()); + hasher.update(gc2.to_bytes()); + let mut bytes = [0u8; 64]; + bytes.copy_from_slice(hasher.finalize().as_slice()); + let e = ScalarNonZero::from_hash(&bytes); + let s = ScalarCanBeZero((a * e).0) + ScalarCanBeZero(r.0); + ( + ga, + Proof { + n: gn, + c1: gc1, + c2: gc2, + s, + }, + ) +} + +/// Creates two zero-knowledge proofs with the same scalar, optimized to share computations. +/// +/// This function is more efficient than calling `create_proof` twice because: +/// - It only computes `ga = a * G` once (shared between both proofs) +/// - It uses the same random value `r`, saving one RNG call +/// - The challenge computation uses the same `ga`, reducing redundant operations +/// +/// # Arguments +/// * `a` - The secret scalar (same for both proofs) +/// * `gm1` - First message element +/// * `gm2` - Second message element +/// * `rng` - Random number generator +/// +/// # Returns +/// A tuple containing: +/// - The shared public key `ga` +/// - First proof for `gm1` +/// - Second proof for `gm2` +pub fn create_proofs_same_scalar( + a: &ScalarNonZero, + gm1: &GroupElement, + gm2: &GroupElement, + rng: &mut R, +) -> (GroupElement, Proof, Proof) { + let r = ScalarNonZero::random(rng); + let ga = a * G; + + // First proof + let gn1 = a * gm1; + let gc1_1 = r * G; + let gc2_1 = r * gm1; + + let mut hasher = Sha512::new(); + hasher.update(ga.to_bytes()); + hasher.update(gm1.to_bytes()); + hasher.update(gn1.to_bytes()); + hasher.update(gc1_1.to_bytes()); + hasher.update(gc2_1.to_bytes()); + let mut bytes = [0u8; 64]; + bytes.copy_from_slice(hasher.finalize().as_slice()); + let e1 = ScalarNonZero::from_hash(&bytes); + let s1 = ScalarCanBeZero((a * e1).0) + ScalarCanBeZero(r.0); + + let proof1 = Proof { + n: gn1, + c1: gc1_1, + c2: gc2_1, + s: s1, + }; + + // Second proof + let gn2 = a * gm2; + let gc1_2 = r * G; + let gc2_2 = r * gm2; + + let mut hasher = Sha512::new(); + hasher.update(ga.to_bytes()); + hasher.update(gm2.to_bytes()); + hasher.update(gn2.to_bytes()); + hasher.update(gc1_2.to_bytes()); + hasher.update(gc2_2.to_bytes()); + let mut bytes = [0u8; 64]; + bytes.copy_from_slice(hasher.finalize().as_slice()); + let e2 = ScalarNonZero::from_hash(&bytes); + let s2 = ScalarCanBeZero((a * e2).0) + ScalarCanBeZero(r.0); + + let proof2 = Proof { + n: gn2, + c1: gc1_2, + c2: gc2_2, + s: s2, + }; + + (ga, proof1, proof2) +} + +/// Verifies a zero-knowledge proof with all components provided separately. +/// +/// This is a low-level verification function that takes all proof components as separate +/// arguments. Most users should use [`verify_proof`] instead. +/// +/// # Arguments +/// +/// * `ga` - The public key `A = a*G` +/// * `gm` - The message `M` +/// * `gn` - The claimed result `N = a*M` +/// * `gc1` - The first commitment `c1 = r*G` +/// * `gc2` - The second commitment `c2 = r*M` +/// * `s` - The response scalar +/// +/// # Returns +/// +/// `true` if the proof is valid, `false` otherwise. +/// +/// # Verification Equations +/// +/// The function checks that: +/// - `s*G == e*A + c1` +/// - `s*M == e*N + c2` +/// +/// where `e` is the challenge derived from hashing all public values. +#[must_use] +pub fn verify_proof_split( + ga: &GroupElement, + gm: &GroupElement, + gn: &GroupElement, + gc1: &GroupElement, + gc2: &GroupElement, + s: &ScalarCanBeZero, +) -> bool { + let mut hasher = Sha512::new(); + hasher.update(ga.to_bytes()); + hasher.update(gm.to_bytes()); + hasher.update(gn.to_bytes()); + hasher.update(gc1.to_bytes()); + hasher.update(gc2.to_bytes()); + let mut bytes = [0u8; 64]; + bytes.copy_from_slice(hasher.finalize().as_slice()); + let e = ScalarNonZero::from_hash(&bytes); + + // FIXME speed up with https://docs.rs/curve25519-dalek/latest/curve25519_dalek/traits/trait.VartimeMultiscalarMul.html + // FIXME check if a faster non-constant time equality can be used + s * G == e * ga + gc1 && s * gm == e * gn + gc2 + // (a*e + r)*G = e*a*G + r*G + // (a*e + r)*gm == e*a*gm + r*gm + + // Optimized using multiscalar multiplication: + // Check: s*G - e*ga - gc1 == 0 and s*gm - e*gn - gc2 == 0 + // This is faster than computing s*G, e*ga, and gc1 separately + // use curve25519_dalek::traits::{IsIdentity, VartimeMultiscalarMul}; + // use curve25519_dalek::ristretto::RistrettoPoint; + // use curve25519_dalek::Scalar; + // let s_scalar = Scalar::from_bytes_mod_order(s.to_bytes()); + // let e_scalar = Scalar::from_bytes_mod_order(e.to_bytes()); + // let neg_e = -e_scalar; + // let neg_one = -Scalar::ONE; + // + // let check1 = RistrettoPoint::vartime_multiscalar_mul( + // &[s_scalar, neg_e, neg_one], + // &[G.0, ga.0, gc1.0] + // ); + // let check2 = RistrettoPoint::vartime_multiscalar_mul( + // &[s_scalar, neg_e, neg_one], + // &[gm.0, gn.0, gc2.0] + // ); + // + // check1.is_identity() && check2.is_identity() +} + +/// Verifies a zero-knowledge proof. +/// +/// This is the standard way to verify a proof. It checks that the proof correctly demonstrates +/// knowledge of the discrete logarithm relationship `N = a*M` without revealing `a`. +/// +/// # Arguments +/// +/// * `ga` - The public key `A = a*G` +/// * `gm` - The message `M` +/// * `p` - The proof to verify +/// +/// # Returns +/// +/// `true` if the proof is valid, `false` otherwise. +/// +/// # Example +/// +/// ``` +/// # use libpep::arithmetic::group_elements::{GroupElement, G}; +/// # use libpep::arithmetic::scalars::ScalarNonZero; +/// # use libpep::core::zkps::{create_proof, verify_proof}; +/// # let mut rng = rand::rng(); +/// # let secret = ScalarNonZero::random(&mut rng); +/// # let message = GroupElement::random(&mut rng); +/// let (public_key, proof) = create_proof(&secret, &message, &mut rng); +/// assert!(verify_proof(&public_key, &message, &proof)); +/// ``` +#[must_use] +pub fn verify_proof(ga: &GroupElement, gm: &GroupElement, p: &Proof) -> bool { + verify_proof_split(ga, gm, &p.n, &p.c1, &p.c2, &p.s) +} + +/// Type alias for signatures, which are structurally identical to proofs. +type Signature = Proof; + +/// Creates a digital signature for a message using a secret key. +/// +/// This function uses the zero-knowledge proof system to create a signature. +/// Each signature uses a fresh random nonce, making signatures unlinkable. +/// +/// # Arguments +/// +/// * `message` - The message to sign (a group element) +/// * `secret_key` - The secret signing key +/// * `rng` - A cryptographically secure random number generator +/// +/// # Returns +/// +/// A signature that can be verified with the corresponding public key. +/// +/// # Example +/// +/// ``` +/// # use libpep::arithmetic::group_elements::{GroupElement, G}; +/// # use libpep::arithmetic::scalars::ScalarNonZero; +/// # use libpep::core::zkps::{sign, verify}; +/// # let mut rng = rand::rng(); +/// let secret_key = ScalarNonZero::random(&mut rng); +/// let public_key = &secret_key * G; +/// let message = GroupElement::random(&mut rng); +/// +/// let signature = sign(&message, &secret_key, &mut rng); +/// assert!(verify(&message, &signature, &public_key)); +/// ``` +pub fn sign( + message: &GroupElement, + secret_key: &ScalarNonZero, + rng: &mut R, +) -> Signature { + create_proof(secret_key, message, rng).1 +} + +/// Verifies a digital signature. +/// +/// # Arguments +/// +/// * `message` - The message that was signed +/// * `p` - The signature to verify +/// * `public_key` - The public key corresponding to the secret key used for signing +/// +/// # Returns +/// +/// `true` if the signature is valid, `false` otherwise. +#[must_use] +pub fn verify(message: &GroupElement, p: &Signature, public_key: &GroupElement) -> bool { + verify_proof(public_key, message, p) +} + +/// Creates a deterministic unlinkable proof. +/// +/// Unlike [`create_proof`], this function uses a deterministic nonce derived from the message +/// rather than a random one. This means: +/// - The same inputs always produce the same proof (deterministic) +/// - Multiple signatures of the same message are identical +/// - The nonce cannot be used to link different proofs (unlinkable) +/// +/// This is useful when you want consistent proofs but don't want random values that could +/// potentially be used for linking. +/// +/// # Arguments +/// +/// * `a` - The secret scalar +/// * `gm` - The message/base group element `M` +/// +/// # Returns +/// +/// A tuple `(A, Proof)` where: +/// - `A = a*G` is the public key +/// - `Proof` is the deterministic zero-knowledge proof +/// +/// # Security Note +/// +/// The deterministic nonce is derived by hashing the message, following the Fiat-Shamir transform. +pub fn create_proof_unlinkable(a: &ScalarNonZero, gm: &GroupElement) -> (GroupElement, Proof) { + let mut hasher = Sha512::new(); + hasher.update(gm.to_bytes()); + let mut bytes = [0u8; 64]; + bytes.copy_from_slice(hasher.finalize().as_slice()); + let r = ScalarNonZero::from_hash(&bytes); + + let ga = a * G; + let gn = a * gm; + let gc1 = r * G; + let gc2 = r * gm; + + let mut hasher = Sha512::new(); + hasher.update(ga.to_bytes()); + hasher.update(gm.to_bytes()); + hasher.update(gn.to_bytes()); + hasher.update(gc1.to_bytes()); + hasher.update(gc2.to_bytes()); + let mut bytes = [0u8; 64]; + bytes.copy_from_slice(hasher.finalize().as_slice()); + let e = ScalarNonZero::from_hash(&bytes); + let s = ScalarCanBeZero((a * e).0) + ScalarCanBeZero(r.0); + ( + ga, + Proof { + n: gn, + c1: gc1, + c2: gc2, + s, + }, + ) +} + +/// Creates a deterministic unlinkable signature. +/// +/// This function creates a signature using a deterministic nonce derived from the message. +/// Unlike [`sign`], which uses random nonces: +/// - The same message and key always produce the same signature +/// - Signatures cannot be used to link different signings +/// - No random number generator is required +/// +/// # Arguments +/// +/// * `message` - The message to sign +/// * `secret_key` - The secret signing key +/// +/// # Returns +/// +/// A deterministic signature that can be verified with the corresponding public key. +/// +/// # Example +/// +/// ``` +/// # use libpep::arithmetic::group_elements::{GroupElement, G}; +/// # use libpep::arithmetic::scalars::ScalarNonZero; +/// # use libpep::core::zkps::{sign_unlinkable, verify}; +/// # let mut rng = rand::rng(); +/// let secret_key = ScalarNonZero::random(&mut rng); +/// let public_key = &secret_key * G; +/// let message = GroupElement::random(&mut rng); +/// +/// let sig1 = sign_unlinkable(&message, &secret_key); +/// let sig2 = sign_unlinkable(&message, &secret_key); +/// assert_eq!(sig1, sig2); // Same inputs produce same signature +/// assert!(verify(&message, &sig1, &public_key)); +/// ``` +pub fn sign_unlinkable(message: &GroupElement, secret_key: &ScalarNonZero) -> Signature { + create_proof_unlinkable(secret_key, message).1 +} + +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::expect_used)] +mod tests { + use crate::arithmetic::group_elements::{GroupElement, G}; + use crate::arithmetic::scalars::ScalarNonZero; + use crate::core::zkps::{create_proof, sign, sign_unlinkable, verify, verify_proof}; + + #[test] + fn elgamal_signature() { + let mut rng = rand::rng(); + // secret key + let s = ScalarNonZero::random(&mut rng); + let s2 = ScalarNonZero::random(&mut rng); + // public key + let gp = s * G; + + let v = GroupElement::random(&mut rng); + let mut signature = sign(&v, &s, &mut rng); + assert!(verify(&v, &signature, &gp)); + + signature = sign(&v, &s2, &mut rng); + assert!(!verify(&v, &signature, &gp)); + } + + #[test] + fn pep_schnorr_basic_offline() { + let mut rng = rand::rng(); + // given a secret a and public M, proof that a certain triplet (A, M, N) is actually calculated by (a*G, M, a * M) + // using Fiat-Shamir transform + + // prover + let a = ScalarNonZero::random(&mut rng); + let gm = GroupElement::random(&mut rng); + + let (ga, p) = create_proof(&a, &gm, &mut rng); + assert_eq!(a * gm, *p); + + // verifier + assert!(verify_proof(&ga, &gm, &p)); + } + + #[test] + fn elgamal_signature_unlinkable() { + let mut rng = rand::rng(); + // secret key + let s = ScalarNonZero::random(&mut rng); + // public key + let gp = s * G; + + let v = GroupElement::random(&mut rng); + let sig1 = sign_unlinkable(&v, &s); + assert!(verify(&v, &sig1, &gp)); + + let sig2 = sign_unlinkable(&v, &s); + assert!(verify(&v, &sig2, &gp)); + assert_eq!(sig1, sig2); + } +} diff --git a/src/lib/data/json/data.rs b/src/lib/data/json/data.rs index 1c4930e..81c30ef 100644 --- a/src/lib/data/json/data.rs +++ b/src/lib/data/json/data.rs @@ -507,6 +507,86 @@ impl crate::data::traits::HasStructure for EncryptedPEPJSONValue { } } +// Verifiable transcryption for JSON + +/// Proof for verifiable transcryption of a PEP JSON value. +/// +/// The structure mirrors the JSON value structure: +/// - Null has no proof +/// - Primitives (Bool, Number, String) contain attribute rekey proofs +/// - Pseudonyms contain pseudonymization proofs (RSK) +/// - Arrays and Objects contain nested proofs +#[cfg(feature = "verifiable")] +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum JSONTranscryptionProof { + Null, + Bool(crate::core::proved::VerifiableRekey), + Number(crate::core::proved::VerifiableRekey), + String(Vec), + Pseudonym { + operation_proofs: Vec, + factors_proof: crate::core::proved::RSKFactorsProof, + }, + Array(Vec>), + Object(HashMap>), +} + +#[cfg(feature = "verifiable")] +impl crate::data::traits::VerifiableTranscryptable for EncryptedPEPJSONValue { + type TranscryptionProof = JSONTranscryptionProof; + + fn verifiable_transcrypt( + &self, + info: &TranscryptionInfo, + rng: &mut R, + ) -> Self::TranscryptionProof { + use crate::data::traits::{VerifiablePseudonymizable, VerifiableRekeyable}; + + match self { + EncryptedPEPJSONValue::Null => JSONTranscryptionProof::Null, + EncryptedPEPJSONValue::Bool(enc) => { + let proof = enc.verifiable_rekey(&info.attribute, rng); + JSONTranscryptionProof::Bool(proof) + } + EncryptedPEPJSONValue::Number(enc) => { + let proof = enc.verifiable_rekey(&info.attribute, rng); + JSONTranscryptionProof::Number(proof) + } + EncryptedPEPJSONValue::String(enc) => { + let proofs = enc.verifiable_rekey(&info.attribute, rng); + JSONTranscryptionProof::String(proofs) + } + EncryptedPEPJSONValue::Pseudonym(enc) => { + let operation_proofs = enc.verifiable_pseudonymize(&info.pseudonym, rng); + let factors_proof = crate::core::proved::RSKFactorsProof::new( + &info.pseudonym.s.0, + &info.pseudonym.k.0, + rng, + ); + JSONTranscryptionProof::Pseudonym { + operation_proofs, + factors_proof, + } + } + EncryptedPEPJSONValue::Array(arr) => { + let proofs = arr + .iter() + .map(|x| Box::new(x.verifiable_transcrypt(info, rng))) + .collect(); + JSONTranscryptionProof::Array(proofs) + } + EncryptedPEPJSONValue::Object(obj) => { + let proofs = obj + .iter() + .map(|(k, v)| (k.clone(), Box::new(v.verifiable_transcrypt(info, rng)))) + .collect(); + JSONTranscryptionProof::Object(proofs) + } + } + } +} + #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used)] mod tests { diff --git a/src/lib/data/long.rs b/src/lib/data/long.rs index 4ed1b6e..ff22b13 100644 --- a/src/lib/data/long.rs +++ b/src/lib/data/long.rs @@ -828,6 +828,55 @@ impl Transcryptable for LongEncryptedAttribute { } } +// Verifiable operations for long types - trait implementations +#[cfg(feature = "verifiable")] +impl crate::data::traits::VerifiablePseudonymizable for LongEncryptedPseudonym { + type PseudonymizationProof = Vec; + + fn verifiable_pseudonymize( + &self, + info: &crate::factors::PseudonymizationInfo, + rng: &mut R, + ) -> Self::PseudonymizationProof { + self.0 + .iter() + .map(|block| block.verifiable_pseudonymize(info, rng)) + .collect() + } +} + +#[cfg(feature = "verifiable")] +impl crate::data::traits::VerifiableRekeyable for LongEncryptedPseudonym { + type RekeyProof = Vec; + + fn verifiable_rekey( + &self, + info: &Self::RekeyInfo, + rng: &mut R, + ) -> Self::RekeyProof { + self.0 + .iter() + .map(|block| block.verifiable_rekey(info, rng)) + .collect() + } +} + +#[cfg(feature = "verifiable")] +impl crate::data::traits::VerifiableRekeyable for LongEncryptedAttribute { + type RekeyProof = Vec; + + fn verifiable_rekey( + &self, + info: &Self::RekeyInfo, + rng: &mut R, + ) -> Self::RekeyProof { + self.0 + .iter() + .map(|block| block.verifiable_rekey(info, rng)) + .collect() + } +} + #[cfg(feature = "batch")] impl crate::data::traits::HasStructure for LongEncryptedPseudonym { type Structure = usize; diff --git a/src/lib/data/records.rs b/src/lib/data/records.rs index 0f8b5a4..9a8d68d 100644 --- a/src/lib/data/records.rs +++ b/src/lib/data/records.rs @@ -9,6 +9,11 @@ use crate::factors::TranscryptionInfo; use crate::keys::{GlobalPublicKeys, SessionKeys}; use rand_core::{CryptoRng, RngCore}; +#[cfg(feature = "verifiable")] +use crate::core::proved::{RSKFactorsProof, VerifiableRSK, VerifiableRekey}; +#[cfg(feature = "verifiable")] +use crate::data::traits::VerifiableTranscryptable; + #[cfg(feature = "long")] use crate::data::long::{ LongAttribute, LongEncryptedAttribute, LongEncryptedPseudonym, LongPseudonym, @@ -527,3 +532,112 @@ impl HasStructure for LongEncryptedRecord { } } } + +// Verifiable transcryption + +/// Proof bundle for verifiable transcryption of a simple record. +/// +/// Contains proofs for both pseudonymization and attribute rekeying. +#[cfg(feature = "verifiable")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RecordTranscryptionProof { + /// Operation proofs for each pseudonym (RSK proofs) + pub pseudonym_operation_proofs: Vec, + /// Shared factors proof for all pseudonyms + pub pseudonym_factors_proof: RSKFactorsProof, + /// Operation proofs for each attribute (Rekey proofs) + pub attribute_operation_proofs: Vec, +} + +/// Proof bundle for verifiable transcryption of a long record. +/// +/// Contains proofs for both pseudonymization and attribute rekeying, +/// with multiple proofs per long pseudonym/attribute (one per block). +#[cfg(feature = "verifiable")] +#[cfg(feature = "long")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LongRecordTranscryptionProof { + /// Operation proofs for each long pseudonym (vectors of RSK proofs) + pub pseudonym_operation_proofs: Vec>, + /// Shared factors proof for all pseudonyms + pub pseudonym_factors_proof: RSKFactorsProof, + /// Operation proofs for each long attribute (vectors of Rekey proofs) + pub attribute_operation_proofs: Vec>, +} + +#[cfg(feature = "verifiable")] +impl VerifiableTranscryptable for EncryptedRecord { + type TranscryptionProof = RecordTranscryptionProof; + + fn verifiable_transcrypt( + &self, + info: &TranscryptionInfo, + rng: &mut R, + ) -> Self::TranscryptionProof { + use crate::data::traits::{VerifiablePseudonymizable, VerifiableRekeyable}; + + let mut pseudonym_operation_proofs = Vec::with_capacity(self.pseudonyms.len()); + + // Generate proofs for all pseudonyms + for pseudonym in &self.pseudonyms { + let operation_proof = pseudonym.verifiable_pseudonymize(&info.pseudonym, rng); + pseudonym_operation_proofs.push(operation_proof); + } + + // Generate shared factors proof once (not message-specific) + let pseudonym_factors_proof = + RSKFactorsProof::new(&info.pseudonym.s.0, &info.pseudonym.k.0, rng); + + // Generate proofs for all attributes + let mut attribute_operation_proofs = Vec::with_capacity(self.attributes.len()); + for attribute in &self.attributes { + let operation_proof = attribute.verifiable_rekey(&info.attribute, rng); + attribute_operation_proofs.push(operation_proof); + } + + RecordTranscryptionProof { + pseudonym_operation_proofs, + pseudonym_factors_proof, + attribute_operation_proofs, + } + } +} + +#[cfg(feature = "verifiable")] +#[cfg(feature = "long")] +impl VerifiableTranscryptable for LongEncryptedRecord { + type TranscryptionProof = LongRecordTranscryptionProof; + + fn verifiable_transcrypt( + &self, + info: &TranscryptionInfo, + rng: &mut R, + ) -> Self::TranscryptionProof { + use crate::data::traits::{VerifiablePseudonymizable, VerifiableRekeyable}; + + let mut pseudonym_operation_proofs = Vec::with_capacity(self.pseudonyms.len()); + + // Generate proofs for all long pseudonyms (returns Vec per pseudonym) + for pseudonym in &self.pseudonyms { + let operation_proofs = pseudonym.verifiable_pseudonymize(&info.pseudonym, rng); + pseudonym_operation_proofs.push(operation_proofs); + } + + // Generate shared factors proof once (not message-specific) + let pseudonym_factors_proof = + RSKFactorsProof::new(&info.pseudonym.s.0, &info.pseudonym.k.0, rng); + + // Generate proofs for all long attributes (returns Vec per attribute) + let mut attribute_operation_proofs = Vec::with_capacity(self.attributes.len()); + for attribute in &self.attributes { + let operation_proofs = attribute.verifiable_rekey(&info.attribute, rng); + attribute_operation_proofs.push(operation_proofs); + } + + LongRecordTranscryptionProof { + pseudonym_operation_proofs, + pseudonym_factors_proof, + attribute_operation_proofs, + } + } +} diff --git a/src/lib/data/simple.rs b/src/lib/data/simple.rs index f71190e..d69f690 100644 --- a/src/lib/data/simple.rs +++ b/src/lib/data/simple.rs @@ -495,6 +495,47 @@ impl Transcryptable for EncryptedAttribute { self.rekey(&info.attribute) } } + +// Verifiable operations - trait implementations +#[cfg(feature = "verifiable")] +impl crate::data::traits::VerifiablePseudonymizable for EncryptedPseudonym { + type PseudonymizationProof = crate::core::proved::VerifiableRSK; + + fn verifiable_pseudonymize( + &self, + info: &crate::factors::PseudonymizationInfo, + rng: &mut R, + ) -> Self::PseudonymizationProof { + crate::core::proved::VerifiableRSK::new(self.value(), &info.s.0, &info.k.0, rng) + } +} + +#[cfg(feature = "verifiable")] +impl crate::data::traits::VerifiableRekeyable for EncryptedPseudonym { + type RekeyProof = crate::core::proved::VerifiableRekey; + + fn verifiable_rekey( + &self, + info: &Self::RekeyInfo, + rng: &mut R, + ) -> Self::RekeyProof { + crate::core::proved::VerifiableRekey::new(self.value(), &info.0, rng) + } +} + +#[cfg(feature = "verifiable")] +impl crate::data::traits::VerifiableRekeyable for EncryptedAttribute { + type RekeyProof = crate::core::proved::VerifiableRekey; + + fn verifiable_rekey( + &self, + info: &Self::RekeyInfo, + rng: &mut R, + ) -> Self::RekeyProof { + crate::core::proved::VerifiableRekey::new(self.value(), &info.0, rng) + } +} + #[cfg(feature = "batch")] impl crate::data::traits::HasStructure for EncryptedPseudonym { type Structure = (); diff --git a/src/lib/data/traits.rs b/src/lib/data/traits.rs index 447bb75..a0ab93a 100644 --- a/src/lib/data/traits.rs +++ b/src/lib/data/traits.rs @@ -162,3 +162,91 @@ pub trait HasStructure { /// Get the structure of this encrypted value. fn structure(&self) -> Self::Structure; } + +// Verifiable operation traits + +/// A trait for encrypted pseudonyms that support verifiable pseudonymization. +/// +/// This trait extends [`Pseudonymizable`] to provide zero-knowledge proofs +/// that pseudonymization operations were performed correctly. +/// +/// The proof contains the result, which can be extracted via `.result()`. +#[cfg(feature = "verifiable")] +pub trait VerifiablePseudonymizable: Pseudonymizable { + /// The proof type for pseudonymization operations. + /// - Simple types use a single proof + /// - Long types use `Vec` of proofs + type PseudonymizationProof; + + /// Pseudonymize with proof generation. + /// + /// Returns an operation proof which contains the result. + /// The result can be extracted from the operation proof via `.result()`. + /// + /// Note: The factors proof (RSKFactorsProof) is not message-specific and should + /// be generated once per pseudonymization info, not per message. + fn verifiable_pseudonymize( + &self, + info: &PseudonymizationInfo, + rng: &mut R, + ) -> Self::PseudonymizationProof; +} + +/// A trait for encrypted types that support verifiable rekeying. +/// +/// This trait extends [`Rekeyable`] to provide zero-knowledge proofs +/// that rekey operations were performed correctly. +/// +/// The proof contains the result, which can be extracted via `.result(original)`. +#[cfg(feature = "verifiable")] +pub trait VerifiableRekeyable: Rekeyable { + /// The proof type for rekey operations. + /// - Simple types use a single proof + /// - Long types use `Vec` of proofs + type RekeyProof; + + /// Rekey with proof generation. + /// + /// Returns an operation proof which contains the result. + /// The result can be extracted from the proof via `.result(original)`. + fn verifiable_rekey( + &self, + info: &Self::RekeyInfo, + rng: &mut R, + ) -> Self::RekeyProof; +} + +/// A trait for encrypted types that support verifiable transcryption. +/// +/// This trait extends [`Transcryptable`] to provide zero-knowledge proofs +/// that transcryption operations were performed correctly. +/// +/// Transcryption combines: +/// - For pseudonyms: verifiable pseudonymization (reshuffle + rekey with proofs) +/// - For attributes: verifiable rekeying (rekey with proofs) +/// - For composite types (JSON, Records): recursive verifiable transcryption +/// +/// The proof structure varies by type: +/// - Simple records: contains vectors of proofs for pseudonyms and attributes +/// - Long records: contains vectors of vectors of proofs +/// - JSON values: nested proof structure matching the JSON shape +#[cfg(feature = "verifiable")] +pub trait VerifiableTranscryptable: Transcryptable { + /// The proof type for transcryption operations. + /// Structure depends on the complexity of the data type. + type TranscryptionProof; + + /// Transcrypt with proof generation. + /// + /// Returns a proof bundle containing: + /// - Operation proofs for pseudonymization (RSK proofs) + /// - Factors proof for pseudonymization + /// - Operation proofs for rekeying attributes + /// + /// The result can be extracted from the proofs. + fn verifiable_transcrypt( + &self, + info: &TranscryptionInfo, + rng: &mut R, + ) -> Self::TranscryptionProof; +} diff --git a/src/lib/factors/commitments.rs b/src/lib/factors/commitments.rs new file mode 100644 index 0000000..e830340 --- /dev/null +++ b/src/lib/factors/commitments.rs @@ -0,0 +1,54 @@ +//! Commitment types bundling public commitments with their proofs. + +#[cfg(feature = "verifiable")] +use crate::core::proved::{ + PseudonymizationFactorCommitments, PseudonymizationFactorCommitmentsProof, + RekeyFactorCommitments, RekeyFactorCommitmentsProof, +}; + +/// Pseudonymization factor commitments bundled with their proofs. +/// +/// This struct contains both the reshuffle and rekey commitments along with +/// their correctness proofs. It's used for verifiable pseudonymization operations. +#[cfg(feature = "verifiable")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ProvedPseudonymizationCommitments { + /// Public commitments for the reshuffle factor + pub reshuffle_commitments: PseudonymizationFactorCommitments, + /// Proof that reshuffle commitments are correct + pub reshuffle_proof: PseudonymizationFactorCommitmentsProof, + /// Public commitments for the rekey factor + pub rekey_commitments: RekeyFactorCommitments, + /// Proof that rekey commitments are correct + pub rekey_proof: RekeyFactorCommitmentsProof, +} + +/// Reshuffle factor commitments bundled with their proof. +/// +/// This struct contains the reshuffle commitments along with their correctness proof. +/// It's used for verifiable reshuffling operations (user-specific, per domain). +#[cfg(feature = "verifiable")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ProvedReshuffleCommitments { + /// Public commitments for the reshuffle factor + pub commitments: PseudonymizationFactorCommitments, + /// Proof that commitments are correct + pub proof: PseudonymizationFactorCommitmentsProof, +} + +/// Rekey factor commitments bundled with their proof. +/// +/// This struct contains the rekey commitments along with their correctness proof. +/// It's used for verifiable rekey operations on both pseudonyms and attributes +/// (session-specific, per encryption context). +#[cfg(feature = "verifiable")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ProvedRekeyCommitments { + /// Public commitments for the rekey factor + pub commitments: RekeyFactorCommitments, + /// Proof that commitments are correct + pub proof: RekeyFactorCommitmentsProof, +} diff --git a/src/lib/factors/mod.rs b/src/lib/factors/mod.rs index 0873d5a..375a09a 100644 --- a/src/lib/factors/mod.rs +++ b/src/lib/factors/mod.rs @@ -10,12 +10,14 @@ //! - [`contexts`]: Context types (PseudonymizationDomain, EncryptionContext) //! - [`secrets`]: Secret types (PseudonymizationSecret, EncryptionSecret) //! - [`types`]: Factor types and Info type aliases -//! - [`derivation`]: Functions for deriving factors from contexts and secrets pub mod contexts; pub mod secrets; pub mod types; +#[cfg(feature = "verifiable")] +pub mod commitments; + #[cfg(feature = "python")] pub mod py; @@ -33,3 +35,8 @@ pub use types::{ PseudonymRekeyInfo, PseudonymizationInfo, RekeyFactor, RerandomizeFactor, ReshuffleFactor, TranscryptionInfo, }; + +#[cfg(feature = "verifiable")] +pub use commitments::{ + ProvedPseudonymizationCommitments, ProvedRekeyCommitments, ProvedReshuffleCommitments, +}; diff --git a/src/lib/factors/py/commitments.rs b/src/lib/factors/py/commitments.rs new file mode 100644 index 0000000..190ebf3 --- /dev/null +++ b/src/lib/factors/py/commitments.rs @@ -0,0 +1,112 @@ +//! Python bindings for commitment types. + +use crate::factors::{ + ProvedPseudonymizationCommitments, ProvedRekeyCommitments, ProvedReshuffleCommitments, +}; +use pyo3::prelude::*; + +#[cfg(feature = "serde")] +use pyo3::exceptions::PyValueError; + +/// Pseudonymization factor commitments with proofs (Python). +#[pyclass(name = "ProvedPseudonymizationCommitments")] +#[derive(Clone)] +pub struct PyProvedPseudonymizationCommitments { + pub(crate) inner: ProvedPseudonymizationCommitments, +} + +#[pymethods] +impl PyProvedPseudonymizationCommitments { + /// Serialize to JSON. + #[cfg(feature = "serde")] + fn to_json(&self) -> PyResult { + serde_json::to_string(&self.inner) + .map_err(|e| PyValueError::new_err(format!("Serialization failed: {}", e))) + } + + /// Deserialize from JSON. + #[cfg(feature = "serde")] + #[staticmethod] + fn from_json(json: &str) -> PyResult { + serde_json::from_str(json) + .map(|inner| PyProvedPseudonymizationCommitments { inner }) + .map_err(|e| PyValueError::new_err(format!("Deserialization failed: {}", e))) + } +} + +impl From for PyProvedPseudonymizationCommitments { + fn from(inner: ProvedPseudonymizationCommitments) -> Self { + PyProvedPseudonymizationCommitments { inner } + } +} + +/// Reshuffle factor commitments with proof (Python). +#[pyclass(name = "ProvedReshuffleCommitments")] +#[derive(Clone)] +pub struct PyProvedReshuffleCommitments { + pub(crate) inner: ProvedReshuffleCommitments, +} + +#[pymethods] +impl PyProvedReshuffleCommitments { + /// Serialize to JSON. + #[cfg(feature = "serde")] + fn to_json(&self) -> PyResult { + serde_json::to_string(&self.inner) + .map_err(|e| PyValueError::new_err(format!("Serialization failed: {}", e))) + } + + /// Deserialize from JSON. + #[cfg(feature = "serde")] + #[staticmethod] + fn from_json(json: &str) -> PyResult { + serde_json::from_str(json) + .map(|inner| PyProvedReshuffleCommitments { inner }) + .map_err(|e| PyValueError::new_err(format!("Deserialization failed: {}", e))) + } +} + +impl From for PyProvedReshuffleCommitments { + fn from(inner: ProvedReshuffleCommitments) -> Self { + PyProvedReshuffleCommitments { inner } + } +} + +/// Rekey factor commitments with proof (Python). +#[pyclass(name = "ProvedRekeyCommitments")] +#[derive(Clone)] +pub struct PyProvedRekeyCommitments { + pub(crate) inner: ProvedRekeyCommitments, +} + +#[pymethods] +impl PyProvedRekeyCommitments { + /// Serialize to JSON. + #[cfg(feature = "serde")] + fn to_json(&self) -> PyResult { + serde_json::to_string(&self.inner) + .map_err(|e| PyValueError::new_err(format!("Serialization failed: {}", e))) + } + + /// Deserialize from JSON. + #[cfg(feature = "serde")] + #[staticmethod] + fn from_json(json: &str) -> PyResult { + serde_json::from_str(json) + .map(|inner| PyProvedRekeyCommitments { inner }) + .map_err(|e| PyValueError::new_err(format!("Deserialization failed: {}", e))) + } +} + +impl From for PyProvedRekeyCommitments { + fn from(inner: ProvedRekeyCommitments) -> Self { + PyProvedRekeyCommitments { inner } + } +} + +pub(crate) fn register_commitment_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { + parent_module.add_class::()?; + parent_module.add_class::()?; + parent_module.add_class::()?; + Ok(()) +} diff --git a/src/lib/factors/py/mod.rs b/src/lib/factors/py/mod.rs index 428f7f2..c49c764 100644 --- a/src/lib/factors/py/mod.rs +++ b/src/lib/factors/py/mod.rs @@ -4,6 +4,9 @@ pub mod contexts; pub mod secrets; pub mod types; +#[cfg(feature = "verifiable")] +pub mod commitments; + pub use contexts::{ PyAttributeRekeyInfo, PyEncryptionContext, PyPseudonymizationDomain, PyPseudonymizationInfo, PyTranscryptionInfo, @@ -15,3 +18,8 @@ pub use secrets::{ pub use types::{ PyAttributeRekeyFactor, PyPseudonymRekeyFactor, PyRerandomizeFactor, PyReshuffleFactor, }; + +#[cfg(feature = "verifiable")] +pub use commitments::{ + PyProvedPseudonymizationCommitments, PyProvedRekeyCommitments, PyProvedReshuffleCommitments, +}; diff --git a/src/lib/factors/secrets.rs b/src/lib/factors/secrets.rs index 8a26063..130943c 100644 --- a/src/lib/factors/secrets.rs +++ b/src/lib/factors/secrets.rs @@ -172,70 +172,3 @@ fn make_factor(secret: &Secret, typ: u32, audience_type: u32, payload: &String) bytes.copy_from_slice(&result_outer); ScalarNonZero::from_hash(&bytes) } - -// Info implementations with new() and reverse() methods - -impl PseudonymizationInfo { - /// Compute the pseudonymization info given pseudonymization domains, sessions and secrets. - pub fn new( - domain_from: &PseudonymizationDomain, - domain_to: &PseudonymizationDomain, - session_from: &EncryptionContext, - session_to: &EncryptionContext, - pseudonymization_secret: &PseudonymizationSecret, - encryption_secret: &EncryptionSecret, - ) -> Self { - let s_from = make_pseudonymisation_factor(pseudonymization_secret, domain_from); - let s_to = make_pseudonymisation_factor(pseudonymization_secret, domain_to); - let reshuffle_factor = ReshuffleFactor(s_from.0.invert() * s_to.0); - let rekey_factor = PseudonymRekeyInfo::new(session_from, session_to, encryption_secret); - Self { - s: reshuffle_factor, - k: rekey_factor, - } - } - - /// Reverse the pseudonymization info (i.e., switch the direction of the pseudonymization). - pub fn reverse(&self) -> Self { - Self { - s: ReshuffleFactor(self.s.0.invert()), - k: PseudonymRekeyFactor(self.k.0.invert()), - } - } -} - -impl PseudonymRekeyInfo { - /// Compute the rekey info for pseudonyms given sessions and secrets. - pub fn new( - session_from: &EncryptionContext, - session_to: &EncryptionContext, - encryption_secret: &EncryptionSecret, - ) -> Self { - let k_from = make_pseudonym_rekey_factor(encryption_secret, session_from); - let k_to = make_pseudonym_rekey_factor(encryption_secret, session_to); - PseudonymRekeyFactor(k_from.0.invert() * k_to.0) - } - - /// Reverse the rekey info (i.e., switch the direction of the rekeying). - pub fn reverse(&self) -> Self { - PseudonymRekeyFactor(self.0.invert()) - } -} - -impl AttributeRekeyInfo { - /// Compute the rekey info for attributes given sessions and secrets. - pub fn new( - session_from: &EncryptionContext, - session_to: &EncryptionContext, - encryption_secret: &EncryptionSecret, - ) -> Self { - let k_from = make_attribute_rekey_factor(encryption_secret, session_from); - let k_to = make_attribute_rekey_factor(encryption_secret, session_to); - AttributeRekeyFactor(k_from.0.invert() * k_to.0) - } - - /// Reverse the rekey info (i.e., switch the direction of the rekeying). - pub fn reverse(&self) -> Self { - AttributeRekeyFactor(self.0.invert()) - } -} diff --git a/src/lib/factors/types.rs b/src/lib/factors/types.rs index e8ed982..8c06eed 100644 --- a/src/lib/factors/types.rs +++ b/src/lib/factors/types.rs @@ -1,6 +1,10 @@ //! Cryptographic factor types for rerandomization, reshuffling, and rekeying operations. use crate::arithmetic::scalars::ScalarNonZero; +use crate::factors::{ + make_attribute_rekey_factor, make_pseudonym_rekey_factor, make_pseudonymisation_factor, + EncryptionContext, EncryptionSecret, PseudonymizationDomain, PseudonymizationSecret, +}; use derive_more::From; /// High-level type for the factor used to [`rerandomize`](crate::core::primitives::rerandomize) an [ElGamal](crate::core::elgamal::ElGamal) ciphertext. @@ -75,15 +79,80 @@ pub struct TranscryptionInfo { pub attribute: AttributeRekeyInfo, } +impl PseudonymizationInfo { + /// Compute the pseudonymization info given pseudonymization domains, sessions and secrets. + pub fn new( + domain_from: &PseudonymizationDomain, + domain_to: &PseudonymizationDomain, + session_from: &EncryptionContext, + session_to: &EncryptionContext, + pseudonymization_secret: &PseudonymizationSecret, + encryption_secret: &EncryptionSecret, + ) -> Self { + let s_from = make_pseudonymisation_factor(pseudonymization_secret, domain_from); + let s_to = make_pseudonymisation_factor(pseudonymization_secret, domain_to); + let reshuffle_factor = ReshuffleFactor(s_from.0.invert() * s_to.0); + let rekey_factor = PseudonymRekeyInfo::new(session_from, session_to, encryption_secret); + Self { + s: reshuffle_factor, + k: rekey_factor, + } + } + + /// Reverse the pseudonymization info (i.e., switch the direction of the pseudonymization). + pub fn reverse(&self) -> Self { + Self { + s: ReshuffleFactor(self.s.0.invert()), + k: PseudonymRekeyFactor(self.k.0.invert()), + } + } +} + +impl PseudonymRekeyInfo { + /// Compute the rekey info for pseudonyms given sessions and secrets. + pub fn new( + session_from: &EncryptionContext, + session_to: &EncryptionContext, + encryption_secret: &EncryptionSecret, + ) -> Self { + let k_from = make_pseudonym_rekey_factor(encryption_secret, session_from); + let k_to = make_pseudonym_rekey_factor(encryption_secret, session_to); + PseudonymRekeyFactor(k_from.0.invert() * k_to.0) + } + + /// Reverse the rekey info (i.e., switch the direction of the rekeying). + pub fn reverse(&self) -> Self { + PseudonymRekeyFactor(self.0.invert()) + } +} + +impl AttributeRekeyInfo { + /// Compute the rekey info for attributes given sessions and secrets. + pub fn new( + session_from: &EncryptionContext, + session_to: &EncryptionContext, + encryption_secret: &EncryptionSecret, + ) -> Self { + let k_from = make_attribute_rekey_factor(encryption_secret, session_from); + let k_to = make_attribute_rekey_factor(encryption_secret, session_to); + AttributeRekeyFactor(k_from.0.invert() * k_to.0) + } + + /// Reverse the rekey info (i.e., switch the direction of the rekeying). + pub fn reverse(&self) -> Self { + AttributeRekeyFactor(self.0.invert()) + } +} + impl TranscryptionInfo { /// Compute the transcryption info given pseudonymization domains, sessions and secrets. pub fn new( - domain_from: &crate::factors::contexts::PseudonymizationDomain, - domain_to: &crate::factors::contexts::PseudonymizationDomain, - session_from: &crate::factors::contexts::EncryptionContext, - session_to: &crate::factors::contexts::EncryptionContext, - pseudonymization_secret: &crate::factors::PseudonymizationSecret, - encryption_secret: &crate::factors::EncryptionSecret, + domain_from: &PseudonymizationDomain, + domain_to: &PseudonymizationDomain, + session_from: &EncryptionContext, + session_to: &EncryptionContext, + pseudonymization_secret: &PseudonymizationSecret, + encryption_secret: &EncryptionSecret, ) -> Self { Self { pseudonym: PseudonymizationInfo::new( diff --git a/src/lib/factors/wasm/commitments.rs b/src/lib/factors/wasm/commitments.rs new file mode 100644 index 0000000..a88ff4e --- /dev/null +++ b/src/lib/factors/wasm/commitments.rs @@ -0,0 +1,79 @@ +//! WASM bindings for commitment types. + +use crate::factors::{ + ProvedPseudonymizationCommitments, ProvedRekeyCommitments, ProvedReshuffleCommitments, +}; +use derive_more::{Deref, From, Into}; +use wasm_bindgen::prelude::*; + +/// Pseudonymization factor commitments with proofs (WASM). +#[derive(Clone, From, Into, Deref)] +#[wasm_bindgen(js_name = ProvedPseudonymizationCommitments)] +pub struct WASMProvedPseudonymizationCommitments(pub(crate) ProvedPseudonymizationCommitments); + +#[wasm_bindgen(js_class = ProvedPseudonymizationCommitments)] +impl WASMProvedPseudonymizationCommitments { + /// Serialize to JSON. + #[cfg(feature = "serde")] + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> Result { + serde_json::to_string(&self.0).map_err(|e| JsValue::from_str(&format!("{}", e))) + } + + /// Deserialize from JSON. + #[cfg(feature = "serde")] + #[wasm_bindgen(js_name = fromJSON)] + pub fn from_json(json: &str) -> Result { + serde_json::from_str(json) + .map(WASMProvedPseudonymizationCommitments) + .map_err(|e| JsValue::from_str(&format!("{}", e))) + } +} + +/// Reshuffle factor commitments with proof (WASM). +#[derive(Clone, From, Into, Deref)] +#[wasm_bindgen(js_name = ProvedReshuffleCommitments)] +pub struct WASMProvedReshuffleCommitments(pub(crate) ProvedReshuffleCommitments); + +#[wasm_bindgen(js_class = ProvedReshuffleCommitments)] +impl WASMProvedReshuffleCommitments { + /// Serialize to JSON. + #[cfg(feature = "serde")] + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> Result { + serde_json::to_string(&self.0).map_err(|e| JsValue::from_str(&format!("{}", e))) + } + + /// Deserialize from JSON. + #[cfg(feature = "serde")] + #[wasm_bindgen(js_name = fromJSON)] + pub fn from_json(json: &str) -> Result { + serde_json::from_str(json) + .map(WASMProvedReshuffleCommitments) + .map_err(|e| JsValue::from_str(&format!("{}", e))) + } +} + +/// Rekey factor commitments with proof (WASM). +#[derive(Clone, From, Into, Deref)] +#[wasm_bindgen(js_name = ProvedRekeyCommitments)] +pub struct WASMProvedRekeyCommitments(pub(crate) ProvedRekeyCommitments); + +#[wasm_bindgen(js_class = ProvedRekeyCommitments)] +impl WASMProvedRekeyCommitments { + /// Serialize to JSON. + #[cfg(feature = "serde")] + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> Result { + serde_json::to_string(&self.0).map_err(|e| JsValue::from_str(&format!("{}", e))) + } + + /// Deserialize from JSON. + #[cfg(feature = "serde")] + #[wasm_bindgen(js_name = fromJSON)] + pub fn from_json(json: &str) -> Result { + serde_json::from_str(json) + .map(WASMProvedRekeyCommitments) + .map_err(|e| JsValue::from_str(&format!("{}", e))) + } +} diff --git a/src/lib/factors/wasm/mod.rs b/src/lib/factors/wasm/mod.rs index 8639100..d98c831 100644 --- a/src/lib/factors/wasm/mod.rs +++ b/src/lib/factors/wasm/mod.rs @@ -4,6 +4,12 @@ pub mod contexts; pub mod secrets; pub mod types; +#[cfg(feature = "verifiable")] +pub mod commitments; + pub use contexts::*; pub use secrets::*; pub use types::*; + +#[cfg(feature = "verifiable")] +pub use commitments::*; diff --git a/src/lib/keys/distribution/mod.rs b/src/lib/keys/distribution/mod.rs index 024626f..235194f 100644 --- a/src/lib/keys/distribution/mod.rs +++ b/src/lib/keys/distribution/mod.rs @@ -8,11 +8,18 @@ //! - [`blinding`]: Blinding factors and blinded global secret keys //! - [`shares`]: Session key shares for transcryptors //! - [`setup`]: System setup functions for creating distributed keys +//! - [`proofs`]: Zero-knowledge proofs for session key shares (verifiable feature) pub mod blinding; pub mod setup; pub mod shares; +#[cfg(feature = "verifiable")] +pub mod proofs; + pub use blinding::*; pub use setup::*; pub use shares::*; + +#[cfg(feature = "verifiable")] +pub use proofs::*; diff --git a/src/lib/keys/distribution/proofs.rs b/src/lib/keys/distribution/proofs.rs new file mode 100644 index 0000000..d3c0100 --- /dev/null +++ b/src/lib/keys/distribution/proofs.rs @@ -0,0 +1,232 @@ +//! Session key share proofs for distributed key generation. +//! +//! This module implements zero-knowledge proofs for session key shares in distributed +//! transcryption scenarios. When multiple transcryptors establish session keys through +//! session key shares, each share u_i = b_i * k_i must be proven to be correctly +//! constructed without revealing the secret factors. + +use crate::arithmetic::group_elements::{GroupElement, G}; +use crate::arithmetic::scalars::ScalarNonZero; +use crate::core::zkps::{create_proof, verify_proof, Proof}; +use rand_core::{CryptoRng, RngCore}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Commitment to a blinding factor b_i. +/// +/// This is B_i = b_i * G, a public commitment to the secret blinding value. +/// The blinding commitment is preconfigured and shared with verifiers to enable +/// verification of session key share proofs. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct BlindingCommitment(pub GroupElement); + +impl BlindingCommitment { + /// Create a new blinding commitment from a blinding factor. + /// + /// Computes B_i = b_i * G. + pub fn new(blinding: &ScalarNonZero) -> Self { + Self(blinding * G) + } + + /// Get the commitment value. + pub fn value(&self) -> &GroupElement { + &self.0 + } +} + +/// Proof that a session key share was correctly constructed. +/// +/// This is a ZKP(U_i; b_i; K_i) proving that: +/// - u_i = b_i * k_i (session key share) +/// - U_i = u_i * G (public commitment to the share) +/// - Using preconfigured B_i = b_i * G (blinding commitment) +/// - Using K_i from stored factor commitments (rekey factor commitment) +/// +/// # Security Note +/// +/// This proof should only be shared with the user requesting the session key, +/// as u_i must remain secret. Unlike transcryption proofs which can be public, +/// session key share proofs contain information that could compromise security +/// if shared publicly. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct SessionKeyShareProof { + /// U_i = u_i * G (public commitment to session key share) + pub share_commitment: GroupElement, + /// Zero-knowledge proof that u_i = b_i * k_i + pub proof: Proof, +} + +impl SessionKeyShareProof { + /// Create a session key share proof. + /// + /// Proves that the session key share u_i was correctly constructed as u_i = b_i * k_i. + /// + /// # Arguments + /// + /// * `blinding` - The blinding factor b_i (kept secret by transcryptor) + /// * `rekey_factor` - The rekey factor k_i (kept secret by transcryptor) + /// * `rekey_commitment` - Public commitment K_i = k_i * G + /// * `rng` - Random number generator + /// + /// # Returns + /// + /// A proof that can be verified by the user to confirm the session key share + /// was constructed correctly. + pub fn new( + blinding: &ScalarNonZero, + rekey_factor: &ScalarNonZero, + rekey_commitment: &GroupElement, + rng: &mut R, + ) -> Self { + // Compute u_i = b_i * k_i (session key share contribution) + let share = blinding * rekey_factor; + + // Create U_i = u_i * G (public commitment) + let share_commitment = share * G; + + // Create ZKP proving knowledge of b_i such that: + // - share_commitment = b_i * rekey_commitment + // - (which implies u_i = b_i * k_i since K_i = k_i * G) + let (_, proof) = create_proof(blinding, rekey_commitment, rng); + + Self { + share_commitment, + proof, + } + } + + /// Verify a session key share proof. + /// + /// Checks that: + /// 1. The proof is valid (proves knowledge of b_i) + /// 2. U_i = b_i * K_i (using the blinding commitment) + /// + /// # Arguments + /// + /// * `blinding_commitment` - B_i = b_i * G (preconfigured commitment) + /// * `rekey_commitment` - K_i = k_i * G (from factor commitments) + /// + /// # Returns + /// + /// `true` if the proof is valid, `false` otherwise + pub fn verify( + &self, + blinding_commitment: &BlindingCommitment, + rekey_commitment: &GroupElement, + ) -> bool { + // Verify the ZKP + // This confirms: share_commitment = b_i * rekey_commitment + // which means: U_i = b_i * K_i = b_i * (k_i * G) = (b_i * k_i) * G = u_i * G + verify_proof(&blinding_commitment.0, rekey_commitment, &self.proof) + } + + /// Get the public commitment to the session key share. + /// + /// Returns U_i = u_i * G. + /// + /// The user should verify this matches the commitment before accepting the share. + pub fn share_commitment(&self) -> &GroupElement { + &self.share_commitment + } +} + +/// Bundle of blinding commitments for a transcryptor. +/// +/// Contains commitments B_i = b_i * G for both pseudonym and attribute blinding factors. +/// These are preconfigured and shared with users to enable verification of session key shares. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct BlindingCommitments { + /// Blinding commitment for pseudonym session keys + pub pseudonym: BlindingCommitment, + /// Blinding commitment for attribute session keys + pub attribute: BlindingCommitment, +} + +impl BlindingCommitments { + /// Create blinding commitments from blinding factors. + pub fn new(pseudonym_blinding: &ScalarNonZero, attribute_blinding: &ScalarNonZero) -> Self { + Self { + pseudonym: BlindingCommitment::new(pseudonym_blinding), + attribute: BlindingCommitment::new(attribute_blinding), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_blinding_commitment() { + let mut rng = rand::rng(); + let blinding = ScalarNonZero::random(&mut rng); + let commitment = BlindingCommitment::new(&blinding); + + assert_eq!(*commitment.value(), blinding * G); + } + + #[test] + fn test_session_key_share_proof_valid() { + let mut rng = rand::rng(); + + // Create secret factors + let blinding = ScalarNonZero::random(&mut rng); + let rekey_factor = ScalarNonZero::random(&mut rng); + + // Create commitments + let blinding_commitment = BlindingCommitment::new(&blinding); + let rekey_commitment = rekey_factor * G; + + // Create proof + let proof = + SessionKeyShareProof::new(&blinding, &rekey_factor, &rekey_commitment, &mut rng); + + // Verify proof + assert!(proof.verify(&blinding_commitment, &rekey_commitment)); + + // Verify U_i = (b_i * k_i) * G + let expected_share_commitment = (blinding * rekey_factor) * G; + assert_eq!(*proof.share_commitment(), expected_share_commitment); + } + + #[test] + fn test_session_key_share_proof_wrong_blinding() { + let mut rng = rand::rng(); + + let blinding = ScalarNonZero::random(&mut rng); + let wrong_blinding = ScalarNonZero::random(&mut rng); + let rekey_factor = ScalarNonZero::random(&mut rng); + + let wrong_commitment = BlindingCommitment::new(&wrong_blinding); + let rekey_commitment = rekey_factor * G; + + let proof = + SessionKeyShareProof::new(&blinding, &rekey_factor, &rekey_commitment, &mut rng); + + // Should fail with wrong blinding commitment + assert!(!proof.verify(&wrong_commitment, &rekey_commitment)); + } + + #[test] + fn test_session_key_share_proof_wrong_rekey() { + let mut rng = rand::rng(); + + let blinding = ScalarNonZero::random(&mut rng); + let rekey_factor = ScalarNonZero::random(&mut rng); + let wrong_rekey_factor = ScalarNonZero::random(&mut rng); + + let blinding_commitment = BlindingCommitment::new(&blinding); + let rekey_commitment = rekey_factor * G; + let wrong_rekey_commitment = wrong_rekey_factor * G; + + let proof = + SessionKeyShareProof::new(&blinding, &rekey_factor, &rekey_commitment, &mut rng); + + // Should fail with wrong rekey commitment + assert!(!proof.verify(&blinding_commitment, &wrong_rekey_commitment)); + } +} diff --git a/src/lib/keys/generation.rs b/src/lib/keys/generation.rs index 375622f..2eb769b 100644 --- a/src/lib/keys/generation.rs +++ b/src/lib/keys/generation.rs @@ -133,11 +133,148 @@ pub fn make_attribute_session_keys( make_session_key_pair(global, context, secret, make_attribute_rekey_factor) } +// Verifiable session key generation with proofs + +#[cfg(feature = "verifiable")] +use crate::core::proved::RekeyFactorCommitments; +#[cfg(feature = "verifiable")] +use crate::keys::distribution::{BlindingCommitment, SessionKeyShareProof}; + +/// Generate session keys with a proof of correct construction. +/// +/// This variant generates a session key along with a zero-knowledge proof that +/// the key was constructed correctly as u_i = b_i * k_i, where: +/// - b_i is a blinding factor +/// - k_i is the rekey factor for the given context +/// +/// The proof allows users to verify the session key share without revealing +/// the secret factors. +/// +/// # Arguments +/// +/// * `global` - Global secret key +/// * `context` - Encryption context +/// * `secret` - Encryption secret +/// * `blinding` - Blinding factor b_i (kept secret) +/// * `rekey_fn` - Function to derive the rekey factor +/// * `rng` - Random number generator for proof generation +/// +/// # Returns +/// +/// A tuple containing: +/// - The public session key +/// - The secret session key (u_i = b_i * k_i * global_secret) +/// - A proof of correct construction +/// - The blinding commitment (B_i = b_i * G) +#[cfg(feature = "verifiable")] +pub fn make_session_key_pair_with_proof( + global: &GSK, + context: &EncryptionContext, + secret: &EncryptionSecret, + blinding: &ScalarNonZero, + rekey_fn: F, + rng: &mut R, +) -> (PK, SK, SessionKeyShareProof, BlindingCommitment) +where + GSK: SecretKey, + PK: From, + SK: From, + RF: RekeyFactor, + F: Fn(&EncryptionSecret, &EncryptionContext) -> RF, + R: RngCore + CryptoRng, +{ + // Compute rekey factor k_i + let k = rekey_fn(secret, context); + + // Compute session key share contribution: u_i = b_i * k_i + let share = blinding * k.scalar(); + + // Compute final session key: sk = u_i * global_secret + let sk = share * *global.value(); + let pk = sk * G; + + // Create blinding commitment B_i = b_i * G + let blinding_commitment = BlindingCommitment::new(blinding); + + // Create rekey factor commitment K_i = k_i * G + let (rekey_commitment, _) = RekeyFactorCommitments::new(&k.scalar(), rng); + + // Create proof that u_i = b_i * k_i + let proof = SessionKeyShareProof::new(blinding, &k.scalar(), &rekey_commitment.val, rng); + + (PK::from(pk), SK::from(sk), proof, blinding_commitment) +} + +/// Generate pseudonym session keys with a proof of correct construction. +/// +/// This is a convenience wrapper around [`make_session_key_pair_with_proof`] for pseudonym keys. +/// +/// # Security Note +/// +/// The returned proof should only be shared with the user requesting the session key, +/// not publicly, as it contains information about the session key share. +#[cfg(feature = "verifiable")] +pub fn make_pseudonym_session_keys_with_proof( + global: &PseudonymGlobalSecretKey, + context: &EncryptionContext, + secret: &EncryptionSecret, + blinding: &ScalarNonZero, + rng: &mut R, +) -> ( + PseudonymSessionPublicKey, + PseudonymSessionSecretKey, + SessionKeyShareProof, + BlindingCommitment, +) { + make_session_key_pair_with_proof( + global, + context, + secret, + blinding, + make_pseudonym_rekey_factor, + rng, + ) +} + +/// Generate attribute session keys with a proof of correct construction. +/// +/// This is a convenience wrapper around [`make_session_key_pair_with_proof`] for attribute keys. +/// +/// # Security Note +/// +/// The returned proof should only be shared with the user requesting the session key, +/// not publicly, as it contains information about the session key share. +#[cfg(feature = "verifiable")] +pub fn make_attribute_session_keys_with_proof( + global: &AttributeGlobalSecretKey, + context: &EncryptionContext, + secret: &EncryptionSecret, + blinding: &ScalarNonZero, + rng: &mut R, +) -> ( + AttributeSessionPublicKey, + AttributeSessionSecretKey, + SessionKeyShareProof, + BlindingCommitment, +) { + make_session_key_pair_with_proof( + global, + context, + secret, + blinding, + make_attribute_rekey_factor, + rng, + ) +} + #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used)] mod tests { use super::*; + #[cfg(feature = "verifiable")] + use crate::core::proved::RekeyFactorCommitments; + #[test] fn make_global_keys_creates_valid_keypairs() { let mut rng = rand::rng(); @@ -227,6 +364,66 @@ mod tests { assert_eq!(public, decoded); } + #[test] + #[cfg(feature = "verifiable")] + fn make_session_keys_with_proof_valid() { + let mut rng = rand::rng(); + let (_global_pk, global_sk) = make_global_keys(&mut rng); + let context = EncryptionContext::from("test-context"); + let secret = EncryptionSecret::from(b"test-secret".to_vec()); + let blinding = ScalarNonZero::random(&mut rng); + + // Generate pseudonym session keys with proof + let (_pub_key, _sec_key, proof, blinding_commitment) = + make_pseudonym_session_keys_with_proof( + &global_sk.pseudonym, + &context, + &secret, + &blinding, + &mut rng, + ); + + // Create rekey factor commitment for verification + let k = make_pseudonym_rekey_factor(&secret, &context); + let (rekey_commitment, _) = RekeyFactorCommitments::new(&k.scalar(), &mut rng); + + // Verify the proof + assert!(proof.verify(&blinding_commitment, &rekey_commitment.val)); + + // Verify the commitment matches + assert_eq!(*blinding_commitment.value(), blinding * G); + } + + #[test] + #[cfg(feature = "verifiable")] + fn make_attribute_session_keys_with_proof_valid() { + let mut rng = rand::rng(); + let (_global_pk, global_sk) = make_global_keys(&mut rng); + let context = EncryptionContext::from("test-context"); + let secret = EncryptionSecret::from(b"test-secret".to_vec()); + let blinding = ScalarNonZero::random(&mut rng); + + // Generate attribute session keys with proof + let (_pub_key, _sec_key, proof, blinding_commitment) = + make_attribute_session_keys_with_proof( + &global_sk.attribute, + &context, + &secret, + &blinding, + &mut rng, + ); + + // Create rekey factor commitment for verification + let k = make_attribute_rekey_factor(&secret, &context); + let (rekey_commitment, _) = RekeyFactorCommitments::new(&k.scalar(), &mut rng); + + // Verify the proof + assert!(proof.verify(&blinding_commitment, &rekey_commitment.val)); + + // Verify the commitment matches + assert_eq!(*blinding_commitment.value(), blinding * G); + } + #[test] fn session_secret_key_serde() { let mut rng = rand::rng(); diff --git a/src/lib/keys/mod.rs b/src/lib/keys/mod.rs index 77600bf..4a53b5b 100644 --- a/src/lib/keys/mod.rs +++ b/src/lib/keys/mod.rs @@ -11,7 +11,7 @@ //! - [`types`]: Key type definitions for global and session keys //! - [`traits`]: Traits for public and secret keys //! - [`generation`]: Functions for generating global and session keys -//! - [`distribution`]: Distributed transcryptor key management (blinding, shares, setup) +//! - [`distribution`]: Distributed transcryptor key management (blinding, shares, setup, ZKPs) pub mod distribution; pub mod generation; @@ -37,3 +37,6 @@ pub use types::{ PseudonymGlobalPublicKey, PseudonymGlobalSecretKey, PseudonymSessionKeys, PseudonymSessionPublicKey, PseudonymSessionSecretKey, SessionKeys, }; + +#[cfg(feature = "verifiable")] +pub use distribution::{BlindingCommitment, BlindingCommitments, SessionKeyShareProof}; diff --git a/src/lib/lib.rs b/src/lib/lib.rs index 2ae4abd..57b64b8 100644 --- a/src/lib/lib.rs +++ b/src/lib/lib.rs @@ -46,6 +46,9 @@ pub mod keys; pub mod prelude; pub mod transcryptor; +#[cfg(feature = "verifiable")] +pub mod verifier; + #[cfg(all(feature = "python", not(feature = "wasm")))] pub mod py; diff --git a/src/lib/py.rs b/src/lib/py.rs index 31bd880..d5ea9bf 100644 --- a/src/lib/py.rs +++ b/src/lib/py.rs @@ -11,6 +11,9 @@ pub use crate::factors::py as factors; pub use crate::keys::py as keys; pub use crate::transcryptor::py as transcryptor; +#[cfg(feature = "verifiable")] +pub use crate::verifier::py as verifier; + use pyo3::prelude::*; pub fn register_module(m: &Bound<'_, PyModule>) -> PyResult<()> { @@ -92,10 +95,23 @@ pub fn register_module(m: &Bound<'_, PyModule>) -> PyResult<()> { factors::contexts::register(&factors_module)?; factors::types::register(&factors_module)?; factors::secrets::register(&factors_module)?; + #[cfg(feature = "verifiable")] + factors::commitments::register_commitment_module(&factors_module)?; m.add_submodule(&factors_module)?; py.import("sys")? .getattr("modules")? .set_item("libpep.factors", &factors_module)?; + // Register verifier as submodule + #[cfg(feature = "verifiable")] + { + let verifier_module = PyModule::new(py, "verifier")?; + verifier::register_verifier_module(&verifier_module)?; + m.add_submodule(&verifier_module)?; + py.import("sys")? + .getattr("modules")? + .set_item("libpep.verifier", &verifier_module)?; + } + Ok(()) } diff --git a/src/lib/transcryptor/mod.rs b/src/lib/transcryptor/mod.rs index 75e8b85..c5a521f 100644 --- a/src/lib/transcryptor/mod.rs +++ b/src/lib/transcryptor/mod.rs @@ -8,6 +8,9 @@ pub mod functions; pub mod prelude; pub mod types; +#[cfg(feature = "verifiable")] +pub mod verifiable; + #[cfg(feature = "python")] pub mod py; @@ -15,7 +18,7 @@ pub mod py; pub mod wasm; // Re-export types -pub use types::Transcryptor; +pub use types::{Transcryptor, TranscryptorId}; // Re-export functions pub use functions::{pseudonymize, rekey, rerandomize, rerandomize_known, transcrypt}; diff --git a/src/lib/transcryptor/py/types.rs b/src/lib/transcryptor/py/types.rs index b5d8491..a4361ad 100644 --- a/src/lib/transcryptor/py/types.rs +++ b/src/lib/transcryptor/py/types.rs @@ -381,6 +381,128 @@ impl PyTranscryptor { "transcrypt_batch() requires Vec[EncryptedRecord], Vec[LongEncryptedRecord], or Vec[EncryptedPEPJSONValue]", )) } + + /// Generate commitment proofs for pseudonymization factors. + #[cfg(feature = "verifiable")] + fn pseudonymization_commitments( + &self, + domain_from: &PyPseudonymizationDomain, + domain_to: &PyPseudonymizationDomain, + session_from: &PyEncryptionContext, + session_to: &PyEncryptionContext, + ) -> crate::factors::py::commitments::PyProvedPseudonymizationCommitments { + use crate::factors::py::commitments::PyProvedPseudonymizationCommitments; + let mut rng = rand::rng(); + let info = self.pseudonymization_info( + &domain_from.0, + &domain_to.0, + &session_from.0, + &session_to.0, + ); + PyProvedPseudonymizationCommitments { + inner: Transcryptor::pseudonymization_commitments(&info, &mut rng), + } + } + + /// Generate commitment proofs for attribute rekey factors. + #[cfg(feature = "verifiable")] + fn attribute_rekey_commitments( + &self, + session_from: &PyEncryptionContext, + session_to: &PyEncryptionContext, + ) -> crate::factors::py::commitments::PyProvedRekeyCommitments { + use crate::factors::py::commitments::PyProvedRekeyCommitments; + let mut rng = rand::rng(); + let info = self.attribute_rekey_info(&session_from.0, &session_to.0); + PyProvedRekeyCommitments { + inner: Transcryptor::attribute_rekey_commitments(&info, &mut rng), + } + } + + /// Generate commitment proofs for pseudonym rekey factors. + #[cfg(feature = "verifiable")] + fn pseudonym_rekey_commitments( + &self, + session_from: &PyEncryptionContext, + session_to: &PyEncryptionContext, + ) -> crate::factors::py::commitments::PyProvedRekeyCommitments { + use crate::factors::py::commitments::PyProvedRekeyCommitments; + let mut rng = rand::rng(); + let info = self.pseudonym_rekey_info(&session_from.0, &session_to.0); + PyProvedRekeyCommitments { + inner: Transcryptor::pseudonym_rekey_commitments(&info, &mut rng), + } + } + + /// Perform verifiable pseudonymization. + /// + /// Returns a tuple of (operation_proof, factors_proof). + #[cfg(feature = "verifiable")] + fn verifiable_pseudonymize( + &self, + encrypted: &PyEncryptedPseudonym, + pseudo_info: &PyPseudonymizationInfo, + ) -> ( + crate::core::py::proved::PyVerifiableRSK, + crate::core::py::proved::PyRSKFactorsProof, + ) { + use crate::data::traits::VerifiablePseudonymizable; + + let mut rng = rand::rng(); + let info = PseudonymizationInfo::from(pseudo_info); + let operation_proof = encrypted.0.verifiable_pseudonymize(&info, &mut rng); + let factors_proof = Transcryptor::pseudonymization_factors_proof(&info, &mut rng); + + ( + crate::core::py::proved::PyVerifiableRSK { + inner: operation_proof, + }, + crate::core::py::proved::PyRSKFactorsProof { + inner: factors_proof, + }, + ) + } + + /// Perform verifiable attribute rekey. + /// + /// Returns the operation proof. + #[cfg(feature = "verifiable")] + fn verifiable_attribute_rekey( + &self, + encrypted: &PyEncryptedAttribute, + rekey_info: &PyAttributeRekeyInfo, + ) -> crate::core::py::proved::PyVerifiableRekey { + use crate::data::traits::VerifiableRekeyable; + + let mut rng = rand::rng(); + let info = AttributeRekeyInfo::from(rekey_info); + let operation_proof = encrypted.0.verifiable_rekey(&info, &mut rng); + + crate::core::py::proved::PyVerifiableRekey { + inner: operation_proof, + } + } + + /// Perform verifiable pseudonym rekey. + /// + /// Returns the operation proof. + #[cfg(feature = "verifiable")] + fn verifiable_pseudonym_rekey( + &self, + encrypted: &PyEncryptedPseudonym, + session_from: &PyEncryptionContext, + session_to: &PyEncryptionContext, + ) -> crate::core::py::proved::PyVerifiableRekey { + use crate::data::traits::VerifiableRekeyable; + + let mut rng = rand::rng(); + let info = self.pseudonym_rekey_info(&session_from.0, &session_to.0); + let operation_proof = encrypted.0.verifiable_rekey(&info, &mut rng); + + crate::core::py::proved::PyVerifiableRekey { + inner: operation_proof, + } + } } pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { diff --git a/src/lib/transcryptor/types.rs b/src/lib/transcryptor/types.rs index 977d35a..1f8034a 100644 --- a/src/lib/transcryptor/types.rs +++ b/src/lib/transcryptor/types.rs @@ -8,6 +8,9 @@ use crate::factors::{ }; use rand_core::{CryptoRng, RngCore}; +/// Transcryptor identifier for distributed PEP systems. +pub type TranscryptorId = String; + /// A PEP transcryptor system that can pseudonymize and rekey data, based on /// a pseudonymisation secret and a rekeying secret. #[derive(Clone)] diff --git a/src/lib/transcryptor/verifiable.rs b/src/lib/transcryptor/verifiable.rs new file mode 100644 index 0000000..a7d6b22 --- /dev/null +++ b/src/lib/transcryptor/verifiable.rs @@ -0,0 +1,165 @@ +//! Verifiable transcryptor operations. +//! +//! This module provides methods for the transcryptor to perform verifiable +//! transcryption operations that generate zero-knowledge proofs. + +use crate::core::proved::RSKFactorsProof; +use crate::factors::{ + AttributeRekeyInfo, ProvedPseudonymizationCommitments, ProvedRekeyCommitments, + PseudonymRekeyInfo, PseudonymizationInfo, +}; +use rand_core::{CryptoRng, RngCore}; + +use super::types::Transcryptor; + +impl Transcryptor { + /// Generate commitments for pseudonymization info. + /// + /// This creates public commitments to the pseudonymization factors that can be published + /// and used by verifiers to check that operations are performed correctly. + /// + /// # Arguments + /// + /// * `info` - The pseudonymization info to create commitments for + /// * `rng` - Random number generator for creating commitments + /// + /// # Returns + /// + /// Proved commitments bundling both reshuffle and rekey commitments with their proofs + pub fn pseudonymization_commitments( + info: &PseudonymizationInfo, + rng: &mut R, + ) -> ProvedPseudonymizationCommitments { + use crate::core::proved::{PseudonymizationFactorCommitments, RekeyFactorCommitments}; + + let (reshuffle_commitments, reshuffle_proof) = + PseudonymizationFactorCommitments::new(&info.s.0, rng); + let (rekey_commitments, rekey_proof) = RekeyFactorCommitments::new(&info.k.0, rng); + + ProvedPseudonymizationCommitments { + reshuffle_commitments, + reshuffle_proof, + rekey_commitments, + rekey_proof, + } + } + + /// Generate commitments for pseudonym rekey info. + /// + /// This creates public commitments to the rekey factor that can be published + /// and used by verifiers to check that rekey operations are performed correctly. + /// + /// # Arguments + /// + /// * `info` - The pseudonym rekey info to create commitments for + /// * `rng` - Random number generator for creating commitments + /// + /// # Returns + /// + /// Proved commitments bundling rekey commitments with their proof + pub fn pseudonym_rekey_commitments( + info: &PseudonymRekeyInfo, + rng: &mut R, + ) -> ProvedRekeyCommitments { + use crate::core::proved::RekeyFactorCommitments; + + let (commitments, proof) = RekeyFactorCommitments::new(&info.0, rng); + + ProvedRekeyCommitments { commitments, proof } + } + + /// Generate commitments for attribute rekey info. + /// + /// This creates public commitments to the rekey factor that can be published + /// and used by verifiers to check that rekey operations are performed correctly. + /// + /// # Arguments + /// + /// * `info` - The attribute rekey info to create commitments for + /// * `rng` - Random number generator for creating commitments + /// + /// # Returns + /// + /// Proved commitments bundling rekey commitments with their proof + pub fn attribute_rekey_commitments( + info: &AttributeRekeyInfo, + rng: &mut R, + ) -> ProvedRekeyCommitments { + use crate::core::proved::RekeyFactorCommitments; + + let (commitments, proof) = RekeyFactorCommitments::new(&info.0, rng); + + ProvedRekeyCommitments { commitments, proof } + } + + /// Perform a verifiable pseudonymization operation. + /// + /// This generates a proof that can be verified by third parties using only + /// the public commitments (not included in this method). + /// + /// The result can be extracted from the proof via `.result()`. + /// + /// # Returns + /// + /// The operation proof (contains the result). + /// + /// # Note + /// + /// The factors proof (RSKFactorsProof) is not message-specific. Generate it once + /// per pseudonymization info using `RSKFactorsProof::new(&info.s.0, &info.k.0, rng)`. + pub fn verifiable_pseudonymize( + &self, + encrypted: &E, + info: &PseudonymizationInfo, + rng: &mut R, + ) -> E::PseudonymizationProof + where + E: crate::data::traits::VerifiablePseudonymizable, + R: RngCore + CryptoRng, + { + encrypted.verifiable_pseudonymize(info, rng) + } + + /// Generate a factors proof for pseudonymization verification. + /// + /// The factors proof is not message-specific and should be generated once + /// per pseudonymization info, not per message. + /// + /// # Arguments + /// + /// * `info` - The pseudonymization info to create a factors proof for + /// * `rng` - Random number generator + /// + /// # Returns + /// + /// The RSK factors proof + pub fn pseudonymization_factors_proof( + info: &PseudonymizationInfo, + rng: &mut R, + ) -> RSKFactorsProof { + RSKFactorsProof::new(&info.s.0, &info.k.0, rng) + } + + /// Perform a verifiable rekey operation. + /// + /// This generates a proof that can be verified by third parties using only + /// the public commitments (not included in this method). + /// + /// The result can be extracted from the proof via `.result(original)`. + /// + /// # Returns + /// + /// The operation proof (contains the result) + pub fn verifiable_rekey( + &self, + encrypted: &E, + info: &E::RekeyInfo, + rng: &mut R, + ) -> E::RekeyProof + where + E: crate::data::traits::VerifiableRekeyable, + R: RngCore + CryptoRng, + { + encrypted.verifiable_rekey(info, rng) + } +} diff --git a/src/lib/transcryptor/wasm/mod.rs b/src/lib/transcryptor/wasm/mod.rs index 269b49a..56d2302 100644 --- a/src/lib/transcryptor/wasm/mod.rs +++ b/src/lib/transcryptor/wasm/mod.rs @@ -6,5 +6,8 @@ pub mod distributed; pub mod functions; pub mod types; +#[cfg(feature = "verifiable")] +pub mod verifiable; + pub use distributed::WASMDistributedTranscryptor; pub use types::WASMTranscryptor; diff --git a/src/lib/verifier/cache.rs b/src/lib/verifier/cache.rs new file mode 100644 index 0000000..df74ed3 --- /dev/null +++ b/src/lib/verifier/cache.rs @@ -0,0 +1,194 @@ +//! Commitment cache for storing and retrieving factor commitments. +//! +//! Transcryptors must use consistent factors for each user (domain) and session (context). +//! This cache enforces integrity by storing verified commitments indexed by: +//! - **Reshuffle factors**: Per pseudonymization domain (user-specific) +//! - **Rekey factors**: Per encryption context (session-specific) +//! +//! The cache follows the pattern from the distributed verifier, storing both `val` and `inv` +//! for each factor after verification. + +use crate::factors::{ + EncryptionContext, ProvedRekeyCommitments, ProvedReshuffleCommitments, PseudonymizationDomain, +}; +use std::collections::HashMap; +use std::hash::Hash; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Trait for commitment caches. +/// +/// This trait defines the interface for storing and retrieving factor commitments. +/// Implementations can use different storage backends (in-memory, persistent, etc.). +pub trait CommitmentsCache { + /// The key type for cache lookups (domain or context). + type Key; + /// The commitment type stored in the cache. + type Commitments; + + /// Create a new empty cache. + fn new() -> Self + where + Self: Sized; + + /// Store commitments for a specific key. + fn store(&mut self, key: Self::Key, commitments: Self::Commitments); + + /// Retrieve commitments for a specific key. + fn retrieve(&self, key: &Self::Key) -> Option<&Self::Commitments>; + + /// Check if commitments exist for a specific key. + fn has(&self, key: &Self::Key) -> bool; + + /// Check if the cache contains specific commitments (regardless of key). + fn contains(&self, commitments: &Self::Commitments) -> bool; + + /// Get the number of entries in the cache. + fn len(&self) -> usize; + + /// Check if the cache is empty. + fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Clear all entries from the cache. + fn clear(&mut self); + + /// Dump all entries as a vector of (key, commitments) pairs. + fn dump(&self) -> Vec<(Self::Key, Self::Commitments)>; +} + +/// In-memory implementation of a commitments cache. +/// +/// This cache stores commitments in a HashMap for fast O(1) lookups. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct InMemoryCommitmentsCache +where + Key: Eq + Hash, +{ + cache: HashMap, +} + +impl InMemoryCommitmentsCache +where + Key: Eq + Hash, +{ + /// Create a new empty in-memory cache. + pub fn new() -> Self { + Self { + cache: HashMap::new(), + } + } +} + +impl Default for InMemoryCommitmentsCache +where + Key: Eq + Hash, +{ + fn default() -> Self { + Self::new() + } +} + +impl CommitmentsCache for InMemoryCommitmentsCache +where + Key: Eq + Hash + Clone, + Commitments: PartialEq + Clone, +{ + type Key = Key; + type Commitments = Commitments; + + fn new() -> Self { + Self { + cache: HashMap::new(), + } + } + + fn store(&mut self, key: Self::Key, commitments: Self::Commitments) { + self.cache.insert(key, commitments); + } + + fn retrieve(&self, key: &Self::Key) -> Option<&Self::Commitments> { + self.cache.get(key) + } + + fn has(&self, key: &Self::Key) -> bool { + self.cache.contains_key(key) + } + + fn contains(&self, commitments: &Self::Commitments) -> bool { + self.cache.values().any(|v| v == commitments) + } + + fn len(&self) -> usize { + self.cache.len() + } + + fn clear(&mut self) { + self.cache.clear(); + } + + fn dump(&self) -> Vec<(Self::Key, Self::Commitments)> { + self.cache + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect() + } +} + +use crate::transcryptor::TranscryptorId; + +/// Type alias for reshuffle commitments cache (indexed by transcryptor ID and domain). +pub type ReshuffleCommitmentsCache = + InMemoryCommitmentsCache<(TranscryptorId, PseudonymizationDomain), ProvedReshuffleCommitments>; + +/// Type alias for pseudonym rekey commitments cache (indexed by transcryptor ID and context). +pub type PseudonymRekeyCommitmentsCache = + InMemoryCommitmentsCache<(TranscryptorId, EncryptionContext), ProvedRekeyCommitments>; + +/// Type alias for attribute rekey commitments cache (indexed by transcryptor ID and context). +pub type AttributeRekeyCommitmentsCache = + InMemoryCommitmentsCache<(TranscryptorId, EncryptionContext), ProvedRekeyCommitments>; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cache_basic_operations() { + let mut cache = InMemoryCommitmentsCache::::new(); + + assert!(cache.is_empty()); + assert_eq!(cache.len(), 0); + + cache.store("key1".to_string(), 42); + assert!(!cache.is_empty()); + assert_eq!(cache.len(), 1); + assert!(cache.has(&"key1".to_string())); + assert_eq!(cache.retrieve(&"key1".to_string()), Some(&42)); + + cache.store("key2".to_string(), 100); + assert_eq!(cache.len(), 2); + + assert!(cache.contains(&42)); + assert!(cache.contains(&100)); + assert!(!cache.contains(&999)); + + cache.clear(); + assert!(cache.is_empty()); + } + + #[test] + fn test_cache_dump() { + let mut cache = InMemoryCommitmentsCache::::new(); + cache.store("a".to_string(), 1); + cache.store("b".to_string(), 2); + + let dump = cache.dump(); + assert_eq!(dump.len(), 2); + assert!(dump.contains(&("a".to_string(), 1))); + assert!(dump.contains(&("b".to_string(), 2))); + } +} diff --git a/src/lib/verifier/mod.rs b/src/lib/verifier/mod.rs new file mode 100644 index 0000000..1e78c59 --- /dev/null +++ b/src/lib/verifier/mod.rs @@ -0,0 +1,33 @@ +//! Verifier for verifiable transcryption operations. +//! +//! The verifier enforces integrity by ensuring transcryptors use consistent factors +//! for each user (domain) and session (context). +//! +//! # Cache Organization +//! +//! - **Reshuffle commitments**: Per pseudonymization domain (user-specific) +//! - **Rekey commitments**: Per encryption context (session-specific) +//! +//! Each cache stores both `val` and `inv` for the factor commitments. + +pub mod cache; +#[allow(clippy::module_inception)] +pub mod verifier; + +#[cfg(feature = "wasm")] +pub mod wasm; + +#[cfg(feature = "python")] +pub mod py; + +pub use cache::{ + AttributeRekeyCommitmentsCache, CommitmentsCache, InMemoryCommitmentsCache, + PseudonymRekeyCommitmentsCache, ReshuffleCommitmentsCache, +}; +pub use verifier::Verifier; + +#[cfg(feature = "wasm")] +pub use wasm::WASMVerifier; + +#[cfg(feature = "python")] +pub use py::PyVerifier; diff --git a/src/lib/verifier/py.rs b/src/lib/verifier/py.rs new file mode 100644 index 0000000..8f4ab2b --- /dev/null +++ b/src/lib/verifier/py.rs @@ -0,0 +1,172 @@ +//! Python bindings for the verifier. + +use crate::data::py::simple::{PyEncryptedAttribute, PyEncryptedPseudonym}; +use crate::factors::py::commitments::{ + PyProvedPseudonymizationCommitments, PyProvedRekeyCommitments, +}; +use crate::factors::py::contexts::{PyEncryptionContext, PyPseudonymizationDomain}; +use crate::verifier::Verifier; +use derive_more::{Deref, From, Into}; +use pyo3::prelude::*; + +#[cfg(feature = "verifiable")] +use crate::core::py::proved::{PyRSKFactorsProof, PyVerifiableRSK, PyVerifiableRekey}; + +/// A verifier for verifiable transcryption operations (Python). +#[derive(From, Into, Deref)] +#[pyclass(name = "Verifier")] +pub struct PyVerifier(Verifier); + +#[pymethods] +impl PyVerifier { + /// Create a new verifier with empty caches. + #[new] + pub fn new() -> Self { + Self(Verifier::new()) + } + + /// Verify pseudonymization commitments. + fn verify_pseudonymization_commitments( + &self, + commitments: &PyProvedPseudonymizationCommitments, + ) -> bool { + self.0 + .verify_pseudonymization_commitments(&commitments.inner) + } + + /// Verify rekey commitments. + fn verify_rekey_commitments(&self, commitments: &PyProvedRekeyCommitments) -> bool { + self.0.verify_rekey_commitments(&commitments.inner) + } + + /// Register pseudonymization commitments for caching. + fn register_pseudonymization_commitments( + &mut self, + transcryptor_id: &str, + domain_from: &PyPseudonymizationDomain, + domain_to: &PyPseudonymizationDomain, + context_from: &PyEncryptionContext, + context_to: &PyEncryptionContext, + commitments: &PyProvedPseudonymizationCommitments, + ) { + self.0.register_pseudonymization_commitments( + &transcryptor_id.to_string(), + &domain_from.0, + &domain_to.0, + &context_from.0, + &context_to.0, + commitments.inner, + ); + } + + /// Register attribute rekey commitments for caching. + fn register_attribute_rekey_commitments( + &mut self, + transcryptor_id: &str, + context_from: &PyEncryptionContext, + context_to: &PyEncryptionContext, + commitments: &PyProvedRekeyCommitments, + ) { + self.0.register_attribute_rekey_commitments( + &transcryptor_id.to_string(), + &context_from.0, + &context_to.0, + commitments.inner, + ); + } + + /// Check if reshuffle commitments exist in cache. + fn has_reshuffle_commitments( + &self, + transcryptor_id: &str, + domain: &PyPseudonymizationDomain, + ) -> bool { + self.0.has_reshuffle_commitments(transcryptor_id, &domain.0) + } + + /// Check if pseudonym rekey commitments exist in cache. + fn has_pseudonym_rekey_commitments( + &self, + transcryptor_id: &str, + context: &PyEncryptionContext, + ) -> bool { + self.0 + .has_pseudonym_rekey_commitments(transcryptor_id, &context.0) + } + + /// Check if attribute rekey commitments exist in cache. + fn has_attribute_rekey_commitments( + &self, + transcryptor_id: &str, + context: &PyEncryptionContext, + ) -> bool { + self.0 + .has_attribute_rekey_commitments(transcryptor_id, &context.0) + } + + /// Clear all cached commitments. + fn clear_cache(&mut self) { + self.0.clear_cache(); + } + + /// Get cache size. + fn cache_size(&self) -> usize { + self.0.cache().total_count() + } + + /// Verify a pseudonymization operation with commitments. + #[cfg(feature = "verifiable")] + fn verify_pseudonymization( + &self, + original: &PyEncryptedPseudonym, + result: &PyEncryptedPseudonym, + operation_proof: &PyVerifiableRSK, + factors_proof: &PyRSKFactorsProof, + commitments: &PyProvedPseudonymizationCommitments, + ) -> bool { + self.0.verify_pseudonymization( + &original.0, + &result.0, + &operation_proof.inner, + &factors_proof.inner, + &commitments.inner, + ) + } + + /// Verify a pseudonym rekey operation with commitments. + #[cfg(feature = "verifiable")] + fn verify_pseudonym_rekey( + &self, + original: &PyEncryptedPseudonym, + result: &PyEncryptedPseudonym, + proof: &PyVerifiableRekey, + commitments: &PyProvedRekeyCommitments, + ) -> bool { + self.0 + .verify_pseudonym_rekey(&original.0, &result.0, &proof.inner, &commitments.inner) + } + + /// Verify an attribute rekey operation with commitments. + #[cfg(feature = "verifiable")] + fn verify_attribute_rekey( + &self, + original: &PyEncryptedAttribute, + result: &PyEncryptedAttribute, + proof: &PyVerifiableRekey, + commitments: &PyProvedRekeyCommitments, + ) -> bool { + self.0 + .verify_attribute_rekey(&original.0, &result.0, &proof.inner, &commitments.inner) + } +} + +impl Default for PyVerifier { + fn default() -> Self { + Self::new() + } +} + +pub(crate) fn register_verifier_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { + parent_module.add_class::()?; + Ok(()) +} diff --git a/src/lib/verifier/verifier.rs b/src/lib/verifier/verifier.rs new file mode 100644 index 0000000..e0bef9b --- /dev/null +++ b/src/lib/verifier/verifier.rs @@ -0,0 +1,851 @@ +//! Verifier for verifiable transcryption operations. +//! +//! The verifier enforces integrity by ensuring transcryptors use consistent factors +//! for each user (domain) and session (context), as described in the paper. +//! +//! This implementation follows the pattern from the distributed verifier, storing +//! verified commitments per individual domain/context and combining them for +//! verification of transitions. + +use crate::arithmetic::group_elements::{GroupElement, G}; +use crate::core::proved::{ + PseudonymizationFactorCommitments, RSKFactorsProof, RekeyFactorCommitments, VerifiableRSK, + VerifiableRekey, +}; +use crate::data::records::{EncryptedRecord, RecordTranscryptionProof}; +use crate::data::simple::{ElGamalEncrypted, EncryptedPseudonym}; +use crate::data::traits::{Pseudonymizable, Rekeyable}; +use crate::factors::{ + EncryptionContext, ProvedPseudonymizationCommitments, ProvedRekeyCommitments, + ProvedReshuffleCommitments, PseudonymizationDomain, +}; +use crate::transcryptor::TranscryptorId; + +use super::cache::{ + AttributeRekeyCommitmentsCache, CommitmentsCache as CommitmentsCacheTrait, + PseudonymRekeyCommitmentsCache, ReshuffleCommitmentsCache, +}; + +/// A verifier for verifiable transcryption operations with commitment caching. +/// +/// The verifier ensures integrity by checking that transcryptors use consistent +/// factors for each user and session: +/// - **Reshuffle factors** must be consistent per pseudonymization domain (user-specific) +/// - **Rekey factors** must be consistent per encryption context (session-specific) +/// +/// # Cache Organization +/// +/// The verifier maintains three separate caches: +/// - Reshuffle commitments indexed by `PseudonymizationDomain` +/// - Pseudonym rekey commitments indexed by `EncryptionContext` +/// - Attribute rekey commitments indexed by `EncryptionContext` +/// +/// Each cache stores both `val` and `inv` for the factor commitments. +pub struct Verifier { + reshuffle_cache: ReshuffleCommitmentsCache, + pseudonym_rekey_cache: PseudonymRekeyCommitmentsCache, + attribute_rekey_cache: AttributeRekeyCommitmentsCache, +} + +impl Verifier { + /// Create a new verifier with empty caches. + #[must_use] + pub fn new() -> Self { + Self { + reshuffle_cache: ReshuffleCommitmentsCache::new(), + pseudonym_rekey_cache: PseudonymRekeyCommitmentsCache::new(), + attribute_rekey_cache: AttributeRekeyCommitmentsCache::new(), + } + } + + // ======================================== + // Commitment validation and storage + // ======================================== + + /// Validate that commitments are not weak (identity or G). + fn validate_not_weak(val: &GroupElement, commitment_type: &str) { + if *val == GroupElement::identity() || *val == G { + panic!("Weak {commitment_type} commitments are not allowed"); + } + } + + /// Store reshuffle commitments for a transcryptor and domain after validation. + /// + /// This validates that: + /// 1. The commitments are not weak (not identity or G) + /// 2. The proof correctly verifies the commitments + /// + /// # Panics + /// + /// Panics if the commitments are weak or the proof is invalid. + pub fn store_reshuffle_commitments( + &mut self, + transcryptor_id: TranscryptorId, + domain: PseudonymizationDomain, + commitments: &ProvedReshuffleCommitments, + ) { + Self::validate_not_weak(&commitments.commitments.val, "reshuffle"); + + if !commitments.proof.verify(&commitments.commitments) { + panic!("Invalid reshuffle commitments proof"); + } + + self.reshuffle_cache + .store((transcryptor_id, domain), *commitments); + } + + /// Store pseudonym rekey commitments for a transcryptor and context after validation. + /// + /// This validates that: + /// 1. The commitments are not weak (not identity or G) + /// 2. The proof correctly verifies the commitments + /// + /// # Panics + /// + /// Panics if the commitments are weak or the proof is invalid. + pub fn store_pseudonym_rekey_commitments( + &mut self, + transcryptor_id: TranscryptorId, + context: EncryptionContext, + commitments: &ProvedRekeyCommitments, + ) { + Self::validate_not_weak(&commitments.commitments.val, "pseudonym rekey"); + + if !commitments.proof.verify(&commitments.commitments) { + panic!("Invalid pseudonym rekey commitments proof"); + } + + self.pseudonym_rekey_cache + .store((transcryptor_id, context), *commitments); + } + + /// Store attribute rekey commitments for a transcryptor and context after validation. + /// + /// This validates that: + /// 1. The commitments are not weak (not identity or G) + /// 2. The proof correctly verifies the commitments + /// + /// # Panics + /// + /// Panics if the commitments are weak or the proof is invalid. + pub fn store_attribute_rekey_commitments( + &mut self, + transcryptor_id: TranscryptorId, + context: EncryptionContext, + commitments: &ProvedRekeyCommitments, + ) { + Self::validate_not_weak(&commitments.commitments.val, "attribute rekey"); + + if !commitments.proof.verify(&commitments.commitments) { + panic!("Invalid attribute rekey commitments proof"); + } + + self.attribute_rekey_cache + .store((transcryptor_id, context), *commitments); + } + + // ======================================== + // Cache queries + // ======================================== + + /// Check if reshuffle commitments exist for a transcryptor and domain. + #[must_use] + pub fn has_reshuffle_commitments( + &self, + transcryptor_id: &str, + domain: &PseudonymizationDomain, + ) -> bool { + self.reshuffle_cache + .has(&(transcryptor_id.to_string(), domain.clone())) + } + + /// Check if pseudonym rekey commitments exist for a transcryptor and context. + #[must_use] + pub fn has_pseudonym_rekey_commitments( + &self, + transcryptor_id: &str, + context: &EncryptionContext, + ) -> bool { + self.pseudonym_rekey_cache + .has(&(transcryptor_id.to_string(), context.clone())) + } + + /// Check if attribute rekey commitments exist for a transcryptor and context. + #[must_use] + pub fn has_attribute_rekey_commitments( + &self, + transcryptor_id: &str, + context: &EncryptionContext, + ) -> bool { + self.attribute_rekey_cache + .has(&(transcryptor_id.to_string(), context.clone())) + } + + // ======================================== + // Commitment registration (for test compatibility) + // ======================================== + + /// Register pseudonymization commitments for a domain/context transition. + /// + /// Note: Transition commitments are already combined (inv from source, val from target). + /// This method stores them such that they can be used for verifying this specific transition. + /// The commitments are stored once and shared between source/target domains and contexts. + pub fn register_pseudonymization_commitments( + &mut self, + transcryptor_id: &TranscryptorId, + domain_from: &PseudonymizationDomain, + domain_to: &PseudonymizationDomain, + context_from: &EncryptionContext, + context_to: &EncryptionContext, + commitments: ProvedPseudonymizationCommitments, + ) { + // Transition commitments are already combined, so we store them once + // They will work for verifying the specific transition domain_from→domain_to, context_from→context_to + let reshuffle_commitments = ProvedReshuffleCommitments { + commitments: commitments.reshuffle_commitments, + proof: commitments.reshuffle_proof, + }; + + let rekey_commitments = ProvedRekeyCommitments { + commitments: commitments.rekey_commitments, + proof: commitments.rekey_proof, + }; + + // Store once for each unique domain/context (avoid duplicates) + let tid = transcryptor_id.clone(); + + if !self.has_reshuffle_commitments(transcryptor_id, domain_from) { + self.store_reshuffle_commitments( + tid.clone(), + domain_from.clone(), + &reshuffle_commitments, + ); + } + if domain_from != domain_to && !self.has_reshuffle_commitments(transcryptor_id, domain_to) { + self.store_reshuffle_commitments( + tid.clone(), + domain_to.clone(), + &reshuffle_commitments, + ); + } + if !self.has_pseudonym_rekey_commitments(transcryptor_id, context_from) { + self.store_pseudonym_rekey_commitments( + tid.clone(), + context_from.clone(), + &rekey_commitments, + ); + } + if context_from != context_to + && !self.has_pseudonym_rekey_commitments(transcryptor_id, context_to) + { + self.store_pseudonym_rekey_commitments(tid, context_to.clone(), &rekey_commitments); + } + } + + /// Register attribute rekey commitments for a transcryptor's context transition. + pub fn register_attribute_rekey_commitments( + &mut self, + transcryptor_id: &TranscryptorId, + context_from: &EncryptionContext, + context_to: &EncryptionContext, + commitments: ProvedRekeyCommitments, + ) { + // Store for both source and target if not already present + let tid = transcryptor_id.clone(); + + if !self.has_attribute_rekey_commitments(transcryptor_id, context_from) { + self.store_attribute_rekey_commitments(tid.clone(), context_from.clone(), &commitments); + } + if context_from != context_to + && !self.has_attribute_rekey_commitments(transcryptor_id, context_to) + { + self.store_attribute_rekey_commitments(tid, context_to.clone(), &commitments); + } + } + + /// Get cached pseudonymization commitments for a transition (if registered). + /// + /// Note: This method is deprecated in the new architecture. Commitments are stored + /// separately per domain/context, not as bundled transitions. This always returns None. + #[must_use] + #[deprecated( + note = "Commitments are now stored per domain/context, use has_reshuffle_commitments and has_pseudonym_rekey_commitments instead" + )] + pub fn get_pseudonymization_commitments( + &self, + _domain_from: &PseudonymizationDomain, + _domain_to: &PseudonymizationDomain, + _context_from: &EncryptionContext, + _context_to: &EncryptionContext, + ) -> Option<&ProvedPseudonymizationCommitments> { + // Commitments are stored separately per domain/context in the new architecture + None + } + + // ======================================== + // Commitment verification + // ======================================== + + /// Verify that pseudonymization commitments (reshuffle + rekey) are correctly constructed. + #[must_use] + pub fn verify_pseudonymization_commitments( + &self, + commitments: &ProvedPseudonymizationCommitments, + ) -> bool { + commitments + .reshuffle_proof + .verify(&commitments.reshuffle_commitments) + && commitments + .rekey_proof + .verify(&commitments.rekey_commitments) + } + + /// Verify that rekey commitments are correctly constructed. + #[must_use] + pub fn verify_rekey_commitments(&self, commitments: &ProvedRekeyCommitments) -> bool { + commitments.proof.verify(&commitments.commitments) + } + + // ======================================== + // Cache management + // ======================================== + + /// Access the internal cache (read-only). + pub fn cache(&self) -> VerifierCache<'_> { + VerifierCache { + reshuffle: &self.reshuffle_cache, + pseudonym_rekey: &self.pseudonym_rekey_cache, + attribute_rekey: &self.attribute_rekey_cache, + } + } + + /// Clear all cached commitments. + pub fn clear_cache(&mut self) { + self.reshuffle_cache.clear(); + self.pseudonym_rekey_cache.clear(); + self.attribute_rekey_cache.clear(); + } + + // ======================================== + // Operation verification with commitments + // ======================================== + + /// Verify a pseudonymization operation (RSK) with commitments passed directly. + /// + /// This is the primary verification method used by most code. It verifies that + /// the operation was performed correctly using the provided commitments. + #[must_use] + pub fn verify_pseudonymization( + &self, + original: &E, + _result: &E, + operation_proof: &VerifiableRSK, + factors_proof: &RSKFactorsProof, + commitments: &ProvedPseudonymizationCommitments, + ) -> bool + where + E: ElGamalEncrypted + Pseudonymizable, + { + // Verify factors proof against commitments + if !factors_proof.verify( + &commitments.reshuffle_commitments, + &commitments.rekey_commitments, + ) { + return false; + } + + // Verify operation proof + operation_proof + .verified_reconstruct( + original.value(), + factors_proof, + &commitments.reshuffle_commitments, + &commitments.rekey_commitments, + ) + .is_some() + } + + /// Verify a pseudonym rekey operation with commitments passed directly. + #[must_use] + pub fn verify_pseudonym_rekey( + &self, + original: &E, + _result: &E, + proof: &VerifiableRekey, + commitments: &ProvedRekeyCommitments, + ) -> bool + where + E: ElGamalEncrypted + Rekeyable, + { + proof + .verified_reconstruct(original.value(), &commitments.commitments) + .is_some() + } + + /// Verify an attribute rekey operation with commitments passed directly. + #[must_use] + pub fn verify_attribute_rekey( + &self, + original: &E, + _result: &E, + proof: &VerifiableRekey, + commitments: &ProvedRekeyCommitments, + ) -> bool + where + E: ElGamalEncrypted + Rekeyable, + { + proof + .verified_reconstruct(original.value(), &commitments.commitments) + .is_some() + } + + /// Verify a complete record transcryption with commitments passed directly. + #[must_use] + pub fn verify_record_transcryption( + &self, + original: &EncryptedRecord, + result: &EncryptedRecord, + proof: &RecordTranscryptionProof, + pseudonym_commitments: &ProvedPseudonymizationCommitments, + attribute_commitments: &ProvedRekeyCommitments, + ) -> bool { + // Verify pseudonym factors proof + if !proof.pseudonym_factors_proof.verify( + &pseudonym_commitments.reshuffle_commitments, + &pseudonym_commitments.rekey_commitments, + ) { + return false; + } + + // Verify each pseudonym operation + for ((orig_pseudo, _result_pseudo), op_proof) in original + .pseudonyms + .iter() + .zip(result.pseudonyms.iter()) + .zip(proof.pseudonym_operation_proofs.iter()) + { + if op_proof + .verified_reconstruct( + orig_pseudo.value(), + &proof.pseudonym_factors_proof, + &pseudonym_commitments.reshuffle_commitments, + &pseudonym_commitments.rekey_commitments, + ) + .is_none() + { + return false; + } + } + + // Verify each attribute operation + for ((orig_attr, _result_attr), op_proof) in original + .attributes + .iter() + .zip(result.attributes.iter()) + .zip(proof.attribute_operation_proofs.iter()) + { + if op_proof + .verified_reconstruct(orig_attr.value(), &attribute_commitments.commitments) + .is_none() + { + return false; + } + } + + true + } + + // ======================================== + // Operation verification using cached commitments + // ======================================== + + /// Helper: Combine rekey commitments for a transition (val from source, inv from target). + fn combine_rekey_commitments( + from: &ProvedRekeyCommitments, + to: &ProvedRekeyCommitments, + ) -> RekeyFactorCommitments { + RekeyFactorCommitments::from(crate::core::proved::FactorCommitments { + val: from.commitments.val, + inv: to.commitments.inv, + }) + } + + /// Verify a pseudonymization operation using cached commitments. + /// + /// This verifies a transition from domain_from→domain_to and context_from→context_to + /// using commitments previously stored in the cache. + #[must_use] + #[allow(clippy::too_many_arguments)] + pub fn verify_pseudonymization_from_cache( + &self, + transcryptor_id: &str, + original: &E, + _result: &E, + operation_proof: &VerifiableRSK, + factors_proof: &RSKFactorsProof, + domain_from: &PseudonymizationDomain, + domain_to: &PseudonymizationDomain, + context_from: &EncryptionContext, + context_to: &EncryptionContext, + ) -> bool + where + E: ElGamalEncrypted + Pseudonymizable, + { + let transcryptor_id = transcryptor_id.to_string(); + + // Retrieve commitments from cache + let Some(reshuffle_from) = self + .reshuffle_cache + .retrieve(&(transcryptor_id.clone(), domain_from.clone())) + else { + return false; + }; + let Some(reshuffle_to) = self + .reshuffle_cache + .retrieve(&(transcryptor_id.clone(), domain_to.clone())) + else { + return false; + }; + let Some(rekey_from) = self + .pseudonym_rekey_cache + .retrieve(&(transcryptor_id.clone(), context_from.clone())) + else { + return false; + }; + let Some(rekey_to) = self + .pseudonym_rekey_cache + .retrieve(&(transcryptor_id, context_to.clone())) + else { + return false; + }; + + // Construct combined commitments for the transition + // For reshuffle: use inv from source domain, val from target domain + let reshuffle_commitments = + PseudonymizationFactorCommitments::from(crate::core::proved::FactorCommitments { + inv: reshuffle_from.commitments.inv, + val: reshuffle_to.commitments.val, + }); + + // For rekey: use val from source context, inv from target context + let rekey_commitments = Self::combine_rekey_commitments(rekey_from, rekey_to); + + // Verify the factors proof (S, K^-1, T) + if !factors_proof.verify(&reshuffle_commitments, &rekey_commitments) { + return false; + } + + // Verify the operation proof + operation_proof + .verified_reconstruct( + original.value(), + factors_proof, + &reshuffle_commitments, + &rekey_commitments, + ) + .is_some() + } + + /// Verify a pseudonym rekey operation using cached commitments. + #[must_use] + pub fn verify_pseudonym_rekey_from_cache( + &self, + transcryptor_id: &str, + original: &E, + _result: &E, + proof: &VerifiableRekey, + context_from: &EncryptionContext, + context_to: &EncryptionContext, + ) -> bool + where + E: ElGamalEncrypted + Rekeyable, + { + let transcryptor_id = transcryptor_id.to_string(); + + let Some(rekey_from) = self + .pseudonym_rekey_cache + .retrieve(&(transcryptor_id.clone(), context_from.clone())) + else { + return false; + }; + let Some(rekey_to) = self + .pseudonym_rekey_cache + .retrieve(&(transcryptor_id, context_to.clone())) + else { + return false; + }; + + let rekey_commitments = Self::combine_rekey_commitments(rekey_from, rekey_to); + + proof + .verified_reconstruct(original.value(), &rekey_commitments) + .is_some() + } + + /// Verify an attribute rekey operation using cached commitments. + #[must_use] + pub fn verify_attribute_rekey_from_cache( + &self, + transcryptor_id: &str, + original: &E, + _result: &E, + proof: &VerifiableRekey, + context_from: &EncryptionContext, + context_to: &EncryptionContext, + ) -> bool + where + E: ElGamalEncrypted + Rekeyable, + { + let transcryptor_id = transcryptor_id.to_string(); + + let Some(rekey_from) = self + .attribute_rekey_cache + .retrieve(&(transcryptor_id.clone(), context_from.clone())) + else { + return false; + }; + let Some(rekey_to) = self + .attribute_rekey_cache + .retrieve(&(transcryptor_id, context_to.clone())) + else { + return false; + }; + + let rekey_commitments = Self::combine_rekey_commitments(rekey_from, rekey_to); + + proof + .verified_reconstruct(original.value(), &rekey_commitments) + .is_some() + } + + /// Verify a complete record transcryption using cached commitments. + #[must_use] + #[allow(clippy::too_many_arguments)] + pub fn verify_record_transcryption_from_cache( + &self, + transcryptor_id: &str, + original: &EncryptedRecord, + result: &EncryptedRecord, + proof: &RecordTranscryptionProof, + domain_from: &PseudonymizationDomain, + domain_to: &PseudonymizationDomain, + context_from: &EncryptionContext, + context_to: &EncryptionContext, + ) -> bool { + let transcryptor_id = transcryptor_id.to_string(); + + // Retrieve commitments from cache + let Some(reshuffle_from) = self + .reshuffle_cache + .retrieve(&(transcryptor_id.clone(), domain_from.clone())) + else { + return false; + }; + let Some(reshuffle_to) = self + .reshuffle_cache + .retrieve(&(transcryptor_id.clone(), domain_to.clone())) + else { + return false; + }; + let Some(pseudonym_rekey_from) = self + .pseudonym_rekey_cache + .retrieve(&(transcryptor_id.clone(), context_from.clone())) + else { + return false; + }; + let Some(pseudonym_rekey_to) = self + .pseudonym_rekey_cache + .retrieve(&(transcryptor_id.clone(), context_to.clone())) + else { + return false; + }; + let Some(attribute_rekey_from) = self + .attribute_rekey_cache + .retrieve(&(transcryptor_id.clone(), context_from.clone())) + else { + return false; + }; + let Some(attribute_rekey_to) = self + .attribute_rekey_cache + .retrieve(&(transcryptor_id, context_to.clone())) + else { + return false; + }; + + // Construct combined commitments for pseudonym operations + let reshuffle_commitments = + PseudonymizationFactorCommitments::from(crate::core::proved::FactorCommitments { + inv: reshuffle_from.commitments.inv, + val: reshuffle_to.commitments.val, + }); + let pseudonym_rekey_commitments = + Self::combine_rekey_commitments(pseudonym_rekey_from, pseudonym_rekey_to); + + // Verify pseudonym factors proof + if !proof + .pseudonym_factors_proof + .verify(&reshuffle_commitments, &pseudonym_rekey_commitments) + { + return false; + } + + // Verify each pseudonym operation + for ((orig_pseudo, _result_pseudo), op_proof) in original + .pseudonyms + .iter() + .zip(result.pseudonyms.iter()) + .zip(proof.pseudonym_operation_proofs.iter()) + { + if op_proof + .verified_reconstruct( + orig_pseudo.value(), + &proof.pseudonym_factors_proof, + &reshuffle_commitments, + &pseudonym_rekey_commitments, + ) + .is_none() + { + return false; + } + } + + // Construct combined commitments for attribute operations + let attribute_rekey_commitments = + Self::combine_rekey_commitments(attribute_rekey_from, attribute_rekey_to); + + // Verify each attribute operation + for ((orig_attr, _result_attr), op_proof) in original + .attributes + .iter() + .zip(result.attributes.iter()) + .zip(proof.attribute_operation_proofs.iter()) + { + if op_proof + .verified_reconstruct(orig_attr.value(), &attribute_rekey_commitments) + .is_none() + { + return false; + } + } + + true + } + + /// Verify long pseudonym pseudonymization with commitments. + #[must_use] + pub fn verify_pseudonymization_long( + &self, + originals: &[EncryptedPseudonym], + results: &[EncryptedPseudonym], + operation_proofs: &[VerifiableRSK], + factors_proof: &RSKFactorsProof, + commitments: &ProvedPseudonymizationCommitments, + ) -> bool { + // Verify factors proof + if !factors_proof.verify( + &commitments.reshuffle_commitments, + &commitments.rekey_commitments, + ) { + return false; + } + + // Verify each block + for ((orig, _result), op_proof) in originals + .iter() + .zip(results.iter()) + .zip(operation_proofs.iter()) + { + if op_proof + .verified_reconstruct( + orig.value(), + factors_proof, + &commitments.reshuffle_commitments, + &commitments.rekey_commitments, + ) + .is_none() + { + return false; + } + } + + true + } + + /// Verify pseudonymization using cached commitments (convenience method). + #[must_use] + #[allow(clippy::too_many_arguments)] + pub fn verify_pseudonymization_cached( + &self, + transcryptor_id: &str, + original: &E, + result: &E, + operation_proof: &VerifiableRSK, + factors_proof: &RSKFactorsProof, + domain_from: &PseudonymizationDomain, + domain_to: &PseudonymizationDomain, + context_from: &EncryptionContext, + context_to: &EncryptionContext, + ) -> bool + where + E: ElGamalEncrypted + Pseudonymizable, + { + self.verify_pseudonymization_from_cache( + transcryptor_id, + original, + result, + operation_proof, + factors_proof, + domain_from, + domain_to, + context_from, + context_to, + ) + } +} + +/// Read-only view of the verifier's cache. +pub struct VerifierCache<'a> { + reshuffle: &'a ReshuffleCommitmentsCache, + pseudonym_rekey: &'a PseudonymRekeyCommitmentsCache, + attribute_rekey: &'a AttributeRekeyCommitmentsCache, +} + +impl<'a> VerifierCache<'a> { + /// Check if the cache is empty. + pub fn is_empty(&self) -> bool { + self.reshuffle.is_empty() + && self.pseudonym_rekey.is_empty() + && self.attribute_rekey.is_empty() + } + + /// Get total count of cached commitments. + pub fn total_count(&self) -> usize { + self.reshuffle.len() + self.pseudonym_rekey.len() + self.attribute_rekey.len() + } + + /// Get count of cached pseudonymization commitments. + pub fn pseudonymization_count(&self) -> usize { + self.reshuffle.len() + self.pseudonym_rekey.len() + } + + /// Get count of cached reshuffle commitments. + pub fn reshuffle_count(&self) -> usize { + self.reshuffle.len() + } + + /// Get count of cached pseudonym rekey commitments. + pub fn pseudonym_rekey_count(&self) -> usize { + self.pseudonym_rekey.len() + } + + /// Get count of cached attribute rekey commitments. + pub fn attribute_rekey_count(&self) -> usize { + self.attribute_rekey.len() + } +} + +impl Default for Verifier { + fn default() -> Self { + Self::new() + } +} diff --git a/src/lib/verifier/wasm.rs b/src/lib/verifier/wasm.rs new file mode 100644 index 0000000..b7a0d3f --- /dev/null +++ b/src/lib/verifier/wasm.rs @@ -0,0 +1,200 @@ +//! WASM bindings for the verifier. + +use crate::data::wasm::simple::{WASMEncryptedAttribute, WASMEncryptedPseudonym}; +use crate::factors::wasm::commitments::{ + WASMProvedPseudonymizationCommitments, WASMProvedRekeyCommitments, +}; +use crate::factors::wasm::contexts::{WASMEncryptionContext, WASMPseudonymizationDomain}; +use crate::verifier::Verifier; +use wasm_bindgen::prelude::*; + +/// A verifier for verifiable transcryption operations (WASM). +#[wasm_bindgen(js_name = Verifier)] +pub struct WASMVerifier { + inner: Verifier, +} + +#[wasm_bindgen(js_class = Verifier)] +impl WASMVerifier { + /// Create a new verifier with empty caches. + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + inner: Verifier::new(), + } + } + + /// Verify pseudonymization commitments. + #[wasm_bindgen(js_name = verifyPseudonymizationCommitments)] + pub fn verify_pseudonymization_commitments( + &self, + commitments: &WASMProvedPseudonymizationCommitments, + ) -> bool { + self.inner + .verify_pseudonymization_commitments(&commitments.0) + } + + /// Verify rekey commitments. + #[wasm_bindgen(js_name = verifyRekeyCommitments)] + pub fn verify_rekey_commitments(&self, commitments: &WASMProvedRekeyCommitments) -> bool { + self.inner.verify_rekey_commitments(&commitments.0) + } + + /// Register pseudonymization commitments for caching. + #[wasm_bindgen(js_name = registerPseudonymizationCommitments)] + pub fn register_pseudonymization_commitments( + &mut self, + transcryptor_id: &str, + domain_from: &WASMPseudonymizationDomain, + domain_to: &WASMPseudonymizationDomain, + context_from: &WASMEncryptionContext, + context_to: &WASMEncryptionContext, + commitments: &WASMProvedPseudonymizationCommitments, + ) { + self.inner.register_pseudonymization_commitments( + &transcryptor_id.to_string(), + &domain_from.0, + &domain_to.0, + &context_from.0, + &context_to.0, + commitments.0, + ); + } + + /// Register attribute rekey commitments for caching. + #[wasm_bindgen(js_name = registerAttributeRekeyCommitments)] + pub fn register_attribute_rekey_commitments( + &mut self, + transcryptor_id: &str, + context_from: &WASMEncryptionContext, + context_to: &WASMEncryptionContext, + commitments: &WASMProvedRekeyCommitments, + ) { + self.inner.register_attribute_rekey_commitments( + &transcryptor_id.to_string(), + &context_from.0, + &context_to.0, + commitments.0, + ); + } + + /// Check if reshuffle commitments exist in cache. + #[wasm_bindgen(js_name = hasReshuffleCommitments)] + pub fn has_reshuffle_commitments( + &self, + transcryptor_id: &str, + domain: &WASMPseudonymizationDomain, + ) -> bool { + self.inner + .has_reshuffle_commitments(transcryptor_id, &domain.0) + } + + /// Check if pseudonym rekey commitments exist in cache. + #[wasm_bindgen(js_name = hasPseudonymRekeyCommitments)] + pub fn has_pseudonym_rekey_commitments( + &self, + transcryptor_id: &str, + context: &WASMEncryptionContext, + ) -> bool { + self.inner + .has_pseudonym_rekey_commitments(transcryptor_id, &context.0) + } + + /// Check if attribute rekey commitments exist in cache. + #[wasm_bindgen(js_name = hasAttributeRekeyCommitments)] + pub fn has_attribute_rekey_commitments( + &self, + transcryptor_id: &str, + context: &WASMEncryptionContext, + ) -> bool { + self.inner + .has_attribute_rekey_commitments(transcryptor_id, &context.0) + } + + /// Clear all cached commitments. + #[wasm_bindgen(js_name = clearCache)] + pub fn clear_cache(&mut self) { + self.inner.clear_cache(); + } + + /// Get cache size. + #[wasm_bindgen(js_name = cacheSize)] + pub fn cache_size(&self) -> usize { + self.inner.cache().total_count() + } + + /// Verify a pseudonymization operation with commitments. + /// + /// Note: Proofs must be passed as JSON strings (due to WASM limitations). + #[cfg(feature = "serde")] + #[wasm_bindgen(js_name = verifyPseudonymization)] + pub fn verify_pseudonymization( + &self, + original: &WASMEncryptedPseudonym, + result: &WASMEncryptedPseudonym, + operation_proof_json: &str, + factors_proof_json: &str, + commitments: &WASMProvedPseudonymizationCommitments, + ) -> Result { + use crate::core::proved::{RSKFactorsProof, VerifiableRSK}; + + let operation_proof: VerifiableRSK = serde_json::from_str(operation_proof_json) + .map_err(|e| JsValue::from_str(&format!("{}", e)))?; + let factors_proof: RSKFactorsProof = serde_json::from_str(factors_proof_json) + .map_err(|e| JsValue::from_str(&format!("{}", e)))?; + + Ok(self.inner.verify_pseudonymization( + &original.0, + &result.0, + &operation_proof, + &factors_proof, + &commitments.0, + )) + } + + /// Verify a pseudonym rekey operation with commitments. + #[cfg(feature = "serde")] + #[wasm_bindgen(js_name = verifyPseudonymRekey)] + pub fn verify_pseudonym_rekey( + &self, + original: &WASMEncryptedPseudonym, + result: &WASMEncryptedPseudonym, + proof_json: &str, + commitments: &WASMProvedRekeyCommitments, + ) -> Result { + use crate::core::proved::VerifiableRekey; + + let proof: VerifiableRekey = + serde_json::from_str(proof_json).map_err(|e| JsValue::from_str(&format!("{}", e)))?; + + Ok(self + .inner + .verify_pseudonym_rekey(&original.0, &result.0, &proof, &commitments.0)) + } + + /// Verify an attribute rekey operation with commitments. + #[cfg(feature = "serde")] + #[wasm_bindgen(js_name = verifyAttributeRekey)] + pub fn verify_attribute_rekey( + &self, + original: &WASMEncryptedAttribute, + result: &WASMEncryptedAttribute, + proof_json: &str, + commitments: &WASMProvedRekeyCommitments, + ) -> Result { + use crate::core::proved::VerifiableRekey; + + let proof: VerifiableRekey = + serde_json::from_str(proof_json).map_err(|e| JsValue::from_str(&format!("{}", e)))?; + + Ok(self + .inner + .verify_attribute_rekey(&original.0, &result.0, &proof, &commitments.0)) + } +} + +impl Default for WASMVerifier { + fn default() -> Self { + Self::new() + } +} diff --git a/tests/verifiable.rs b/tests/verifiable.rs new file mode 100644 index 0000000..625be7e --- /dev/null +++ b/tests/verifiable.rs @@ -0,0 +1,711 @@ +//! Tests for verifiable transcryption operations. + +#![cfg(feature = "verifiable")] +#![allow(clippy::expect_used, clippy::unwrap_used)] + +use libpep::client::{decrypt, encrypt}; +use libpep::data::simple::*; +use libpep::data::traits::{ + VerifiablePseudonymizable, VerifiableRekeyable, VerifiableTranscryptable, +}; +use libpep::factors::contexts::*; +use libpep::factors::{EncryptionSecret, PseudonymizationSecret}; +use libpep::keys::*; +use libpep::transcryptor::Transcryptor; +use libpep::verifier::Verifier; + +#[test] +fn test_verifiable_pseudonymization_simple() { + let rng = &mut rand::rng(); + + // Setup + let (_pseudonym_global_public, pseudonym_global_secret) = make_pseudonym_global_keys(rng); + let pseudo_secret = PseudonymizationSecret::from("secret".into()); + let enc_secret = EncryptionSecret::from("secret".into()); + + let domain1 = PseudonymizationDomain::from("domain1"); + let domain2 = PseudonymizationDomain::from("domain2"); + let session1 = EncryptionContext::from("session1"); + let session2 = EncryptionContext::from("session2"); + + let (pseudonym_session1_public, _pseudonym_session1_secret) = + make_pseudonym_session_keys(&pseudonym_global_secret, &session1, &enc_secret); + let (_pseudonym_session2_public, pseudonym_session2_secret) = + make_pseudonym_session_keys(&pseudonym_global_secret, &session2, &enc_secret); + + // Client: Encrypt a pseudonym + let pseudo = Pseudonym::random(rng); + let enc_pseudo = encrypt(&pseudo, &pseudonym_session1_public, rng); + + // Transcryptor: Generate secret info and public commitments + let transcryptor = Transcryptor::new(pseudo_secret.clone(), enc_secret.clone()); + let info = transcryptor.pseudonymization_info(&domain1, &domain2, &session1, &session2); + let commitments = Transcryptor::pseudonymization_commitments(&info, rng); + + // Transcryptor: Perform verifiable pseudonymization + let operation_proof = enc_pseudo.verifiable_pseudonymize(&info, rng); + let factors_proof = Transcryptor::pseudonymization_factors_proof(&info, rng); + let result = EncryptedPseudonym::from_value(operation_proof.result()); + + // Verifier: Verify commitments and operation (uses only public data) + let verifier = Verifier::new(); + assert!(verifier.verify_pseudonymization_commitments(&commitments)); + assert!(verifier.verify_pseudonymization( + &enc_pseudo, + &result, + &operation_proof, + &factors_proof, + &commitments, + )); + + // Client: Decrypt result + #[cfg(feature = "elgamal3")] + let _decrypted = + decrypt(&result, &pseudonym_session2_secret).expect("decryption should succeed"); + #[cfg(not(feature = "elgamal3"))] + let _decrypted = decrypt(&result, &pseudonym_session2_secret); +} + +#[test] +fn test_verifiable_pseudonym_rekey() { + let rng = &mut rand::rng(); + + // Setup + let (_pseudonym_global_public, pseudonym_global_secret) = make_pseudonym_global_keys(rng); + let pseudo_secret = PseudonymizationSecret::from("secret".into()); + let enc_secret = EncryptionSecret::from("secret".into()); + + let session1 = EncryptionContext::from("session1"); + let session2 = EncryptionContext::from("session2"); + + let (pseudonym_session1_public, pseudonym_session1_secret) = + make_pseudonym_session_keys(&pseudonym_global_secret, &session1, &enc_secret); + let (_pseudonym_session2_public, pseudonym_session2_secret) = + make_pseudonym_session_keys(&pseudonym_global_secret, &session2, &enc_secret); + + // Client: Encrypt a pseudonym + let pseudo = Pseudonym::random(rng); + let enc_pseudo = encrypt(&pseudo, &pseudonym_session1_public, rng); + + // Transcryptor: Generate secret rekey info and public commitments + let transcryptor = Transcryptor::new(pseudo_secret.clone(), enc_secret.clone()); + let info = transcryptor.pseudonym_rekey_info(&session1, &session2); + let commitments = Transcryptor::pseudonym_rekey_commitments(&info, rng); + + // Transcryptor: Perform verifiable rekey + let operation_proof = enc_pseudo.verifiable_rekey(&info, rng); + let result = EncryptedPseudonym::from_value(operation_proof.result(enc_pseudo.value())); + + // Verifier: Verify commitments and operation + let verifier = Verifier::new(); + assert!(verifier.verify_rekey_commitments(&commitments)); + assert!(verifier.verify_pseudonym_rekey(&enc_pseudo, &result, &operation_proof, &commitments,)); + + // Client: Verify result decrypts correctly (same plaintext, different session) + #[cfg(feature = "elgamal3")] + let decrypted = + decrypt(&result, &pseudonym_session2_secret).expect("decryption should succeed"); + #[cfg(not(feature = "elgamal3"))] + let decrypted = decrypt(&result, &pseudonym_session2_secret); + + #[cfg(feature = "elgamal3")] + let original_decrypted = + decrypt(&enc_pseudo, &pseudonym_session1_secret).expect("decryption should succeed"); + #[cfg(not(feature = "elgamal3"))] + let original_decrypted = decrypt(&enc_pseudo, &pseudonym_session1_secret); + + assert_eq!(decrypted, original_decrypted); +} + +#[test] +fn test_verifiable_attribute_rekey() { + let rng = &mut rand::rng(); + + // Setup + let (_attribute_global_public, attribute_global_secret) = make_attribute_global_keys(rng); + let pseudo_secret = PseudonymizationSecret::from("secret".into()); + let enc_secret = EncryptionSecret::from("secret".into()); + + let session1 = EncryptionContext::from("session1"); + let session2 = EncryptionContext::from("session2"); + + let (attribute_session1_public, attribute_session1_secret) = + make_attribute_session_keys(&attribute_global_secret, &session1, &enc_secret); + let (_attribute_session2_public, attribute_session2_secret) = + make_attribute_session_keys(&attribute_global_secret, &session2, &enc_secret); + + // Client: Encrypt an attribute + let attr = Attribute::random(rng); + let enc_attr = encrypt(&attr, &attribute_session1_public, rng); + + // Transcryptor: Generate secret rekey info and public commitments + let transcryptor = Transcryptor::new(pseudo_secret.clone(), enc_secret.clone()); + let info = transcryptor.attribute_rekey_info(&session1, &session2); + let commitments = Transcryptor::attribute_rekey_commitments(&info, rng); + + // Transcryptor: Perform verifiable rekey + let operation_proof = enc_attr.verifiable_rekey(&info, rng); + let result = EncryptedAttribute::from_value(operation_proof.result(enc_attr.value())); + + // Verifier: Verify commitments and operation + let verifier = Verifier::new(); + assert!(verifier.verify_rekey_commitments(&commitments)); + assert!(verifier.verify_attribute_rekey(&enc_attr, &result, &operation_proof, &commitments,)); + + // Client: Verify result decrypts correctly + #[cfg(feature = "elgamal3")] + let decrypted = + decrypt(&result, &attribute_session2_secret).expect("decryption should succeed"); + #[cfg(not(feature = "elgamal3"))] + let decrypted = decrypt(&result, &attribute_session2_secret); + + #[cfg(feature = "elgamal3")] + let original_decrypted = + decrypt(&enc_attr, &attribute_session1_secret).expect("decryption should succeed"); + #[cfg(not(feature = "elgamal3"))] + let original_decrypted = decrypt(&enc_attr, &attribute_session1_secret); + + assert_eq!(decrypted, original_decrypted); +} + +#[test] +fn test_verification_fails_with_wrong_commitments() { + let rng = &mut rand::rng(); + + // Setup + let (_pseudonym_global_public, pseudonym_global_secret) = make_pseudonym_global_keys(rng); + let pseudo_secret = PseudonymizationSecret::from("secret".into()); + let enc_secret = EncryptionSecret::from("secret".into()); + + let domain1 = PseudonymizationDomain::from("domain1"); + let domain2 = PseudonymizationDomain::from("domain2"); + let domain3 = PseudonymizationDomain::from("domain3"); + let session1 = EncryptionContext::from("session1"); + let session2 = EncryptionContext::from("session2"); + + let (pseudonym_session1_public, _) = + make_pseudonym_session_keys(&pseudonym_global_secret, &session1, &enc_secret); + + // Client: Encrypt a pseudonym + let pseudo = Pseudonym::random(rng); + let enc_pseudo = encrypt(&pseudo, &pseudonym_session1_public, rng); + + // Transcryptor: Generate correct and wrong commitments + let transcryptor = Transcryptor::new(pseudo_secret.clone(), enc_secret.clone()); + let info = transcryptor.pseudonymization_info(&domain1, &domain2, &session1, &session2); + let commitments = Transcryptor::pseudonymization_commitments(&info, rng); + + let wrong_info = transcryptor.pseudonymization_info(&domain1, &domain3, &session1, &session2); + let wrong_commitments = Transcryptor::pseudonymization_commitments(&wrong_info, rng); + + // Transcryptor: Perform operation with correct info + let operation_proof = enc_pseudo.verifiable_pseudonymize(&info, rng); + let factors_proof = Transcryptor::pseudonymization_factors_proof(&info, rng); + let result = EncryptedPseudonym::from_value(operation_proof.result()); + + // Verifier: Create mixed (incorrect) commitments + use libpep::factors::ProvedPseudonymizationCommitments; + let mixed_commitments = ProvedPseudonymizationCommitments { + reshuffle_commitments: commitments.reshuffle_commitments, + reshuffle_proof: wrong_commitments.reshuffle_proof, + rekey_commitments: commitments.rekey_commitments, + rekey_proof: wrong_commitments.rekey_proof, + }; + + // Verifier: Verification with mixed commitments should fail + let verifier = Verifier::new(); + assert!(!verifier.verify_pseudonymization_commitments(&mixed_commitments)); + + // Verifier: Verification with correct commitments should succeed + assert!(verifier.verify_pseudonymization( + &enc_pseudo, + &result, + &operation_proof, + &factors_proof, + &commitments, + )); +} + +#[test] +fn test_transcryptor_generic_verifiable_methods() { + let rng = &mut rand::rng(); + + // Setup + let (_pseudonym_global_public, pseudonym_global_secret) = make_pseudonym_global_keys(rng); + let pseudo_secret = PseudonymizationSecret::from("secret".into()); + let enc_secret = EncryptionSecret::from("secret".into()); + + let domain1 = PseudonymizationDomain::from("domain1"); + let domain2 = PseudonymizationDomain::from("domain2"); + let session1 = EncryptionContext::from("session1"); + let session2 = EncryptionContext::from("session2"); + + let (pseudonym_session1_public, _) = + make_pseudonym_session_keys(&pseudonym_global_secret, &session1, &enc_secret); + + // Client: Encrypt a pseudonym + let pseudo = Pseudonym::random(rng); + let enc_pseudo = encrypt(&pseudo, &pseudonym_session1_public, rng); + + // Transcryptor: Generate info and commitments + let transcryptor = Transcryptor::new(pseudo_secret.clone(), enc_secret.clone()); + let info = transcryptor.pseudonymization_info(&domain1, &domain2, &session1, &session2); + let commitments = Transcryptor::pseudonymization_commitments(&info, rng); + + // Transcryptor: Use generic verifiable_pseudonymize method + let operation_proof = transcryptor.verifiable_pseudonymize(&enc_pseudo, &info, rng); + let factors_proof = Transcryptor::pseudonymization_factors_proof(&info, rng); + let result = EncryptedPseudonym::from_value(operation_proof.result()); + + // Verifier: Verify commitments and operation + let verifier = Verifier::new(); + assert!(verifier.verify_pseudonymization_commitments(&commitments)); + assert!(verifier.verify_pseudonymization( + &enc_pseudo, + &result, + &operation_proof, + &factors_proof, + &commitments, + )); +} + +#[test] +fn test_verifiable_long_pseudonym_pseudonymization() { + let rng = &mut rand::rng(); + + // Setup keys and secrets + let (_pseudonym_global_public, pseudonym_global_secret) = make_pseudonym_global_keys(rng); + let pseudo_secret = PseudonymizationSecret::from("secret".into()); + let enc_secret = EncryptionSecret::from("secret".into()); + let transcryptor = Transcryptor::new(pseudo_secret.clone(), enc_secret.clone()); + + let domain1 = PseudonymizationDomain::from("domain1"); + let domain2 = PseudonymizationDomain::from("domain2"); + let session1 = EncryptionContext::from("session1"); + let session2 = EncryptionContext::from("session2"); + + let (pseudonym_session1_public, _) = + make_pseudonym_session_keys(&pseudonym_global_secret, &session1, &enc_secret); + let (_, pseudonym_session2_secret) = + make_pseudonym_session_keys(&pseudonym_global_secret, &session2, &enc_secret); + + // Create and encrypt a long pseudonym (multi-block) + use libpep::data::long::LongPseudonym; + + let data = b"This is a long pseudonym that spans multiple blocks!"; + let long_pseudo = LongPseudonym::from_bytes_padded(data); + let enc_long_pseudo = encrypt(&long_pseudo, &pseudonym_session1_public, rng); + + // Generate secret info and public commitments + let info = transcryptor.pseudonymization_info(&domain1, &domain2, &session1, &session2); + let commitments = Transcryptor::pseudonymization_commitments(&info, rng); + + // Perform verifiable pseudonymization + let operation_proofs = enc_long_pseudo.verifiable_pseudonymize(&info, rng); + let factors_proof = Transcryptor::pseudonymization_factors_proof(&info, rng); + + // Extract results from proofs + use libpep::data::long::LongEncryptedPseudonym; + let result = LongEncryptedPseudonym( + operation_proofs + .iter() + .map(|proof| EncryptedPseudonym::from_value(proof.result())) + .collect(), + ); + + // Verify + let verifier = Verifier::new(); + assert!(verifier.verify_pseudonymization_commitments(&commitments)); + assert!(verifier.verify_pseudonymization_long( + &enc_long_pseudo.0, + &result.0, + &operation_proofs, + &factors_proof, + &commitments, + )); + + // Verify result decrypts correctly + #[cfg(feature = "elgamal3")] + let _decrypted = + decrypt(&result, &pseudonym_session2_secret).expect("decryption should succeed"); + #[cfg(not(feature = "elgamal3"))] + let _decrypted = decrypt(&result, &pseudonym_session2_secret); + + // Note: After pseudonymization (domain change), the pseudonym is no longer in the + // original format and cannot be converted back to the original bytes. + // The important thing is that the operation is verifiable and decryption succeeds. +} + +#[test] +fn test_verifiable_record_transcryption() { + let rng = &mut rand::rng(); + + // Setup + let (_pseudonym_global_public, pseudonym_global_secret) = make_pseudonym_global_keys(rng); + let (_attribute_global_public, attribute_global_secret) = make_attribute_global_keys(rng); + let pseudo_secret = PseudonymizationSecret::from("secret".into()); + let enc_secret = EncryptionSecret::from("secret".into()); + let transcryptor = Transcryptor::new(pseudo_secret.clone(), enc_secret.clone()); + + let domain1 = PseudonymizationDomain::from("domain1"); + let domain2 = PseudonymizationDomain::from("domain2"); + let session1 = EncryptionContext::from("session1"); + let session2 = EncryptionContext::from("session2"); + + // Create session keys + let (pseudonym_session1_public, _) = + make_pseudonym_session_keys(&pseudonym_global_secret, &session1, &enc_secret); + let (attribute_session1_public, _) = + make_attribute_session_keys(&attribute_global_secret, &session1, &enc_secret); + let (_, pseudonym_session2_secret) = + make_pseudonym_session_keys(&pseudonym_global_secret, &session2, &enc_secret); + let (_, attribute_session2_secret) = + make_attribute_session_keys(&attribute_global_secret, &session2, &enc_secret); + + let session1_keys = libpep::keys::SessionKeys { + pseudonym: libpep::keys::PseudonymSessionKeys { + public: pseudonym_session1_public, + secret: pseudonym_session2_secret, + }, + attribute: libpep::keys::AttributeSessionKeys { + public: attribute_session1_public, + secret: attribute_session2_secret, + }, + }; + + // Create a record with pseudonyms and attributes + use libpep::data::records::{EncryptedRecord, Record}; + let record = Record::new( + vec![Pseudonym::random(rng), Pseudonym::random(rng)], + vec![ + Attribute::random(rng), + Attribute::random(rng), + Attribute::random(rng), + ], + ); + + let enc_record = encrypt(&record, &session1_keys, rng); + + // Generate transcryption info and commitments + let transcryption_info = + transcryptor.transcryption_info(&domain1, &domain2, &session1, &session2); + let pseudonym_commitments = + Transcryptor::pseudonymization_commitments(&transcryption_info.pseudonym, rng); + let attribute_commitments = + Transcryptor::attribute_rekey_commitments(&transcryption_info.attribute, rng); + + // Perform verifiable transcryption + use libpep::data::traits::VerifiableTranscryptable; + let proof = enc_record.verifiable_transcrypt(&transcryption_info, rng); + + // Extract result from proof + let result = EncryptedRecord::new( + proof + .pseudonym_operation_proofs + .iter() + .map(|p| EncryptedPseudonym::from_value(p.result())) + .collect(), + proof + .attribute_operation_proofs + .iter() + .zip(enc_record.attributes.iter()) + .map(|(p, orig)| EncryptedAttribute::from_value(p.result(orig.value()))) + .collect(), + ); + + // Verify + let verifier = Verifier::new(); + assert!(verifier.verify_pseudonymization_commitments(&pseudonym_commitments)); + assert!(verifier.verify_rekey_commitments(&attribute_commitments)); + assert!(verifier.verify_record_transcryption( + &enc_record, + &result, + &proof, + &pseudonym_commitments, + &attribute_commitments, + )); + + // Verify the record can be decrypted (though we can't verify correctness without proper keys) + let session2_keys = libpep::keys::SessionKeys { + pseudonym: libpep::keys::PseudonymSessionKeys { + public: pseudonym_session1_public, + secret: pseudonym_session2_secret, + }, + attribute: libpep::keys::AttributeSessionKeys { + public: attribute_session1_public, + secret: attribute_session2_secret, + }, + }; + + #[cfg(feature = "elgamal3")] + let _decrypted = decrypt(&result, &session2_keys).expect("decryption should succeed"); + #[cfg(not(feature = "elgamal3"))] + let _decrypted = decrypt(&result, &session2_keys); +} + +#[test] +fn test_verifier_cache() { + let rng = &mut rand::rng(); + + // Setup + let (_pseudonym_global_public, pseudonym_global_secret) = make_pseudonym_global_keys(rng); + let pseudo_secret = PseudonymizationSecret::from("secret".into()); + let enc_secret = EncryptionSecret::from("secret".into()); + + let domain1 = PseudonymizationDomain::from("domain1"); + let domain2 = PseudonymizationDomain::from("domain2"); + let domain3 = PseudonymizationDomain::from("domain3"); + let session1 = EncryptionContext::from("session1"); + let session2 = EncryptionContext::from("session2"); + + let (pseudonym_session1_public, _) = + make_pseudonym_session_keys(&pseudonym_global_secret, &session1, &enc_secret); + + // Transcryptor: Generate info and commitments + let transcryptor = Transcryptor::new(pseudo_secret.clone(), enc_secret.clone()); + let info = transcryptor.pseudonymization_info(&domain1, &domain2, &session1, &session2); + let commitments = Transcryptor::pseudonymization_commitments(&info, rng); + + // Verifier: Register commitments in cache + let mut verifier = Verifier::new(); + assert!(verifier.cache().is_empty()); + assert_eq!(verifier.cache().total_count(), 0); + + let transcryptor_id = String::from("transcryptor1"); + verifier.register_pseudonymization_commitments( + &transcryptor_id, + &domain1, + &domain2, + &session1, + &session2, + commitments, + ); + + assert!(!verifier.cache().is_empty()); + assert!(verifier.cache().total_count() >= 2); + assert!(verifier.cache().total_count() <= 4); + + // Verifier: Check cache contents + assert!(verifier.has_reshuffle_commitments(&transcryptor_id, &domain1)); + assert!(verifier.has_pseudonym_rekey_commitments(&transcryptor_id, &session1)); + assert!(!verifier.has_reshuffle_commitments(&transcryptor_id, &domain3)); + + // Client: Encrypt data + let pseudo = Pseudonym::random(rng); + let enc_pseudo = encrypt(&pseudo, &pseudonym_session1_public, rng); + + // Transcryptor: Perform operation + let operation_proof = enc_pseudo.verifiable_pseudonymize(&info, rng); + let factors_proof = Transcryptor::pseudonymization_factors_proof(&info, rng); + let result = EncryptedPseudonym::from_value(operation_proof.result()); + + // Verifier: Verify using cached commitments + assert!(verifier.verify_pseudonymization_cached( + &transcryptor_id, + &enc_pseudo, + &result, + &operation_proof, + &factors_proof, + &domain1, + &domain2, + &session1, + &session2, + )); + + // Verifier: Verification with wrong domains should fail (not in cache) + assert!(!verifier.verify_pseudonymization_cached( + &transcryptor_id, + &enc_pseudo, + &result, + &operation_proof, + &factors_proof, + &domain1, + &domain3, + &session1, + &session2, + )); + + // Verifier: Clear cache + verifier.clear_cache(); + assert!(verifier.cache().is_empty()); + assert_eq!(verifier.cache().total_count(), 0); +} + +#[test] +fn test_two_transcryptors_with_verification() { + // Demonstrates distributed transcryption with two transcryptors performing + // commutative partial transformations on the same domain transition (A→B). + + use libpep::client::distributed::{make_attribute_session_key, make_pseudonym_session_key}; + use libpep::data::records::EncryptedRecord; + use libpep::keys::distribution::make_distributed_global_keys; + use libpep::transcryptor::DistributedTranscryptor; + + let rng = &mut rand::rng(); + + let enc_secret1 = EncryptionSecret::from("encryption1".into()); + let enc_secret2 = EncryptionSecret::from("encryption2".into()); + + // Setup distributed system with 2 transcryptors + let (_global_public_keys, blinded_global_keys, blinding_factors) = + make_distributed_global_keys(2, rng); + + let transcryptor1 = DistributedTranscryptor::new( + PseudonymizationSecret::from("secret1".into()), + enc_secret1.clone(), + blinding_factors[0], + ); + let transcryptor2 = DistributedTranscryptor::new( + PseudonymizationSecret::from("secret2".into()), + enc_secret2.clone(), + blinding_factors[1], + ); + + let domain_a = PseudonymizationDomain::from("domain_a"); + let domain_b = PseudonymizationDomain::from("domain_b"); + let session1 = EncryptionContext::from("session1"); + let session2 = EncryptionContext::from("session2"); + + // Setup: Reconstruct session1 keys from both transcryptors' shares + let session1_shares = [ + transcryptor1.session_key_shares(&session1), + transcryptor2.session_key_shares(&session1), + ]; + + let (pseudonym_session1_public, _pseudonym_session1_secret) = make_pseudonym_session_key( + blinded_global_keys.pseudonym, + &session1_shares + .iter() + .map(|s| s.pseudonym) + .collect::>(), + ); + let (attribute_session1_public, _attribute_session1_secret) = make_attribute_session_key( + blinded_global_keys.attribute, + &session1_shares + .iter() + .map(|s| s.attribute) + .collect::>(), + ); + + // Client: Encrypt data and create record + let pseudo = Pseudonym::random(rng); + let attr = Attribute::random(rng); + let enc_pseudo = encrypt(&pseudo, &pseudonym_session1_public, rng); + let enc_attr = encrypt(&attr, &attribute_session1_public, rng); + + let record = EncryptedRecord { + pseudonyms: vec![enc_pseudo], + attributes: vec![enc_attr], + }; + + // Transcryptor1: Generate commitments and perform partial transformation + let info1 = transcryptor1.transcryption_info(&domain_a, &domain_b, &session1, &session2); + let pseudonym_commitments1 = Transcryptor::pseudonymization_commitments(&info1.pseudonym, rng); + let attribute_commitments1 = Transcryptor::attribute_rekey_commitments(&info1.attribute, rng); + + let proof1 = record.verifiable_transcrypt(&info1, rng); + let result1 = EncryptedRecord::new( + proof1 + .pseudonym_operation_proofs + .iter() + .map(|p| EncryptedPseudonym::from_value(p.result())) + .collect(), + proof1 + .attribute_operation_proofs + .iter() + .zip(record.attributes.iter()) + .map(|(p, orig)| EncryptedAttribute::from_value(p.result(orig.value()))) + .collect(), + ); + + // Verifier: Register and verify transcryptor1's work + let mut verifier = Verifier::new(); + let transcryptor1_id = String::from("transcryptor1"); + + verifier.register_pseudonymization_commitments( + &transcryptor1_id, + &domain_a, + &domain_b, + &session1, + &session2, + pseudonym_commitments1, + ); + verifier.register_attribute_rekey_commitments( + &transcryptor1_id, + &session1, + &session2, + attribute_commitments1, + ); + + assert!(verifier.verify_pseudonymization_commitments(&pseudonym_commitments1)); + assert!(verifier.verify_rekey_commitments(&attribute_commitments1)); + assert!(verifier.verify_record_transcryption( + &record, + &result1, + &proof1, + &pseudonym_commitments1, + &attribute_commitments1, + )); + + // Transcryptor2: Generate commitments and perform another partial transformation + let info2 = transcryptor2.transcryption_info(&domain_a, &domain_b, &session1, &session2); + let pseudonym_commitments2 = Transcryptor::pseudonymization_commitments(&info2.pseudonym, rng); + let attribute_commitments2 = Transcryptor::attribute_rekey_commitments(&info2.attribute, rng); + + let proof2 = result1.verifiable_transcrypt(&info2, rng); + let result2 = EncryptedRecord::new( + proof2 + .pseudonym_operation_proofs + .iter() + .map(|p| EncryptedPseudonym::from_value(p.result())) + .collect(), + proof2 + .attribute_operation_proofs + .iter() + .zip(result1.attributes.iter()) + .map(|(p, orig)| EncryptedAttribute::from_value(p.result(orig.value()))) + .collect(), + ); + + // Verifier: Verify transcryptor2's work + let verifier2 = Verifier::new(); + assert!(verifier2.verify_pseudonymization_commitments(&pseudonym_commitments2)); + assert!(verifier2.verify_rekey_commitments(&attribute_commitments2)); + assert!(verifier2.verify_record_transcryption( + &result1, + &result2, + &proof2, + &pseudonym_commitments2, + &attribute_commitments2, + )); + + // Client: Decrypt final result using reconstructed session2 keys + #[cfg(feature = "elgamal3")] + { + let session2_shares = [ + transcryptor1.session_key_shares(&session2), + transcryptor2.session_key_shares(&session2), + ]; + + let (_pseudonym_session2_public, pseudonym_session2_secret) = make_pseudonym_session_key( + blinded_global_keys.pseudonym, + &session2_shares + .iter() + .map(|s| s.pseudonym) + .collect::>(), + ); + let (_attribute_session2_public, attribute_session2_secret) = make_attribute_session_key( + blinded_global_keys.attribute, + &session2_shares + .iter() + .map(|s| s.attribute) + .collect::>(), + ); + + let _final_pseudo = decrypt(&result2.pseudonyms[0], &pseudonym_session2_secret) + .expect("decrypt final pseudonym failed"); + let final_attr = decrypt(&result2.attributes[0], &attribute_session2_secret) + .expect("decrypt final attribute failed"); + assert_eq!(final_attr, attr); + } +}