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