elastic_elgamal/proofs/
log_equality.rs

1//! [`LogEqualityProof`] and related logic.
2
3use merlin::Transcript;
4use rand_core::{CryptoRng, RngCore};
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7
8#[cfg(feature = "serde")]
9use crate::serde::ScalarHelper;
10use crate::{
11    alloc::{vec, Vec},
12    group::Group,
13    proofs::{TranscriptForGroup, VerificationError},
14    PublicKey, SecretKey,
15};
16
17/// Zero-knowledge proof of equality of two discrete logarithms in different bases,
18/// aka Chaum–Pedersen protocol.
19///
20/// # Construction
21///
22/// This proof is a result of the [Fiat–Shamir transform][fst] applied to a standard
23/// ZKP of equality of the two discrete logs in different bases.
24///
25/// - Public parameters of the proof are the two bases `G` and `K` in a prime-order group
26///   in which discrete log problem is believed to be hard.
27/// - Prover and verifier both know group elements `R` and `B`, which presumably have
28///   the same discrete log in bases `G` and `K` respectively.
29/// - Prover additionally knows the discrete log in question: `r = dlog_G(R) = dlog_K(B)`.
30///
31/// The interactive proof is specified as a sigma protocol (see, e.g., [this course])
32/// as follows:
33///
34/// 1. **Commitment:** The prover generates random scalar `x`. The prover sends to the verifier
35///    `X_G = [x]G` and `X_K = [x]K`.
36/// 2. **Challenge:** The verifier sends to the prover random scalar `c`.
37/// 3. **Response:** The prover computes scalar `s = x + cr` and sends it to the verifier.
38///
39/// Verification equations are:
40///
41/// ```text
42/// [s]G ?= X_G + [c]R;
43/// [s]K ?= X_K + [c]B.
44/// ```
45///
46/// In the non-interactive version of the proof, challenge `c` is derived from `hash(M)`,
47/// where `hash()` is a cryptographically secure hash function, and `M` is an optional message
48/// verified together with the proof (cf. public-key digital signatures). If `M` is set, we
49/// use a proof as a *signature of knowledge*. This allows to tie the proof to the context,
50/// so it cannot be (re)used in other contexts.
51///
52/// To reduce the size of the proof, we use the trick underpinning ring signature constructions.
53/// Namely, we represent the proof as `(c, s)`; during verification, we restore `X_G`, `X_K`
54/// from the original verification equations above.
55///
56/// # Implementation details
57///
58/// - The proof is serialized as 2 scalars: `(c, s)`.
59/// - Proof generation is constant-time. Verification is **not** constant-time.
60/// - Challenge `c` is derived using [`Transcript`] API.
61///
62/// # Examples
63///
64/// ```
65/// # use elastic_elgamal::{group::Ristretto, Keypair, SecretKey, LogEqualityProof};
66/// # use merlin::Transcript;
67/// # use rand::thread_rng;
68/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
69/// let mut rng = thread_rng();
70/// let (log_base, _) =
71///     Keypair::<Ristretto>::generate(&mut rng).into_tuple();
72/// let (power_g, discrete_log) =
73///     Keypair::<Ristretto>::generate(&mut rng).into_tuple();
74/// let power_k = log_base.as_element() * discrete_log.expose_scalar();
75///
76/// let proof = LogEqualityProof::new(
77///     &log_base,
78///     &discrete_log,
79///     (power_g.as_element(), power_k),
80///     &mut Transcript::new(b"custom_proof"),
81///     &mut rng,
82/// );
83/// proof.verify(
84///     &log_base,
85///     (power_g.as_element(), power_k),
86///     &mut Transcript::new(b"custom_proof"),
87/// )?;
88/// # Ok(())
89/// # }
90/// ```
91///
92/// [fst]: https://en.wikipedia.org/wiki/Fiat%E2%80%93Shamir_heuristic
93/// [this course]: http://www.cs.au.dk/~ivan/Sigma.pdf
94#[derive(Debug, Clone, Copy)]
95#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
96#[cfg_attr(feature = "serde", serde(bound = ""))]
97pub struct LogEqualityProof<G: Group> {
98    #[cfg_attr(feature = "serde", serde(with = "ScalarHelper::<G>"))]
99    challenge: G::Scalar,
100    #[cfg_attr(feature = "serde", serde(with = "ScalarHelper::<G>"))]
101    response: G::Scalar,
102}
103
104impl<G: Group> LogEqualityProof<G> {
105    /// Creates a new proof.
106    ///
107    /// # Parameters
108    ///
109    /// - `log_base` is the second discrete log base (`K` in the notation above). The first
110    ///   log base is always the [`Group`] generator.
111    /// - `secret` is the discrete log (`r` in the notation above).
112    /// - `powers` are `[r]G` and `[r]K`, respectively. It is **not** checked whether `r`
113    ///   is a discrete log of these powers; if this is not the case, the constructed proof
114    ///   will not [`verify`](Self::verify()).
115    pub fn new<R: CryptoRng + RngCore>(
116        log_base: &PublicKey<G>,
117        secret: &SecretKey<G>,
118        powers: (G::Element, G::Element),
119        transcript: &mut Transcript,
120        rng: &mut R,
121    ) -> Self {
122        transcript.start_proof(b"log_eq");
123        transcript.append_element_bytes(b"K", log_base.as_bytes());
124        transcript.append_element::<G>(b"[r]G", &powers.0);
125        transcript.append_element::<G>(b"[r]K", &powers.1);
126
127        let random_scalar = SecretKey::<G>::generate(rng);
128        transcript.append_element::<G>(b"[x]G", &G::mul_generator(random_scalar.expose_scalar()));
129        transcript.append_element::<G>(
130            b"[x]K",
131            &(log_base.as_element() * random_scalar.expose_scalar()),
132        );
133        let challenge = transcript.challenge_scalar::<G>(b"c");
134        let response = challenge * secret.expose_scalar() + random_scalar.expose_scalar();
135
136        Self {
137            challenge,
138            response,
139        }
140    }
141
142    /// Verifies this proof.
143    ///
144    /// # Parameters
145    ///
146    /// - `log_base` is the second discrete log base (`K` in the notation above). The first
147    ///   log base is always the [`Group`] generator.
148    /// - `powers` are group elements presumably equal to `[r]G` and `[r]K` respectively,
149    ///   where `r` is a secret scalar.
150    ///
151    /// # Errors
152    ///
153    /// Returns an error if this proof does not verify.
154    pub fn verify(
155        &self,
156        log_base: &PublicKey<G>,
157        powers: (G::Element, G::Element),
158        transcript: &mut Transcript,
159    ) -> Result<(), VerificationError> {
160        let commitments = (
161            G::vartime_double_mul_generator(&-self.challenge, powers.0, &self.response),
162            G::vartime_multi_mul(
163                &[-self.challenge, self.response],
164                [powers.1, log_base.as_element()],
165            ),
166        );
167
168        transcript.start_proof(b"log_eq");
169        transcript.append_element_bytes(b"K", log_base.as_bytes());
170        transcript.append_element::<G>(b"[r]G", &powers.0);
171        transcript.append_element::<G>(b"[r]K", &powers.1);
172        transcript.append_element::<G>(b"[x]G", &commitments.0);
173        transcript.append_element::<G>(b"[x]K", &commitments.1);
174        let expected_challenge = transcript.challenge_scalar::<G>(b"c");
175
176        if expected_challenge == self.challenge {
177            Ok(())
178        } else {
179            Err(VerificationError::ChallengeMismatch)
180        }
181    }
182
183    /// Serializes this proof into bytes. As described [above](#implementation-details),
184    /// the is serialized as 2 scalars: `(c, s)`, i.e., challenge and response.
185    pub fn to_bytes(self) -> Vec<u8> {
186        let mut bytes = vec![0_u8; 2 * G::SCALAR_SIZE];
187        G::serialize_scalar(&self.challenge, &mut bytes[..G::SCALAR_SIZE]);
188        G::serialize_scalar(&self.response, &mut bytes[G::SCALAR_SIZE..]);
189        bytes
190    }
191
192    /// Attempts to parse the proof from `bytes`. Returns `None` if `bytes` do not represent
193    /// a well-formed proof.
194    pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
195        if bytes.len() != 2 * G::SCALAR_SIZE {
196            return None;
197        }
198
199        let challenge = G::deserialize_scalar(&bytes[..G::SCALAR_SIZE])?;
200        let response = G::deserialize_scalar(&bytes[G::SCALAR_SIZE..])?;
201        Some(Self {
202            challenge,
203            response,
204        })
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use rand::thread_rng;
211
212    use super::*;
213    use crate::group::Ristretto;
214
215    type Keypair = crate::Keypair<Ristretto>;
216
217    #[test]
218    fn log_equality_basics() {
219        let mut rng = thread_rng();
220        let log_base = Keypair::generate(&mut rng).public().clone();
221
222        for _ in 0..100 {
223            let (generator_val, secret) = Keypair::generate(&mut rng).into_tuple();
224            let key_val = log_base.as_element() * secret.expose_scalar();
225            let proof = LogEqualityProof::new(
226                &log_base,
227                &secret,
228                (generator_val.as_element(), key_val),
229                &mut Transcript::new(b"testing_log_equality"),
230                &mut rng,
231            );
232
233            proof
234                .verify(
235                    &log_base,
236                    (generator_val.as_element(), key_val),
237                    &mut Transcript::new(b"testing_log_equality"),
238                )
239                .unwrap();
240        }
241    }
242}