monero_clsag/
lib.rs

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