elastic_elgamal/app/
choice.rs

1//! Encrypted choice.
2
3use merlin::Transcript;
4use rand_core::{CryptoRng, RngCore};
5#[cfg(feature = "serde")]
6use serde::{de::DeserializeOwned, Deserialize, Serialize};
7use zeroize::Zeroizing;
8
9use core::{fmt, iter, ops};
10
11use crate::{
12    alloc::{vec, Vec},
13    group::Group,
14    Ciphertext, CiphertextWithValue, LogEqualityProof, PublicKey, RingProof, RingProofBuilder,
15    VerificationError,
16};
17
18/// Encapsulation of functionality for proving and verifying correctness of the sum of option
19/// ciphertexts in an [`EncryptedChoice`].
20///
21/// This trait is not meant to be implemented for external types.
22pub trait ProveSum<G: Group>: Clone + crate::sealed::Sealed {
23    /// Produced / verified proofs.
24    #[cfg(not(feature = "serde"))]
25    type Proof: Sized;
26    /// Produced / verified proofs.
27    #[cfg(feature = "serde")]
28    type Proof: Sized + Serialize + DeserializeOwned;
29
30    #[doc(hidden)]
31    fn prove<R: CryptoRng + RngCore>(
32        &self,
33        ciphertext: &CiphertextWithValue<G, u64>,
34        receiver: &PublicKey<G>,
35        rng: &mut R,
36    ) -> Self::Proof;
37
38    #[doc(hidden)]
39    fn verify(
40        &self,
41        ciphertext: &Ciphertext<G>,
42        proof: &Self::Proof,
43        receiver: &PublicKey<G>,
44    ) -> Result<(), ChoiceVerificationError>;
45}
46
47/// Single-choice setup for [`EncryptedChoice`], in which it can contain a single selected option.
48///
49/// # Examples
50///
51/// See [`EncryptedChoice`] docs for an example of usage.
52#[derive(Debug, Clone, Copy)]
53pub struct SingleChoice(());
54
55impl crate::sealed::Sealed for SingleChoice {}
56
57impl<G: Group> ProveSum<G> for SingleChoice {
58    type Proof = LogEqualityProof<G>;
59
60    fn prove<R: CryptoRng + RngCore>(
61        &self,
62        ciphertext: &CiphertextWithValue<G, u64>,
63        receiver: &PublicKey<G>,
64        rng: &mut R,
65    ) -> Self::Proof {
66        LogEqualityProof::new(
67            receiver,
68            ciphertext.randomness(),
69            (
70                ciphertext.inner().random_element,
71                ciphertext.inner().blinded_element - G::generator(),
72            ),
73            &mut Transcript::new(b"choice_encryption_sum"),
74            rng,
75        )
76    }
77
78    fn verify(
79        &self,
80        ciphertext: &Ciphertext<G>,
81        proof: &Self::Proof,
82        receiver: &PublicKey<G>,
83    ) -> Result<(), ChoiceVerificationError> {
84        let powers = (
85            ciphertext.random_element,
86            ciphertext.blinded_element - G::generator(),
87        );
88        proof
89            .verify(
90                receiver,
91                powers,
92                &mut Transcript::new(b"choice_encryption_sum"),
93            )
94            .map_err(ChoiceVerificationError::Sum)
95    }
96}
97
98/// Multi-choice setup for [`EncryptedChoice`], in which it can contain any possible number
99/// of selected options (`0..=n`, where `n` is the number of options).
100///
101/// # Examples
102///
103/// See [`EncryptedChoice`] docs for an example of usage.
104#[derive(Debug, Clone, Copy)]
105pub struct MultiChoice(());
106
107impl crate::sealed::Sealed for MultiChoice {}
108
109impl<G: Group> ProveSum<G> for MultiChoice {
110    type Proof = ();
111
112    fn prove<R: CryptoRng + RngCore>(
113        &self,
114        _ciphertext: &CiphertextWithValue<G, u64>,
115        _receiver: &PublicKey<G>,
116        _rng: &mut R,
117    ) -> Self::Proof {
118        // Do nothing.
119    }
120
121    fn verify(
122        &self,
123        _ciphertext: &Ciphertext<G>,
124        _proof: &Self::Proof,
125        _receiver: &PublicKey<G>,
126    ) -> Result<(), ChoiceVerificationError> {
127        Ok(()) // no failure conditions
128    }
129}
130
131/// Parameters of an [`EncryptedChoice`] polling.
132#[derive(Debug)]
133pub struct ChoiceParams<G: Group, S: ProveSum<G>> {
134    options_count: usize,
135    sum_prover: S,
136    receiver: PublicKey<G>,
137}
138
139impl<G: Group, S: ProveSum<G>> Clone for ChoiceParams<G, S> {
140    fn clone(&self) -> Self {
141        Self {
142            options_count: self.options_count,
143            sum_prover: self.sum_prover.clone(),
144            receiver: self.receiver.clone(),
145        }
146    }
147}
148
149impl<G: Group, S: ProveSum<G>> ChoiceParams<G, S> {
150    fn check_options_count(&self, actual_count: usize) -> Result<(), ChoiceVerificationError> {
151        if self.options_count == actual_count {
152            Ok(())
153        } else {
154            Err(ChoiceVerificationError::OptionsLenMismatch {
155                expected: self.options_count,
156                actual: actual_count,
157            })
158        }
159    }
160
161    /// Returns the public key for which the [`EncryptedChoice`] are encrypted.
162    pub fn receiver(&self) -> &PublicKey<G> {
163        &self.receiver
164    }
165
166    /// Returns the number of options in these parameters.
167    pub fn options_count(&self) -> usize {
168        self.options_count
169    }
170}
171
172impl<G: Group> ChoiceParams<G, SingleChoice> {
173    /// Creates parameters for a single-choice polling.
174    ///
175    /// # Panics
176    ///
177    /// Panics if provided `options_count` is zero.
178    pub fn single(receiver: PublicKey<G>, options_count: usize) -> Self {
179        assert!(options_count > 0, "Number of options must be positive");
180        Self {
181            options_count,
182            sum_prover: SingleChoice(()),
183            receiver,
184        }
185    }
186}
187
188impl<G: Group> ChoiceParams<G, MultiChoice> {
189    /// Creates parameters for a multi-choice polling.
190    ///
191    /// # Panics
192    ///
193    /// Panics if provided `options_count` is zero.
194    pub fn multi(receiver: PublicKey<G>, options_count: usize) -> Self {
195        assert!(options_count > 0, "Number of options must be positive");
196        Self {
197            options_count,
198            sum_prover: MultiChoice(()),
199            receiver,
200        }
201    }
202}
203
204/// Zero or more encrypted choices from `n` options (`n >= 1`) together with zero-knowledge
205/// proofs of correctness.
206///
207/// # Construction
208///
209/// The choice is represented as a vector of `n` *choice ciphertexts* of Boolean values (0 or 1),
210/// where the ciphertexts for the chosen options encrypt 1 and the other ciphertexts encrypt 0.
211/// This ensures that multiple [`EncryptedChoice`]s can be added (e.g., within a voting protocol).
212///
213/// Zero-knowledge proofs are:
214///
215/// - A [`RingProof`] attesting that all `n` ciphertexts encrypt 0 or 1.
216///   This proof can be obtained via [`Self::range_proof()`].
217/// - A [`LogEqualityProof`] attesting that the encrypted values sum up to 1. Combined with
218///   the range proof, this means that exactly one of encrypted values is 1, and all others are 0.
219///   This proof can be obtained via [`Self::sum_proof()`]. This proof is absent for
220///   a [`MultiChoice`] setup (`sum_proof()` just returns `()`).
221///
222/// # Examples
223///
224/// ## Single-choice setup
225///
226/// ```
227/// # use elastic_elgamal::{
228/// #     app::{ChoiceParams, EncryptedChoice}, group::Ristretto, DiscreteLogTable, Keypair,
229/// # };
230/// # use rand::thread_rng;
231/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
232/// let mut rng = thread_rng();
233/// let (pk, sk) = Keypair::<Ristretto>::generate(&mut rng).into_tuple();
234/// let choice_params = ChoiceParams::single(pk, 5);
235///
236/// let choice = 2;
237/// let enc = EncryptedChoice::single(&choice_params, choice, &mut rng);
238/// let choices = enc.verify(&choice_params)?;
239///
240/// // `choices` is a slice of 5 Boolean value ciphertexts
241/// assert_eq!(choices.len(), 5);
242/// let lookup_table = DiscreteLogTable::new(0..=1);
243/// for (idx, &v) in choices.iter().enumerate() {
244///     assert_eq!(
245///         sk.decrypt(v, &lookup_table),
246///         Some((idx == choice) as u64)
247///     );
248/// }
249/// # Ok(())
250/// # }
251/// ```
252///
253/// ## Multi-choice setup
254///
255/// ```
256/// # use elastic_elgamal::{
257/// #     app::{ChoiceParams, EncryptedChoice}, group::Ristretto, DiscreteLogTable, Keypair,
258/// # };
259/// # use rand::thread_rng;
260/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
261/// let mut rng = thread_rng();
262/// let (pk, sk) = Keypair::<Ristretto>::generate(&mut rng).into_tuple();
263/// let choice_params = ChoiceParams::multi(pk, 5);
264///
265/// let choices = [true, false, true, true, false];
266/// let enc = EncryptedChoice::new(&choice_params, &choices, &mut rng);
267/// let recovered_choices = enc.verify(&choice_params)?;
268///
269/// let lookup_table = DiscreteLogTable::new(0..=1);
270/// for (idx, &v) in recovered_choices.iter().enumerate() {
271///     assert_eq!(sk.decrypt(v, &lookup_table), Some(choices[idx] as u64));
272/// }
273/// # Ok(())
274/// # }
275/// ```
276#[derive(Debug, Clone)]
277#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
278#[cfg_attr(feature = "serde", serde(bound = ""))]
279pub struct EncryptedChoice<G: Group, S: ProveSum<G>> {
280    choices: Vec<Ciphertext<G>>,
281    range_proof: RingProof<G>,
282    sum_proof: S::Proof,
283}
284
285impl<G: Group> EncryptedChoice<G, SingleChoice> {
286    /// Creates a new encrypted choice.
287    ///
288    /// # Panics
289    ///
290    /// Panics if `choice` exceeds the maximum index allowed by `params`.
291    pub fn single<R: CryptoRng + RngCore>(
292        params: &ChoiceParams<G, SingleChoice>,
293        choice: usize,
294        rng: &mut R,
295    ) -> Self {
296        assert!(
297            choice < params.options_count,
298            "invalid choice {choice}; expected a value in 0..{}",
299            params.options_count
300        );
301        let choices: Vec<_> = (0..params.options_count).map(|i| choice == i).collect();
302        Self::new(params, &Zeroizing::new(choices), rng)
303    }
304}
305
306#[allow(clippy::len_without_is_empty)] // `is_empty()` would always be false
307impl<G: Group, S: ProveSum<G>> EncryptedChoice<G, S> {
308    /// Creates an encrypted multi-choice.
309    ///
310    /// For a [`SingleChoice`] polling, it is caller's responsibility to ensure that `choices`
311    /// contains exactly one `true` value; otherwise, the produced proof will not verify.
312    ///
313    /// # Panics
314    ///
315    /// Panics if the length of `choices` differs from the number of options specified in `params`.
316    pub fn new<R: CryptoRng + RngCore>(
317        params: &ChoiceParams<G, S>,
318        choices: &[bool],
319        rng: &mut R,
320    ) -> Self {
321        assert!(!choices.is_empty(), "No choices provided");
322        assert_eq!(
323            choices.len(),
324            params.options_count,
325            "Mismatch between expected and actual number of choices"
326        );
327
328        let admissible_values = [G::identity(), G::generator()];
329        let mut ring_responses = vec![G::Scalar::default(); 2 * params.options_count];
330        let mut transcript = Transcript::new(b"encrypted_choice_ranges");
331        let mut proof_builder = RingProofBuilder::new(
332            &params.receiver,
333            params.options_count,
334            &mut ring_responses,
335            &mut transcript,
336            rng,
337        );
338
339        let sum = choices.iter().map(|&flag| u64::from(flag)).sum::<u64>();
340        let choices: Vec<_> = choices
341            .iter()
342            .map(|&flag| proof_builder.add_value(&admissible_values, usize::from(flag)))
343            .collect();
344        let range_proof = RingProof::new(proof_builder.build(), ring_responses);
345
346        let sum_ciphertext = choices.iter().cloned().reduce(ops::Add::add).unwrap();
347        let sum_ciphertext = sum_ciphertext.with_value(sum);
348        let sum_proof = params
349            .sum_prover
350            .prove(&sum_ciphertext, &params.receiver, rng);
351        Self {
352            choices: choices.into_iter().map(|choice| choice.inner).collect(),
353            range_proof,
354            sum_proof,
355        }
356    }
357
358    /// Verifies the zero-knowledge proofs in this choice and returns Boolean ciphertexts
359    /// for all options.
360    ///
361    /// # Errors
362    ///
363    /// Returns an error if the `choice` is malformed or its proofs fail verification.
364    #[allow(clippy::missing_panics_doc)]
365    pub fn verify(
366        &self,
367        params: &ChoiceParams<G, S>,
368    ) -> Result<&[Ciphertext<G>], ChoiceVerificationError> {
369        params.check_options_count(self.choices.len())?;
370        let sum_of_ciphertexts = self.choices.iter().copied().reduce(ops::Add::add);
371        let sum_of_ciphertexts = sum_of_ciphertexts.unwrap();
372        // ^ `unwrap()` is safe; `params` cannot have 0 options by construction
373        params
374            .sum_prover
375            .verify(&sum_of_ciphertexts, &self.sum_proof, &params.receiver)?;
376
377        let admissible_values = [G::identity(), G::generator()];
378        self.range_proof
379            .verify(
380                &params.receiver,
381                iter::repeat(&admissible_values as &[_]).take(self.choices.len()),
382                self.choices.iter().copied(),
383                &mut Transcript::new(b"encrypted_choice_ranges"),
384            )
385            .map(|()| self.choices.as_slice())
386            .map_err(ChoiceVerificationError::Range)
387    }
388
389    /// Returns the number of encrypted choices. This value is equal to
390    /// [`ChoiceParams::options_count()`] with which the encryption was created.
391    pub fn len(&self) -> usize {
392        self.choices.len()
393    }
394
395    /// Returns ciphertexts for all options **without** checking the validity of this choice.
396    pub fn choices_unchecked(&self) -> &[Ciphertext<G>] {
397        &self.choices
398    }
399
400    /// Returns the range proof for the choice ciphertexts.
401    pub fn range_proof(&self) -> &RingProof<G> {
402        &self.range_proof
403    }
404
405    /// Returns the sum proof for the choice ciphertexts.
406    pub fn sum_proof(&self) -> &S::Proof {
407        &self.sum_proof
408    }
409}
410
411/// Error verifying an [`EncryptedChoice`].
412#[derive(Debug)]
413#[non_exhaustive]
414pub enum ChoiceVerificationError {
415    /// Mismatch between expected and actual number of options in the `EncryptedChoice`.
416    OptionsLenMismatch {
417        /// Expected number of options.
418        expected: usize,
419        /// Actual number of options.
420        actual: usize,
421    },
422    /// Error verifying [`EncryptedChoice::sum_proof()`].
423    Sum(VerificationError),
424    /// Error verifying [`EncryptedChoice::range_proof()`].
425    Range(VerificationError),
426}
427
428impl fmt::Display for ChoiceVerificationError {
429    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
430        match self {
431            Self::OptionsLenMismatch { expected, actual } => write!(
432                formatter,
433                "number of options in the ballot ({actual}) differs from expected ({expected})",
434            ),
435            Self::Sum(err) => write!(formatter, "cannot verify sum proof: {err}"),
436            Self::Range(err) => write!(formatter, "cannot verify range proofs: {err}"),
437        }
438    }
439}
440
441#[cfg(feature = "std")]
442impl std::error::Error for ChoiceVerificationError {
443    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
444        match self {
445            Self::Sum(err) | Self::Range(err) => Some(err),
446            _ => None,
447        }
448    }
449}
450
451#[cfg(test)]
452mod tests {
453    use rand::thread_rng;
454
455    use super::*;
456    use crate::{
457        group::{Generic, Ristretto},
458        Keypair,
459    };
460
461    fn test_bogus_encrypted_choice_does_not_work<G: Group>() {
462        let mut rng = thread_rng();
463        let (receiver, _) = Keypair::<G>::generate(&mut rng).into_tuple();
464        let params = ChoiceParams::single(receiver.clone(), 5);
465
466        let mut choice = EncryptedChoice::single(&params, 2, &mut rng);
467        let (encrypted_one, _) = receiver.encrypt_bool(true, &mut rng);
468        choice.choices[0] = encrypted_one;
469        assert!(choice.verify(&params).is_err());
470
471        let mut choice = EncryptedChoice::single(&params, 4, &mut rng);
472        let (encrypted_zero, _) = receiver.encrypt_bool(false, &mut rng);
473        choice.choices[4] = encrypted_zero;
474        assert!(choice.verify(&params).is_err());
475
476        let mut choice = EncryptedChoice::single(&params, 4, &mut rng);
477        choice.choices[4].blinded_element =
478            choice.choices[4].blinded_element + G::mul_generator(&G::Scalar::from(10));
479        choice.choices[3].blinded_element =
480            choice.choices[3].blinded_element - G::mul_generator(&G::Scalar::from(10));
481        // These modifications leave `choice.sum_proof` correct, but the range proofs
482        // for the last 2 choices should no longer verify.
483        assert!(choice.verify(&params).is_err());
484    }
485
486    #[test]
487    fn bogus_encrypted_choice_does_not_work_for_edwards() {
488        test_bogus_encrypted_choice_does_not_work::<Ristretto>();
489    }
490
491    #[test]
492    fn bogus_encrypted_choice_does_not_work_for_k256() {
493        test_bogus_encrypted_choice_does_not_work::<Generic<k256::Secp256k1>>();
494    }
495}