monero_clsag/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3#![cfg_attr(not(feature = "std"), no_std)]
4#![allow(non_snake_case)]
5
6use core::ops::Deref as _;
7use std_shims::{
8  vec,
9  vec::Vec,
10  io::{self, Read, Write},
11};
12
13use rand_core::{RngCore, CryptoRng};
14
15use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
16use subtle::{Choice, ConstantTimeEq as _, ConditionallySelectable as _};
17
18use curve25519_dalek::{
19  constants::ED25519_BASEPOINT_POINT,
20  scalar::Scalar as DScalar,
21  traits::{IsIdentity as _, MultiscalarMul as _, VartimePrecomputedMultiscalarMul as _},
22  edwards::{EdwardsPoint, VartimeEdwardsPrecomputation},
23};
24#[cfg(feature = "compile-time-generators")]
25use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE;
26#[cfg(not(feature = "compile-time-generators"))]
27use curve25519_dalek::constants::ED25519_BASEPOINT_POINT as ED25519_BASEPOINT_TABLE;
28
29use monero_io::*;
30use monero_ed25519::*;
31
32mod decoys;
33pub(crate) use decoys::MAX_RING_SIZE;
34pub use decoys::Decoys;
35
36#[cfg(feature = "multisig")]
37mod multisig;
38#[cfg(feature = "multisig")]
39pub use multisig::{ClsagMultisigMaskSender, ClsagAddendum, ClsagMultisig};
40
41#[cfg(all(feature = "std", test))]
42mod tests;
43
44#[cfg(feature = "std")]
45static G_PRECOMP_CELL: std_shims::sync::LazyLock<VartimeEdwardsPrecomputation> =
46  std_shims::sync::LazyLock::new(|| VartimeEdwardsPrecomputation::new([ED25519_BASEPOINT_POINT]));
47/// A cached (if std) pre-computation of the Ed25519 generator, G.
48#[cfg(feature = "std")]
49#[allow(non_snake_case)]
50fn G_PRECOMP() -> &'static VartimeEdwardsPrecomputation {
51  &G_PRECOMP_CELL
52}
53/// A cached (if std) pre-computation of the Ed25519 generator, G.
54#[cfg(not(feature = "std"))]
55#[allow(non_snake_case)]
56fn G_PRECOMP() -> VartimeEdwardsPrecomputation {
57  VartimeEdwardsPrecomputation::new([ED25519_BASEPOINT_POINT])
58}
59
60/// Errors when working with CLSAGs.
61#[derive(Clone, Copy, PartialEq, Eq, Debug, thiserror::Error)]
62pub enum ClsagError {
63  /// The ring was invalid (such as being too small or too large).
64  #[error("invalid ring")]
65  InvalidRing,
66  /// The discrete logarithm of the key, scaling G, wasn't equivalent to the signing ring member.
67  #[error("invalid commitment")]
68  InvalidKey,
69  /// The commitment opening provided did not match the ring member's.
70  #[error("invalid commitment")]
71  InvalidCommitment,
72  /// The key image was invalid (such as being identity or torsioned)
73  #[error("invalid key image")]
74  InvalidImage,
75  /// The `D` component was invalid.
76  #[error("invalid D")]
77  InvalidD,
78  /// The `s` vector was invalid.
79  #[error("invalid s")]
80  InvalidS,
81  /// The `c1` variable was invalid.
82  #[error("invalid c1")]
83  InvalidC1,
84}
85
86/// Context on the input being signed for.
87#[derive(Clone, Zeroize, ZeroizeOnDrop)]
88pub struct ClsagContext {
89  // The opening for the commitment of the signing ring member
90  commitment: Commitment,
91  // Selected ring members' positions, signer index, and ring
92  decoys: Decoys,
93}
94
95impl ClsagContext {
96  /// Create a new context, as necessary for signing.
97  ///
98  /// This function runs in time variable to the length of the ring and the validity of the
99  /// arguments.
100  pub fn new(decoys: Decoys, commitment: Commitment) -> Result<ClsagContext, ClsagError> {
101    // Validate the commitment matches
102    if bool::from(!decoys.signer_ring_members()[1].ct_eq(&commitment.commit())) {
103      Err(ClsagError::InvalidCommitment)?;
104    }
105
106    Ok(ClsagContext { commitment, decoys })
107  }
108}
109
110#[allow(clippy::large_enum_variant)]
111enum Mode {
112  Sign { signer_index: u8, A: EdwardsPoint, AH: EdwardsPoint },
113  Verify { c1: DScalar, D_serialized: CompressedPoint },
114}
115
116// Core of the CLSAG algorithm, applicable to both sign and verify with minimal differences
117//
118// Said differences are covered via the above Mode
119fn core(
120  ring: &[[Point; 2]],
121  I: &EdwardsPoint,
122  pseudo_out: &EdwardsPoint,
123  msg_hash: &[u8; 32],
124  D_torsion_free: &EdwardsPoint,
125  s: &[Scalar],
126  A_c1: &Mode,
127) -> ((EdwardsPoint, DScalar, DScalar), DScalar) {
128  let n = ring.len();
129
130  let images_precomp = match A_c1 {
131    Mode::Sign { .. } => None,
132    Mode::Verify { .. } => Some(VartimeEdwardsPrecomputation::new([I, D_torsion_free])),
133  };
134  let D_inv_eight = D_torsion_free * Scalar::INV_EIGHT.into();
135
136  // Generate the transcript
137  // Instead of generating multiple, a single transcript is created and then edited as needed
138  const PREFIX: &[u8] = b"CLSAG_";
139  #[rustfmt::skip]
140  const AGG_0: &[u8]  =       b"agg_0";
141  #[rustfmt::skip]
142  const ROUND: &[u8]  =       b"round";
143  const PREFIX_AGG_0_LEN: usize = PREFIX.len() + AGG_0.len();
144
145  let mut to_hash = Vec::with_capacity(((2 * n) + 5) * 32);
146  to_hash.extend(PREFIX);
147  to_hash.extend(AGG_0);
148  to_hash.extend([0; 32 - PREFIX_AGG_0_LEN]);
149
150  let mut P = Vec::with_capacity(n);
151  for member in ring {
152    P.push(member[0].into());
153    to_hash.extend(member[0].compress().to_bytes());
154  }
155
156  let mut C = Vec::with_capacity(n);
157  for member in ring {
158    C.push(member[1].into() - pseudo_out);
159    to_hash.extend(member[1].compress().to_bytes());
160  }
161
162  to_hash.extend(I.compress().to_bytes());
163  match A_c1 {
164    Mode::Sign { .. } => {
165      to_hash.extend(D_inv_eight.compress().to_bytes());
166    }
167    Mode::Verify { D_serialized, .. } => {
168      to_hash.extend(D_serialized.to_bytes());
169    }
170  }
171  to_hash.extend(pseudo_out.compress().to_bytes());
172  // mu_P with agg_0
173  let mu_P = Scalar::hash(&to_hash).into();
174  // mu_C with agg_1
175  to_hash[PREFIX_AGG_0_LEN - 1] = b'1';
176  let mu_C = Scalar::hash(&to_hash).into();
177
178  // Truncate it for the round transcript, altering the DST as needed
179  to_hash.truncate(((2 * n) + 1) * 32);
180  for i in 0 .. ROUND.len() {
181    to_hash[PREFIX.len() + i] = ROUND[i];
182  }
183  // Unfortunately, it's I D pseudo_out instead of pseudo_out I D, meaning this needs to be
184  // truncated just to add it back
185  to_hash.extend(pseudo_out.compress().to_bytes());
186  to_hash.extend(msg_hash);
187
188  // Configure the loop based on if we're signing or verifying
189  let start;
190  let end;
191  let iter_end;
192  let mut c;
193  match A_c1 {
194    Mode::Sign { signer_index, A, AH } => {
195      let signer_index = usize::from(*signer_index);
196      start = signer_index + 1;
197      end = signer_index + n;
198      iter_end = 2 * n;
199      to_hash.extend(A.compress().to_bytes());
200      to_hash.extend(AH.compress().to_bytes());
201      c = Scalar::hash(&to_hash).into();
202    }
203
204    Mode::Verify { c1, .. } => {
205      start = 0;
206      end = n;
207      iter_end = n;
208      c = *c1;
209    }
210  }
211
212  // Perform the core loop
213  let mut in_range = Choice::from(0);
214  let mut c1 = c;
215  for mut i in 0 .. iter_end {
216    in_range |= i.ct_eq(&start);
217    in_range ^= i.ct_eq(&end);
218    i %= n;
219
220    let c_p = mu_P * c;
221    let c_c = mu_C * c;
222
223    // (s_i * G) + (c_p * P_i) + (c_c * C_i)
224    let L = match A_c1 {
225      Mode::Sign { .. } => EdwardsPoint::multiscalar_mul(
226        [s[i].into(), c_p, c_c],
227        [ED25519_BASEPOINT_POINT, P[i], C[i]],
228      ),
229      Mode::Verify { .. } => {
230        G_PRECOMP().vartime_mixed_multiscalar_mul([s[i].into()], [c_p, c_c], [P[i], C[i]])
231      }
232    };
233
234    let PH = Point::biased_hash(P[i].compress().0).into();
235
236    // (c_p * I) + (c_c * D) + (s_i * PH)
237    let R = match A_c1 {
238      Mode::Sign { .. } => {
239        EdwardsPoint::multiscalar_mul([c_p, c_c, s[i].into()], [I, D_torsion_free, &PH])
240      }
241      Mode::Verify { .. } => images_precomp
242        .as_ref()
243        .expect("value populated when verifying wasn't populated")
244        .vartime_mixed_multiscalar_mul([c_p, c_c], [s[i].into()], [PH]),
245    };
246
247    to_hash.truncate(((2 * n) + 3) * 32);
248    to_hash.extend(L.compress().to_bytes());
249    to_hash.extend(R.compress().to_bytes());
250    c.conditional_assign(&Scalar::hash(&to_hash).into(), in_range);
251
252    c1.conditional_assign(&c, in_range & i.ct_eq(&(n - 1)));
253  }
254
255  // This first tuple is needed to continue signing, the latter is the c to be tested/worked with
256  ((D_inv_eight, c * mu_P, c * mu_C), c1)
257}
258
259/// The CLSAG signature, as used in Monero.
260#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
261pub struct Clsag {
262  /// The difference of the commitment randomnesses, scaling the key image generator.
263  pub D: CompressedPoint,
264  /// The responses for each ring member.
265  pub s: Vec<Scalar>,
266  /// The first challenge in the ring.
267  pub c1: Scalar,
268}
269
270struct ClsagSignCore {
271  incomplete_clsag: Clsag,
272  pseudo_out: EdwardsPoint,
273  key_challenge: DScalar,
274  challenged_mask: DScalar,
275}
276
277impl Clsag {
278  // Sign core is the extension of core as needed for signing, yet is shared between single signer
279  // and multisig, hence why it's still core
280  fn sign_core<R: RngCore + CryptoRng>(
281    rng: &mut R,
282    I: &EdwardsPoint,
283    input: &ClsagContext,
284    mask: DScalar,
285    msg_hash: &[u8; 32],
286    A: EdwardsPoint,
287    AH: EdwardsPoint,
288  ) -> ClsagSignCore {
289    let signer_index = input.decoys.signer_index();
290
291    let pseudo_out = Commitment::new(Scalar::from(mask), input.commitment.amount).commit().into();
292    let mask_delta = input.commitment.mask.into() - mask;
293
294    let H = Point::biased_hash(input.decoys.signer_ring_members()[0].compress().to_bytes()).into();
295    let D = H * mask_delta;
296    let mut s = Vec::with_capacity(input.decoys.ring().len());
297    for _ in 0 .. input.decoys.ring().len() {
298      s.push(Scalar::random(rng));
299    }
300    let ((D, c_p, c_c), c1) = core(
301      input.decoys.ring(),
302      I,
303      &pseudo_out,
304      msg_hash,
305      &D,
306      &s,
307      &Mode::Sign { signer_index, A, AH },
308    );
309
310    ClsagSignCore {
311      incomplete_clsag: Clsag {
312        D: CompressedPoint::from(D.compress().to_bytes()),
313        s,
314        c1: Scalar::from(c1),
315      },
316      pseudo_out,
317      key_challenge: c_p,
318      challenged_mask: c_c * mask_delta,
319    }
320  }
321
322  /// Sign CLSAG signatures for the provided inputs.
323  ///
324  /// Monero ensures the rerandomized input commitments have the same value as the outputs by
325  /// checking `sum(rerandomized_input_commitments) - sum(output_commitments) == 0`. This requires
326  /// not only the amounts balance, yet also
327  /// `sum(input_commitment_masks) - sum(output_commitment_masks)`.
328  ///
329  /// Monero solves this by following the wallet protocol to determine each output commitment's
330  /// randomness, then using random masks for all but the last input. The last input is
331  /// rerandomized to the necessary mask for the equation to balance.
332  ///
333  /// Due to Monero having this behavior, it only makes sense to sign CLSAGs as a list, hence this
334  /// API being the way it is.
335  ///
336  /// `inputs` is of the form (discrete logarithm of the key, context).
337  ///
338  /// `sum_outputs` is for the sum of the output commitments' masks.
339  ///
340  /// This function runs in time variable to the validity of the arguments and the public data.
341  ///
342  /// WARNING: This follows the Fiat-Shamir transcript format used by the Monero protocol, which
343  /// makes assumptions on what has already been transcripted and bound to within `msg_hash`. Do
344  /// not use this if you don't know what you're doing.
345  pub fn sign<R: RngCore + CryptoRng>(
346    rng: &mut R,
347    mut inputs: Vec<(Zeroizing<Scalar>, ClsagContext)>,
348    sum_outputs: Scalar,
349    msg_hash: [u8; 32],
350  ) -> Result<Vec<(Clsag, Point)>, ClsagError> {
351    // Create the key images
352    let mut key_image_generators = vec![];
353    let mut key_images = vec![];
354    for input in &inputs {
355      let key = Zeroizing::new((*input.0.deref()).into());
356      let public_key = input.1.decoys.signer_ring_members()[0].into();
357
358      // Check the key is consistent
359      if bool::from(!(ED25519_BASEPOINT_TABLE * key.deref()).ct_eq(&public_key)) {
360        Err(ClsagError::InvalidKey)?;
361      }
362
363      let key_image_generator = Point::biased_hash(public_key.compress().0).into();
364      key_image_generators.push(key_image_generator);
365      key_images.push(key_image_generator * key.deref());
366    }
367
368    let mut res = Vec::with_capacity(inputs.len());
369    let mut sum_pseudo_outs = DScalar::ZERO;
370    for i in 0 .. inputs.len() {
371      let mask;
372      // If this is the last input, set the mask as described above
373      if i == (inputs.len() - 1) {
374        mask = sum_outputs.into() - sum_pseudo_outs;
375      } else {
376        mask = Scalar::random(rng).into();
377        sum_pseudo_outs += mask;
378      }
379
380      let mut nonce = Zeroizing::new(Scalar::random(rng).into());
381      let ClsagSignCore { mut incomplete_clsag, pseudo_out, key_challenge, challenged_mask } =
382        Clsag::sign_core(
383          rng,
384          &key_images[i],
385          &inputs[i].1,
386          mask,
387          &msg_hash,
388          nonce.deref() * ED25519_BASEPOINT_TABLE,
389          nonce.deref() * key_image_generators[i],
390        );
391      {
392        // Effectively r - c x, except c x is (c_p x) + (c_c z), where z is the delta between the
393        // ring member's commitment and our pseudo-out commitment (which will only have a known
394        // discrete log over G if the amounts cancel out)
395        let signer_s = Scalar::from(
396          nonce.deref() -
397            ((key_challenge * Zeroizing::new((*inputs[i].0.deref()).into()).deref()) +
398              challenged_mask),
399        );
400        for s_index in 0 ..= MAX_RING_SIZE {
401          if usize::from(s_index) == incomplete_clsag.s.len() {
402            break;
403          }
404          let signer_index = s_index.ct_eq(&inputs[i].1.decoys.signer_index());
405          incomplete_clsag.s[usize::from(s_index)].conditional_assign(&signer_s, signer_index);
406        }
407      }
408      let clsag = incomplete_clsag;
409
410      // Zeroize private keys and nonces.
411      inputs[i].0.zeroize();
412      nonce.zeroize();
413
414      debug_assert!(clsag
415        .verify(
416          inputs[i].1.decoys.ring().iter().map(|r| [r[0].compress(), r[1].compress()]).collect(),
417          &key_images[i].compress().to_bytes().into(),
418          &pseudo_out.compress().to_bytes().into(),
419          &msg_hash
420        )
421        .is_ok());
422
423      res.push((clsag, Point::from(pseudo_out)));
424    }
425
426    Ok(res)
427  }
428
429  /// Verify a CLSAG signature for the provided context.
430  ///
431  /// WARNING: This follows the Fiat-Shamir transcript format used by the Monero protocol, which
432  /// makes assumptions on what has already been transcripted and bound to within `msg_hash`. Do
433  /// not use this if you don't know what you're doing.
434  pub fn verify(
435    &self,
436    ring: Vec<[CompressedPoint; 2]>,
437    I: &CompressedPoint,
438    pseudo_out: &CompressedPoint,
439    msg_hash: &[u8; 32],
440  ) -> Result<(), ClsagError> {
441    // Preliminary checks
442    // s, c1, and points must also be encoded canonically, which is checked at time of decode
443    if ring.is_empty() {
444      Err(ClsagError::InvalidRing)?;
445    }
446    if ring.len() != self.s.len() {
447      Err(ClsagError::InvalidS)?;
448    }
449
450    let I = I.decompress().ok_or(ClsagError::InvalidImage)?;
451    let Some(I) = I.key_image() else { Err(ClsagError::InvalidImage)? };
452
453    let Some(pseudo_out) = pseudo_out.decompress() else {
454      return Err(ClsagError::InvalidCommitment);
455    };
456    let Some(D) = self.D.decompress() else {
457      return Err(ClsagError::InvalidD);
458    };
459    let D_torsion_free = D.into().mul_by_cofactor();
460    if D_torsion_free.is_identity() {
461      Err(ClsagError::InvalidD)?;
462    }
463
464    let ring = ring
465      .into_iter()
466      .map(|r| Some([r[0].decompress()?, r[1].decompress()?]))
467      .collect::<Option<Vec<_>>>()
468      .ok_or(ClsagError::InvalidRing)?;
469
470    let (_, c1) = core(
471      &ring,
472      &I,
473      &pseudo_out.into(),
474      msg_hash,
475      &D_torsion_free,
476      &self.s,
477      &Mode::Verify { c1: self.c1.into(), D_serialized: self.D },
478    );
479    if c1 != self.c1.into() {
480      Err(ClsagError::InvalidC1)?;
481    }
482    Ok(())
483  }
484
485  /// Write a CLSAG.
486  pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
487    write_raw_vec(Scalar::write, &self.s, w)?;
488    self.c1.write(w)?;
489    self.D.write(w)
490  }
491
492  /// Read a CLSAG.
493  pub fn read<R: Read>(decoys: usize, r: &mut R) -> io::Result<Clsag> {
494    Ok(Clsag {
495      s: read_raw_vec(Scalar::read, decoys, r)?,
496      c1: Scalar::read(r)?,
497      D: CompressedPoint::read(r)?,
498    })
499  }
500}