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#[cfg(feature = "std")]
49#[allow(non_snake_case)]
50fn G_PRECOMP() -> &'static VartimeEdwardsPrecomputation {
51 &G_PRECOMP_CELL
52}
53#[cfg(not(feature = "std"))]
55#[allow(non_snake_case)]
56fn G_PRECOMP() -> VartimeEdwardsPrecomputation {
57 VartimeEdwardsPrecomputation::new([ED25519_BASEPOINT_POINT])
58}
59
60#[derive(Clone, Copy, PartialEq, Eq, Debug, thiserror::Error)]
62pub enum ClsagError {
63 #[error("invalid ring")]
65 InvalidRing,
66 #[error("invalid commitment")]
68 InvalidKey,
69 #[error("invalid commitment")]
71 InvalidCommitment,
72 #[error("invalid key image")]
74 InvalidImage,
75 #[error("invalid D")]
77 InvalidD,
78 #[error("invalid s")]
80 InvalidS,
81 #[error("invalid c1")]
83 InvalidC1,
84}
85
86#[derive(Clone, Zeroize, ZeroizeOnDrop)]
88pub struct ClsagContext {
89 commitment: Commitment,
91 decoys: Decoys,
93}
94
95impl ClsagContext {
96 pub fn new(decoys: Decoys, commitment: Commitment) -> Result<ClsagContext, ClsagError> {
101 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
116fn 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 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 let mu_P = Scalar::hash(&to_hash).into();
174 to_hash[PREFIX_AGG_0_LEN - 1] = b'1';
176 let mu_C = Scalar::hash(&to_hash).into();
177
178 to_hash.truncate(((2 * n) + 1) * 32);
180 for i in 0 .. ROUND.len() {
181 to_hash[PREFIX.len() + i] = ROUND[i];
182 }
183 to_hash.extend(pseudo_out.compress().to_bytes());
186 to_hash.extend(msg_hash);
187
188 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 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 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 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 ((D_inv_eight, c * mu_P, c * mu_C), c1)
257}
258
259#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
261pub struct Clsag {
262 pub D: CompressedPoint,
264 pub s: Vec<Scalar>,
266 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 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 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 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 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 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 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 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 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 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 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 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}