diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index c68e9013059..71af2719143 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -51,6 +51,7 @@ dbsync dcbor decompressor delegators +dleq dockerhub Dominik dotenv diff --git a/rust/catalyst-voting/Cargo.toml b/rust/catalyst-voting/Cargo.toml index 9044c3ce5e5..e26728cea5f 100644 --- a/rust/catalyst-voting/Cargo.toml +++ b/rust/catalyst-voting/Cargo.toml @@ -13,7 +13,8 @@ workspace = true [dependencies] thiserror = "1.0.56" rand_core = "0.6.4" -curve25519-dalek = { version = "4.0" } +curve25519-dalek = { version = "4.0", features = ["digest"] } +blake2b_simd = "1.0.2" [dev-dependencies] proptest = {version = "1.5.0" } diff --git a/rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs b/rust/catalyst-voting/src/crypto/babystep_giantstep.rs similarity index 99% rename from rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs rename to rust/catalyst-voting/src/crypto/babystep_giantstep.rs index 35bc38ada09..891b2695a4d 100644 --- a/rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs +++ b/rust/catalyst-voting/src/crypto/babystep_giantstep.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; -use super::{GroupElement, Scalar}; +use super::group::{GroupElement, Scalar}; /// Default balance value. /// Make steps asymmetric, in order to better use caching of baby steps. diff --git a/rust/catalyst-voting/src/crypto/elgamal.rs b/rust/catalyst-voting/src/crypto/elgamal.rs index c33ccfc2db7..1fbb5c057ac 100644 --- a/rust/catalyst-voting/src/crypto/elgamal.rs +++ b/rust/catalyst-voting/src/crypto/elgamal.rs @@ -1,7 +1,7 @@ //! Implementation of the lifted ``ElGamal`` crypto system, and combine with `ChaCha` //! stream cipher to produce a hybrid encryption scheme. -use std::ops::{Add, Mul}; +use std::ops::{Add, Deref, Mul}; use rand_core::CryptoRngCore; @@ -19,6 +19,22 @@ pub struct PublicKey(GroupElement); #[derive(Debug, Clone, PartialEq, Eq)] pub struct Ciphertext(GroupElement, GroupElement); +impl Deref for SecretKey { + type Target = Scalar; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Deref for PublicKey { + type Target = GroupElement; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl SecretKey { /// Generate a random `SecretKey` value from the random number generator. pub fn generate(rng: &mut R) -> Self { @@ -35,15 +51,25 @@ impl SecretKey { impl Ciphertext { /// Generate a zero `Ciphertext`. /// The same as encrypt a `Scalar::zero()` message and `Scalar::zero()` randomness. - pub(crate) fn zero() -> Self { + pub fn zero() -> Self { Ciphertext(GroupElement::zero(), GroupElement::zero()) } + + /// Get the first element of the `Ciphertext`. + pub fn first(&self) -> &GroupElement { + &self.0 + } + + /// Get the second element of the `Ciphertext`. + pub fn second(&self) -> &GroupElement { + &self.1 + } } /// Given a `message` represented as a `Scalar`, return a ciphertext using the /// lifted ``ElGamal`` mechanism. /// Returns a ciphertext of type `Ciphertext`. -pub(crate) fn encrypt(message: &Scalar, public_key: &PublicKey, randomness: &Scalar) -> Ciphertext { +pub fn encrypt(message: &Scalar, public_key: &PublicKey, randomness: &Scalar) -> Ciphertext { let e1 = GroupElement::GENERATOR.mul(randomness); let e2 = &GroupElement::GENERATOR.mul(message) + &public_key.0.mul(randomness); Ciphertext(e1, e2) @@ -51,7 +77,7 @@ pub(crate) fn encrypt(message: &Scalar, public_key: &PublicKey, randomness: &Sca /// Decrypt ``ElGamal`` `Ciphertext`, returns the original message represented as a /// `GroupElement`. -pub(crate) fn decrypt(cipher: &Ciphertext, secret_key: &SecretKey) -> GroupElement { +pub fn decrypt(cipher: &Ciphertext, secret_key: &SecretKey) -> GroupElement { &(&cipher.0 * &secret_key.0.negate()) + &cipher.1 } diff --git a/rust/catalyst-voting/src/crypto/group/mod.rs b/rust/catalyst-voting/src/crypto/group/mod.rs index afea38ca878..d76e3329032 100644 --- a/rust/catalyst-voting/src/crypto/group/mod.rs +++ b/rust/catalyst-voting/src/crypto/group/mod.rs @@ -1,8 +1,6 @@ //! Group definitions used in voting protocol. //! For more information, see: -mod babystep_giantstep; mod ristretto255; -pub(crate) use babystep_giantstep::BabyStepGiantStep; pub(crate) use ristretto255::{GroupElement, Scalar}; diff --git a/rust/catalyst-voting/src/crypto/group/ristretto255.rs b/rust/catalyst-voting/src/crypto/group/ristretto255.rs index 3f2a718dba6..092bec59a6f 100644 --- a/rust/catalyst-voting/src/crypto/group/ristretto255.rs +++ b/rust/catalyst-voting/src/crypto/group/ristretto255.rs @@ -9,7 +9,8 @@ use std::{ use curve25519_dalek::{ constants::{RISTRETTO_BASEPOINT_POINT, RISTRETTO_BASEPOINT_TABLE}, - ristretto::RistrettoPoint as Point, + digest::{consts::U64, Digest}, + ristretto::{CompressedRistretto, RistrettoPoint as Point}, scalar::Scalar as IScalar, traits::Identity, }; @@ -67,6 +68,22 @@ impl Scalar { pub fn inverse(&self) -> Scalar { Scalar(self.0.invert()) } + + /// Convert this `Scalar` to its underlying sequence of bytes. + pub fn to_bytes(&self) -> [u8; 32] { + self.0.to_bytes() + } + + /// Attempt to construct a `Scalar` from a canonical byte representation. + pub fn from_bytes(bytes: [u8; 32]) -> Option { + IScalar::from_canonical_bytes(bytes).map(Scalar).into() + } + + /// Generate a `Scalar` from a hash digest. + pub fn from_hash(hash: D) -> Scalar + where D: Digest { + Scalar(IScalar::from_hash(hash)) + } } impl GroupElement { @@ -77,6 +94,19 @@ impl GroupElement { pub fn zero() -> Self { GroupElement(Point::identity()) } + + /// Convert this `GroupElement` to its underlying sequence of bytes. + /// Always encode the compressed value. + pub fn to_bytes(&self) -> [u8; 32] { + self.0.compress().to_bytes() + } + + /// Attempt to construct a `Scalar` from a compressed value byte representation. + pub fn from_bytes(bytes: &[u8; 32]) -> Option { + Some(GroupElement( + CompressedRistretto::from_slice(bytes).ok()?.decompress()?, + )) + } } // `std::ops` traits implementations @@ -133,6 +163,14 @@ impl Sub<&Scalar> for &Scalar { } } +impl Sub<&GroupElement> for &GroupElement { + type Output = GroupElement; + + fn sub(self, other: &GroupElement) -> GroupElement { + GroupElement(self.0 + (-other.0)) + } +} + #[cfg(test)] mod tests { use proptest::{ @@ -152,6 +190,21 @@ mod tests { } } + #[proptest] + fn scalar_to_bytes_from_bytes_test(e1: Scalar) { + let bytes = e1.to_bytes(); + let e2 = Scalar::from_bytes(bytes).unwrap(); + assert_eq!(e1, e2); + } + + #[proptest] + fn group_element_to_bytes_from_bytes_test(e: Scalar) { + let ge1 = GroupElement::GENERATOR.mul(&e); + let bytes = ge1.to_bytes(); + let ge2 = GroupElement::from_bytes(&bytes).unwrap(); + assert_eq!(ge1, ge2); + } + #[proptest] fn scalar_arithmetic_tests(e1: Scalar, e2: Scalar, e3: Scalar) { assert_eq!(&(&e1 + &e2) + &e3, &e1 + &(&e2 + &e3)); @@ -174,6 +227,7 @@ mod tests { let ge3 = GroupElement::GENERATOR.mul(&(&e1 + &e2)); assert_eq!(&ge1 + &ge2, ge3); + assert_eq!(&(&ge1 + &ge2) - &ge2, ge1); let ge = GroupElement::GENERATOR.mul(&e1).mul(&e1.inverse()); assert_eq!(ge, GroupElement::GENERATOR); diff --git a/rust/catalyst-voting/src/crypto/hash.rs b/rust/catalyst-voting/src/crypto/hash.rs new file mode 100644 index 00000000000..f767f0afbd4 --- /dev/null +++ b/rust/catalyst-voting/src/crypto/hash.rs @@ -0,0 +1,50 @@ +//! Blake2b-256 hash implementation. + +use curve25519_dalek::digest::{ + consts::U64, typenum::Unsigned, FixedOutput, HashMarker, Output, OutputSizeUser, Update, +}; + +/// Blake2b-512 hasher instance. +pub struct Blake2b512Hasher(blake2b_simd::State); + +impl Blake2b512Hasher { + /// Create a new `Blake2b256Hasher`. + pub fn new() -> Self { + Self( + blake2b_simd::Params::new() + .hash_length(Self::output_size()) + .to_state(), + ) + } +} + +// Implementation of the `digest::Digest` trait for `Blake2b256Hasher`. + +impl Default for Blake2b512Hasher { + fn default() -> Self { + Self::new() + } +} + +impl Update for Blake2b512Hasher { + fn update(&mut self, data: &[u8]) { + self.0.update(data); + } +} + +impl OutputSizeUser for Blake2b512Hasher { + type OutputSize = U64; + + fn output_size() -> usize { + Self::OutputSize::USIZE + } +} + +impl FixedOutput for Blake2b512Hasher { + fn finalize_into(self, out: &mut Output) { + let hash = self.0.finalize(); + out.copy_from_slice(hash.as_bytes()); + } +} + +impl HashMarker for Blake2b512Hasher {} diff --git a/rust/catalyst-voting/src/crypto/mod.rs b/rust/catalyst-voting/src/crypto/mod.rs index 16822f6efed..69d41e87d64 100644 --- a/rust/catalyst-voting/src/crypto/mod.rs +++ b/rust/catalyst-voting/src/crypto/mod.rs @@ -1,4 +1,7 @@ //! Crypto primitives which are used by voting protocol. +pub(crate) mod babystep_giantstep; pub(crate) mod elgamal; pub(crate) mod group; +pub(crate) mod hash; +pub(crate) mod zk_dl_equality; diff --git a/rust/catalyst-voting/src/crypto/zk_dl_equality.rs b/rust/catalyst-voting/src/crypto/zk_dl_equality.rs new file mode 100644 index 00000000000..a77cd3e4b32 --- /dev/null +++ b/rust/catalyst-voting/src/crypto/zk_dl_equality.rs @@ -0,0 +1,111 @@ +//! Non-interactive Zero Knowledge proof of Discrete Logarithm +//! Equality (DLEQ). +//! +//! The proof is the following: +//! +//! `NIZK{(base_1, base_2, point_1, point_2), (dlog): point_1 = base_1^dlog AND point_2 = +//! base_2^dlog}` +//! +//! which makes the statement, the two bases `base_1` and `base_2`, and the two +//! points `point_1` and `point_2`. The witness, on the other hand +//! is the discrete logarithm, `dlog`. + +// cspell: words NIZK dlog + +use curve25519_dalek::digest::Update; + +use super::{ + group::{GroupElement, Scalar}, + hash::Blake2b512Hasher, +}; + +/// DLEQ proof struct +pub struct DleqProof(Scalar, Scalar); + +/// Generates a DLEQ proof. +pub fn generate_dleq_proof( + base_1: &GroupElement, base_2: &GroupElement, point_1: &GroupElement, point_2: &GroupElement, + dlog: &Scalar, randomness: &Scalar, +) -> DleqProof { + let a_1 = base_1 * randomness; + let a_2 = base_2 * randomness; + + let challenge = calculate_challenge(base_1, base_2, point_1, point_2, &a_1, &a_2); + let response = &(dlog * &challenge) + randomness; + + DleqProof(challenge, response) +} + +/// Verify a DLEQ proof. +pub fn verify_dleq_proof( + proof: &DleqProof, base_1: &GroupElement, base_2: &GroupElement, point_1: &GroupElement, + point_2: &GroupElement, +) -> bool { + let a_1 = &(base_1 * &proof.1) - &(point_1 * &proof.0); + let a_2 = &(base_2 * &proof.1) - &(point_2 * &proof.0); + + let challenge = calculate_challenge(base_1, base_2, point_1, point_2, &a_1, &a_2); + challenge == proof.0 +} + +/// Calculates the challenge value. +/// Its a hash value represented as `Scalar` of all provided elements. +fn calculate_challenge( + base_1: &GroupElement, base_2: &GroupElement, point_1: &GroupElement, point_2: &GroupElement, + a_1: &GroupElement, a_2: &GroupElement, +) -> Scalar { + let blake2b_hasher = Blake2b512Hasher::new() + .chain(base_1.to_bytes()) + .chain(base_2.to_bytes()) + .chain(point_1.to_bytes()) + .chain(point_2.to_bytes()) + .chain(a_1.to_bytes()) + .chain(a_2.to_bytes()); + + Scalar::from_hash(blake2b_hasher) +} + +#[cfg(test)] +mod tests { + use std::ops::Mul; + + use test_strategy::proptest; + + use super::*; + + #[proptest] + fn zk_dleq_test(e1: Scalar, e2: Scalar, dlog1: Scalar, dlog2: Scalar, randomness: Scalar) { + let base_1 = GroupElement::GENERATOR.mul(&e1); + let base_2 = GroupElement::GENERATOR.mul(&e2); + + let point_1 = base_1.mul(&dlog1); + let point_2 = base_2.mul(&dlog1); + + let proof = generate_dleq_proof(&base_1, &base_2, &point_1, &point_2, &dlog1, &randomness); + assert!(verify_dleq_proof( + &proof, &base_1, &base_2, &point_1, &point_2 + )); + + // use different discrete logarithm for both points + let point_1 = base_1.mul(&dlog2); + let point_2 = base_2.mul(&dlog2); + + let proof = generate_dleq_proof(&base_1, &base_2, &point_1, &point_2, &dlog1, &randomness); + assert!(!verify_dleq_proof( + &proof, &base_1, &base_2, &point_1, &point_2 + )); + + // use different discrete logarithm across points + let point_1 = base_1.mul(&dlog1); + let point_2 = base_2.mul(&dlog2); + + let proof = generate_dleq_proof(&base_1, &base_2, &point_1, &point_2, &dlog1, &randomness); + assert!(!verify_dleq_proof( + &proof, &base_1, &base_2, &point_1, &point_2 + )); + let proof = generate_dleq_proof(&base_1, &base_2, &point_1, &point_2, &dlog2, &randomness); + assert!(!verify_dleq_proof( + &proof, &base_1, &base_2, &point_1, &point_2 + )); + } +} diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index ef3e7456cde..0387354ba97 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -2,8 +2,8 @@ //! //! ```rust //! use catalyst_voting::{ -//! decrypt_tally, encrypt_vote, tally, DecryptionTallySetup, EncryptionRandomness, SecretKey, -//! Vote, +//! decrypt_tally, encrypt_vote, generate_tally_proof, tally, verify_tally_proof, +//! DecryptionTallySetup, EncryptionRandomness, SecretKey, Vote, //! }; //! //! struct Voter { @@ -58,6 +58,11 @@ //! }) //! .collect(); //! +//! let tally_proofs: Vec<_> = encrypted_tallies +//! .iter() +//! .map(|t| generate_tally_proof(t, &election_secret_key, &mut rng)) +//! .collect(); +//! //! let decryption_tally_setup = DecryptionTallySetup::new( //! voter_1.voting_power + voter_2.voting_power + voter_3.voting_power, //! ) @@ -67,6 +72,13 @@ //! .map(|t| decrypt_tally(t, &election_secret_key, &decryption_tally_setup).unwrap()) //! .collect(); //! +//! let is_ok = tally_proofs +//! .iter() +//! .zip(encrypted_tallies.iter()) +//! .zip(decrypted_tallies.iter()) +//! .all(|((p, enc_t), t)| verify_tally_proof(enc_t, *t, &election_public_key, p)); +//! assert!(is_ok); +//! //! assert_eq!(decrypted_tallies, vec![ //! voter_1.voting_power, //! voter_2.voting_power, @@ -75,9 +87,9 @@ //! ``` mod crypto; -pub mod tally; -pub mod voter; +mod tally; +mod voter; pub use crypto::elgamal::{PublicKey, SecretKey}; -pub use tally::{decrypt_tally, tally, DecryptionTallySetup}; -pub use voter::{encrypt_vote, EncryptionRandomness, Vote}; +pub use tally::{proof::*, *}; +pub use voter::*; diff --git a/rust/catalyst-voting/src/tally.rs b/rust/catalyst-voting/src/tally/mod.rs similarity index 97% rename from rust/catalyst-voting/src/tally.rs rename to rust/catalyst-voting/src/tally/mod.rs index a6ef55a02b7..81cd10990ea 100644 --- a/rust/catalyst-voting/src/tally.rs +++ b/rust/catalyst-voting/src/tally/mod.rs @@ -1,11 +1,14 @@ //! Module containing all primitives related to the tally process. +pub mod proof; + use std::ops::{Add, Mul}; use crate::{ crypto::{ + babystep_giantstep::BabyStepGiantStep, elgamal::{decrypt, Ciphertext, SecretKey}, - group::{BabyStepGiantStep, Scalar}, + group::Scalar, }, voter::EncryptedVote, }; diff --git a/rust/catalyst-voting/src/tally/proof.rs b/rust/catalyst-voting/src/tally/proof.rs new file mode 100644 index 00000000000..8417bf9fa70 --- /dev/null +++ b/rust/catalyst-voting/src/tally/proof.rs @@ -0,0 +1,56 @@ +//! Tally proof generation and verification procedures. +//! It allows to transparently verify the correctness of decryption tally procedure. + +use std::ops::Mul; + +use rand_core::CryptoRngCore; + +use super::EncryptedTally; +use crate::{ + crypto::{ + group::{GroupElement, Scalar}, + zk_dl_equality::{generate_dleq_proof, verify_dleq_proof, DleqProof}, + }, + PublicKey, SecretKey, +}; + +/// Tally proof struct. +#[allow(clippy::module_name_repetitions)] +pub struct TallyProof(DleqProof); + +/// Generates a tally proof. +/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#tally-proof) +#[allow(clippy::module_name_repetitions)] +pub fn generate_tally_proof( + encrypted_tally: &EncryptedTally, secret_key: &SecretKey, rng: &mut R, +) -> TallyProof { + let randomness = Scalar::random(rng); + let e1 = encrypted_tally.0.first(); + let d = e1.mul(secret_key); + + let proof = generate_dleq_proof( + &GroupElement::GENERATOR, + e1, + &secret_key.public_key(), + &d, + secret_key, + &randomness, + ); + + TallyProof(proof) +} + +/// Verifies a tally proof. +/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#tally-proof) +#[must_use] +#[allow(clippy::module_name_repetitions)] +pub fn verify_tally_proof( + encrypted_tally: &EncryptedTally, tally: u64, public_key: &PublicKey, proof: &TallyProof, +) -> bool { + let tally = Scalar::from(tally); + let e1 = encrypted_tally.0.first(); + let e2 = encrypted_tally.0.second(); + let d = e2 - &GroupElement::GENERATOR.mul(&tally); + + verify_dleq_proof(&proof.0, &GroupElement::GENERATOR, e1, public_key, &d) +} diff --git a/rust/catalyst-voting/tests/voting_test.rs b/rust/catalyst-voting/tests/voting_test.rs index d68ab42e43b..a17aa81cdcf 100644 --- a/rust/catalyst-voting/tests/voting_test.rs +++ b/rust/catalyst-voting/tests/voting_test.rs @@ -1,7 +1,8 @@ //! A general voting integration test, which performs a full voting procedure. use catalyst_voting::{ - decrypt_tally, encrypt_vote, tally, DecryptionTallySetup, EncryptionRandomness, SecretKey, Vote, + decrypt_tally, encrypt_vote, generate_tally_proof, tally, verify_tally_proof, + DecryptionTallySetup, EncryptionRandomness, SecretKey, Vote, }; use proptest::prelude::ProptestConfig; use test_strategy::{proptest, Arbitrary}; @@ -50,11 +51,23 @@ fn voting_test(voters: [Voter; 100]) { let total_voting_power = voting_powers.iter().sum(); let decryption_tally_setup = DecryptionTallySetup::new(total_voting_power).unwrap(); + let tally_proofs: Vec<_> = encrypted_tallies + .iter() + .map(|t| generate_tally_proof(t, &election_secret_key, &mut rng)) + .collect(); + let decrypted_tallies: Vec<_> = encrypted_tallies .iter() .map(|t| decrypt_tally(t, &election_secret_key, &decryption_tally_setup).unwrap()) .collect(); + let is_ok = tally_proofs + .iter() + .zip(encrypted_tallies.iter()) + .zip(decrypted_tallies.iter()) + .all(|((p, enc_t), t)| verify_tally_proof(enc_t, *t, &election_public_key, p)); + assert!(is_ok); + let expected_tallies: Vec<_> = (0..VOTING_OPTIONS) .map(|i| { voters