From ce58bd7cd75bfb693c66ac00b131b1d41b420f95 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Thu, 19 Dec 2024 20:53:11 -0800 Subject: [PATCH] Add the new version of dec To replace the old dec when we are converting Presigning --- PAPER.md | 2 + synedrion/src/cggmp21/sigma.rs | 1 + synedrion/src/cggmp21/sigma/dec_new.rs | 294 +++++++++++++++++++++++++ synedrion/src/tools/bitvec.rs | 81 ++++++- synedrion/src/uint/public_signed.rs | 14 ++ 5 files changed, 384 insertions(+), 8 deletions(-) create mode 100644 synedrion/src/cggmp21/sigma/dec_new.rs diff --git a/PAPER.md b/PAPER.md index 7f3544d2..b19788a9 100644 --- a/PAPER.md +++ b/PAPER.md @@ -16,6 +16,8 @@ Replies from Nikos Makriyannis are marked as (NM) # Typos +In `П_dec` (Fig. 28), Inputs, the condition `(1 + N)^z` should read `(1 + N)^y`. Also note that at the point where it is used, the first secret argument corresponds to `y`, and the second to `x`. + The randomness derivation (`\mu` in Output, step 1, in Fig. 6) is overly complicated - `\mu = (C mod N)^(N^(-1) mod phi(N))` works just as well. Also, `(1 + N)^m mod N^2 == (1 + m * N) mod N^2`, which is much faster to compute, but I guess the exponential form is conventional. In `П^{fac}` (Fig. 28), step 2: `q` the curve order, not to be confused with `q` the RSA prime in the Inputs. diff --git a/synedrion/src/cggmp21/sigma.rs b/synedrion/src/cggmp21/sigma.rs index cf89093c..21c58ba3 100644 --- a/synedrion/src/cggmp21/sigma.rs +++ b/synedrion/src/cggmp21/sigma.rs @@ -2,6 +2,7 @@ mod aff_g; mod dec; +mod dec_new; mod enc; mod fac; mod log_star; diff --git a/synedrion/src/cggmp21/sigma/dec_new.rs b/synedrion/src/cggmp21/sigma/dec_new.rs new file mode 100644 index 00000000..58e84bcf --- /dev/null +++ b/synedrion/src/cggmp21/sigma/dec_new.rs @@ -0,0 +1,294 @@ +//! Paillier Special Decryption in the Exponent ($\Pi^{dec}$, Section A.6, Fig. 28) + +#![allow(dead_code)] + +use alloc::{boxed::Box, vec::Vec}; + +use rand_core::CryptoRngCore; +use serde::{Deserialize, Serialize}; + +use super::super::{ + conversion::{scalar_from_signed, secret_scalar_from_signed}, + SchemeParams, +}; +use crate::{ + curve::Point, + paillier::{Ciphertext, CiphertextWire, MaskedRandomizer, PaillierParams, PublicKeyPaillier, RPParams, Randomizer}, + tools::{ + bitvec::BitVec, + hashing::{Chain, Hashable, XofHasher}, + }, + uint::{PublicSigned, SecretSigned}, +}; + +const HASH_TAG: &[u8] = b"P_dec"; + +pub(crate) struct DecSecretInputs<'a, P: SchemeParams> { + /// $x \in \mathbb{I}$, that is $x \in \pm 2^\ell$ (see N.B. just before Section 4.1) + pub x: &'a SecretSigned<::Uint>, + /// $y \in \mathbb{I}$, that is $x \in \pm 2^{\ell^\prime}$ (see N.B. just before Section 4.1) + pub y: &'a SecretSigned<::Uint>, + /// $\rho$, a Paillier randomizer for the public key $N_0$. + pub rho: &'a Randomizer, +} + +pub(crate) struct DecPublicInputs<'a, P: SchemeParams> { + /// Paillier public key $N_0$. + pub pk0: &'a PublicKeyPaillier, + /// Paillier ciphertext $K$ such that $enc_0(y, \rho) = K (*) x + D$. + // NOTE: paper says `enc_0(z, \rho)` which is a typo. + pub cap_k: &'a Ciphertext, + /// Point $X = g * x$, where $g$ is the curve generator. + pub cap_x: &'a Point, + /// Paillier ciphertext $D$, see the doc for `cap_k` above. + pub cap_d: &'a Ciphertext, + /// Point $S = g * y$, where $g$ is the curve generator. + pub cap_s: &'a Point, +} + +/// ZK proof: Paillier decryption modulo $q$. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct DecProof { + e: BitVec, + commitments: Box<[DecProofCommitment

]>, + elements: Box<[DecProofElement

]>, +} + +struct DecProofEphemeral { + alpha: SecretSigned<::Uint>, + beta: SecretSigned<::Uint>, + r: Randomizer, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct DecProofCommitment { + cap_a: CiphertextWire, + cap_b: Point, + cap_c: Point, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct DecProofElement { + z: PublicSigned<::Uint>, + omega: PublicSigned<::Uint>, + nu: MaskedRandomizer, +} + +impl DecProof

{ + pub fn new( + rng: &mut impl CryptoRngCore, + secret: DecSecretInputs<'_, P>, + public: DecPublicInputs<'_, P>, + setup: &RPParams, + aux: &impl Hashable, + ) -> Self { + secret.x.assert_exponent_range(P::L_BOUND); + secret.y.assert_exponent_range(P::LP_BOUND); + assert_eq!(public.cap_k.public_key(), public.pk0); + assert_eq!(public.cap_d.public_key(), public.pk0); + + let (ephemerals, commitments): (Vec<_>, Vec<_>) = (0..P::SECURITY_PARAMETER) + .map(|_| { + let alpha = SecretSigned::random_in_exponent_range(rng, P::L_BOUND + P::EPS_BOUND); + let beta = SecretSigned::random_in_exponent_range(rng, P::LP_BOUND + P::EPS_BOUND); + let r = Randomizer::random(rng, public.pk0); + + let cap_a = + (public.cap_k * &-&alpha + Ciphertext::new_with_randomizer(&public.pk0, &beta, &r)).to_wire(); + let cap_b = secret_scalar_from_signed::

(&beta).mul_by_generator(); + let cap_c = secret_scalar_from_signed::

(&alpha).mul_by_generator(); + + let ephemeral = DecProofEphemeral::

{ alpha, beta, r }; + let commitment = DecProofCommitment { cap_a, cap_b, cap_c }; + + (ephemeral, commitment) + }) + .unzip(); + + let mut reader = XofHasher::new_with_dst(HASH_TAG) + // commitments + .chain(&commitments) + // public parameters + .chain(public.pk0.as_wire()) + .chain(&public.cap_k.to_wire()) + .chain(&public.cap_x) + .chain(&public.cap_d.to_wire()) + .chain(&public.cap_s) + .chain(&setup.to_wire()) + .chain(aux) + .finalize_to_reader(); + + // Non-interactive challenge + let e = BitVec::from_xof_reader(&mut reader, P::SECURITY_PARAMETER); + + let elements = ephemerals + .into_iter() + .zip(e.bits()) + .map(|(ephemeral, e_bit)| { + let mut z = ephemeral.alpha; + if *e_bit { + z = z + secret.x; + } + + let mut omega = ephemeral.beta; + if *e_bit { + omega = omega + secret.y; + } + + let nu = if *e_bit { + secret.rho.to_masked(&ephemeral.r, &PublicSigned::one()) + } else { + secret.rho.to_masked(&ephemeral.r, &PublicSigned::zero()) + }; + + DecProofElement { + z: z.to_public(), + omega: omega.to_public(), + nu, + } + }) + .collect::>(); + + Self { + e, + elements: elements.into(), + commitments: commitments.into(), + } + } + + pub fn verify(&self, public: DecPublicInputs<'_, P>, setup: &RPParams, aux: &impl Hashable) -> bool { + let mut reader = XofHasher::new_with_dst(HASH_TAG) + // commitments + .chain(&self.commitments) + // public parameters + .chain(public.pk0.as_wire()) + .chain(&public.cap_k.to_wire()) + .chain(&public.cap_x) + .chain(&public.cap_d.to_wire()) + .chain(&public.cap_s) + .chain(&setup.to_wire()) + .chain(aux) + .finalize_to_reader(); + + // Non-interactive challenge + let e = BitVec::from_xof_reader(&mut reader, P::SECURITY_PARAMETER); + + if e != self.e { + return false; + } + + if e.bits().len() != self.commitments.len() || e.bits().len() != self.elements.len() { + return false; + } + + for ((e_bit, commitment), element) in e + .bits() + .iter() + .cloned() + .zip(self.commitments.iter()) + .zip(self.elements.iter()) + { + // enc(\omega_j, \nu_j) (+) K (*) (-z_j) == A_j (+) D (*) e_j + let cap_a = commitment.cap_a.to_precomputed(public.pk0); + let lhs = Ciphertext::new_public_with_randomizer(public.pk0, &element.omega, &element.nu) + + public.cap_k * &-element.z; + let rhs = if e_bit { cap_a + public.cap_d } else { cap_a }; + if lhs != rhs { + return false; + } + + // g * z_j == C_j + X * e_j + let lhs = scalar_from_signed::

(&element.z).mul_by_generator(); + let rhs = if e_bit { + commitment.cap_c + *public.cap_x + } else { + commitment.cap_c + }; + if lhs != rhs { + return false; + } + + // g * \omega_j == B_j + S * e_j + let lhs = scalar_from_signed::

(&element.omega).mul_by_generator(); + let rhs = if e_bit { + commitment.cap_b + *public.cap_s + } else { + commitment.cap_b + }; + if lhs != rhs { + return false; + } + } + + true + } +} + +#[cfg(test)] +mod tests { + use rand_core::OsRng; + + use super::{DecProof, DecPublicInputs, DecSecretInputs}; + use crate::{ + cggmp21::{conversion::secret_scalar_from_signed, SchemeParams, TestParams}, + paillier::{Ciphertext, PaillierParams, RPParams, Randomizer, SecretKeyPaillierWire}, + uint::SecretSigned, + }; + + #[test] + fn prove_and_verify() { + type Params = TestParams; + type Paillier = ::Paillier; + + let sk = SecretKeyPaillierWire::::random(&mut OsRng).into_precomputed(); + let pk = sk.public_key(); + + let setup = RPParams::random(&mut OsRng); + + let aux: &[u8] = b"abcde"; + + let x = SecretSigned::random_in_exponent_range(&mut OsRng, Params::L_BOUND); + let y = SecretSigned::random_in_exponent_range(&mut OsRng, Params::LP_BOUND); + let rho = Randomizer::random(&mut OsRng, pk); + + // We need $enc_0(y, \rho) = K (*) x + D$, + // so we can choose the plaintext of `K` at random, and derive the plaintext of `D` + // (not deriving `y` since we want it to be in a specific range). + + let k = SecretSigned::random_in_exponent_range(&mut OsRng, Paillier::PRIME_BITS * 2 - 1); + let cap_k = Ciphertext::new(&mut OsRng, pk, &k); + let cap_d = Ciphertext::new_with_randomizer(pk, &y, &rho) + &cap_k * &-&x; + + let cap_x = secret_scalar_from_signed::(&x).mul_by_generator(); + let cap_s = secret_scalar_from_signed::(&y).mul_by_generator(); + + let proof = DecProof::::new( + &mut OsRng, + DecSecretInputs { + x: &x, + y: &y, + rho: &rho, + }, + DecPublicInputs { + pk0: pk, + cap_k: &cap_k, + cap_x: &cap_x, + cap_d: &cap_d, + cap_s: &cap_s, + }, + &setup, + &aux, + ); + assert!(proof.verify( + DecPublicInputs { + pk0: pk, + cap_k: &cap_k, + cap_x: &cap_x, + cap_d: &cap_d, + cap_s: &cap_s, + }, + &setup, + &aux + )); + } +} diff --git a/synedrion/src/tools/bitvec.rs b/synedrion/src/tools/bitvec.rs index f5717bea..53711016 100644 --- a/synedrion/src/tools/bitvec.rs +++ b/synedrion/src/tools/bitvec.rs @@ -1,19 +1,84 @@ -use alloc::{boxed::Box, vec}; +use alloc::{boxed::Box, string::String, vec, vec::Vec}; use core::ops::BitXorAssign; +use digest::XofReader; use rand_core::CryptoRngCore; use serde::{Deserialize, Serialize}; use serde_encoded_bytes::{Base64, SliceLike}; -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct BitVec(#[serde(with = "SliceLike::")] Box<[u8]>); +#[derive(Serialize, Deserialize)] +struct PackedBitVec { + bits: u32, + #[serde(with = "SliceLike::")] + byte_vec: Box<[u8]>, +} + +impl TryFrom for BitVec { + type Error = String; + fn try_from(source: PackedBitVec) -> Result { + if source.bits.div_ceil(8) as usize > source.byte_vec.len() { + return Err("The declared number of bits is greater than the size of the byte string".into()); + } + let bits = source + .bits + .try_into() + .map_err(|_| "The number of bits does not fit into `usize`")?; + Ok(BitVec::from_bytes_unchecked(bits, &source.byte_vec)) + } +} + +impl From for PackedBitVec { + fn from(source: BitVec) -> Self { + let bytes = source.0.len().div_ceil(8); + let mut byte_vec = vec![0u8; bytes]; + + for (i, bit) in source.0.iter().enumerate() { + let byte_position = i / 8; + let bit_mask = 1 << (7 - (i % 8)); + if *bit { + byte_vec[byte_position] |= bit_mask; + } + } + + Self { + bits: source.0.len() as u32, + byte_vec: byte_vec.into(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(try_from = "PackedBitVec", into = "PackedBitVec")] +pub(crate) struct BitVec(Box<[bool]>); impl BitVec { - pub fn random(rng: &mut impl CryptoRngCore, min_bits: usize) -> Self { - let len = (min_bits - 1) / 8 + 1; // minimum number of bytes containing `min_bits` bits. - let mut bytes = vec![0; len]; - rng.fill_bytes(&mut bytes); - Self(bytes.into()) + fn from_bytes_unchecked(bits: usize, byte_vec: &[u8]) -> Self { + debug_assert!(bits.div_ceil(8) <= byte_vec.len()); + let mut bit_vec = Vec::with_capacity(bits); + for i in 0..bits { + let byte_position = i / 8; + let bit_mask = 1 << (7 - (i % 8)); + bit_vec.push(byte_vec[byte_position] & bit_mask != 0); + } + Self(bit_vec.into()) + } + + pub fn random(rng: &mut impl CryptoRngCore, bits: usize) -> Self { + let bytes = bits.div_ceil(8); + let mut byte_vec = vec![0; bytes]; + rng.fill_bytes(&mut byte_vec); + Self::from_bytes_unchecked(bits, &byte_vec) + } + + pub fn bits(&self) -> &[bool] { + &self.0 + } + + pub fn from_xof_reader(reader: &mut impl XofReader, bits: usize) -> Self { + let bytes = bits.div_ceil(8); + let mut byte_vec = vec![0u8; bytes]; + reader.read(&mut byte_vec); + Self::from_bytes_unchecked(bits, &byte_vec) } } diff --git a/synedrion/src/uint/public_signed.rs b/synedrion/src/uint/public_signed.rs index b56c33ae..80ed5ed3 100644 --- a/synedrion/src/uint/public_signed.rs +++ b/synedrion/src/uint/public_signed.rs @@ -117,6 +117,20 @@ where Some(result) } + pub fn one() -> Self { + Self { + value: T::one(), + bound: 1, + } + } + + pub fn zero() -> Self { + Self { + value: T::zero(), + bound: 0, + } + } + pub fn is_negative(&self) -> bool { self.value.bit_vartime(T::BITS - 1) }