1#![cfg_attr(docsrs, feature(doc_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::{Choice, ConstantTimeEq, ConditionallySelectable};
18
19use curve25519_dalek::{
20 constants::ED25519_BASEPOINT_POINT,
21 scalar::Scalar as DScalar,
22 traits::{IsIdentity, MultiscalarMul, VartimePrecomputedMultiscalarMul},
23 edwards::{EdwardsPoint, VartimeEdwardsPrecomputation},
24};
25#[cfg(feature = "compile-time-generators")]
26use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE;
27#[cfg(not(feature = "compile-time-generators"))]
28use curve25519_dalek::constants::ED25519_BASEPOINT_POINT as ED25519_BASEPOINT_TABLE;
29
30use monero_io::*;
31use monero_ed25519::*;
32
33mod decoys;
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 =
295 Point::biased_hash(input.decoys.ring()[usize::from(signer_index)][0].compress().to_bytes())
296 .into();
297 let D = H * mask_delta;
298 let mut s = Vec::with_capacity(input.decoys.ring().len());
299 for _ in 0 .. input.decoys.ring().len() {
300 s.push(Scalar::random(rng));
301 }
302 let ((D, c_p, c_c), c1) = core(
303 input.decoys.ring(),
304 I,
305 &pseudo_out,
306 msg_hash,
307 &D,
308 &s,
309 &Mode::Sign { signer_index, A, AH },
310 );
311
312 ClsagSignCore {
313 incomplete_clsag: Clsag {
314 D: CompressedPoint::from(D.compress().to_bytes()),
315 s,
316 c1: Scalar::from(c1),
317 },
318 pseudo_out,
319 key_challenge: c_p,
320 challenged_mask: c_c * mask_delta,
321 }
322 }
323
324 pub fn sign<R: RngCore + CryptoRng>(
348 rng: &mut R,
349 mut inputs: Vec<(Zeroizing<Scalar>, ClsagContext)>,
350 sum_outputs: Scalar,
351 msg_hash: [u8; 32],
352 ) -> Result<Vec<(Clsag, Point)>, ClsagError> {
353 let mut key_image_generators = vec![];
355 let mut key_images = vec![];
356 for input in &inputs {
357 let key = Zeroizing::new((*input.0.deref()).into());
358 let public_key = input.1.decoys.signer_ring_members()[0].into();
359
360 if bool::from(!(ED25519_BASEPOINT_TABLE * key.deref()).ct_eq(&public_key)) {
362 Err(ClsagError::InvalidKey)?;
363 }
364
365 let key_image_generator = Point::biased_hash(public_key.compress().0).into();
366 key_image_generators.push(key_image_generator);
367 key_images.push(key_image_generator * key.deref());
368 }
369
370 let mut res = Vec::with_capacity(inputs.len());
371 let mut sum_pseudo_outs = DScalar::ZERO;
372 for i in 0 .. inputs.len() {
373 let mask;
374 if i == (inputs.len() - 1) {
376 mask = sum_outputs.into() - sum_pseudo_outs;
377 } else {
378 mask = Scalar::random(rng).into();
379 sum_pseudo_outs += mask;
380 }
381
382 let mut nonce = Zeroizing::new(Scalar::random(rng).into());
383 let ClsagSignCore { mut incomplete_clsag, pseudo_out, key_challenge, challenged_mask } =
384 Clsag::sign_core(
385 rng,
386 &key_images[i],
387 &inputs[i].1,
388 mask,
389 &msg_hash,
390 nonce.deref() * ED25519_BASEPOINT_TABLE,
391 nonce.deref() * key_image_generators[i],
392 );
393 incomplete_clsag.s[usize::from(inputs[i].1.decoys.signer_index())] = Scalar::from(
397 nonce.deref() -
398 ((key_challenge * Zeroizing::new((*inputs[i].0.deref()).into()).deref()) +
399 challenged_mask),
400 );
401 let clsag = incomplete_clsag;
402
403 inputs[i].0.zeroize();
405 nonce.zeroize();
406
407 debug_assert!(clsag
408 .verify(
409 inputs[i].1.decoys.ring().iter().map(|r| [r[0].compress(), r[1].compress()]).collect(),
410 &key_images[i].compress().to_bytes().into(),
411 &pseudo_out.compress().to_bytes().into(),
412 &msg_hash
413 )
414 .is_ok());
415
416 res.push((clsag, Point::from(pseudo_out)));
417 }
418
419 Ok(res)
420 }
421
422 pub fn verify(
428 &self,
429 ring: Vec<[CompressedPoint; 2]>,
430 I: &CompressedPoint,
431 pseudo_out: &CompressedPoint,
432 msg_hash: &[u8; 32],
433 ) -> Result<(), ClsagError> {
434 if ring.is_empty() {
437 Err(ClsagError::InvalidRing)?;
438 }
439 if ring.len() != self.s.len() {
440 Err(ClsagError::InvalidS)?;
441 }
442
443 let I = I.decompress().ok_or(ClsagError::InvalidImage)?;
444 let Some(I) = I.key_image() else { Err(ClsagError::InvalidImage)? };
445
446 let Some(pseudo_out) = pseudo_out.decompress() else {
447 return Err(ClsagError::InvalidCommitment);
448 };
449 let Some(D) = self.D.decompress() else {
450 return Err(ClsagError::InvalidD);
451 };
452 let D_torsion_free = D.into().mul_by_cofactor();
453 if D_torsion_free.is_identity() {
454 Err(ClsagError::InvalidD)?;
455 }
456
457 let ring = ring
458 .into_iter()
459 .map(|r| Some([r[0].decompress()?, r[1].decompress()?]))
460 .collect::<Option<Vec<_>>>()
461 .ok_or(ClsagError::InvalidRing)?;
462
463 let (_, c1) = core(
464 &ring,
465 &I,
466 &pseudo_out.into(),
467 msg_hash,
468 &D_torsion_free,
469 &self.s,
470 &Mode::Verify { c1: self.c1.into(), D_serialized: self.D },
471 );
472 if c1 != self.c1.into() {
473 Err(ClsagError::InvalidC1)?;
474 }
475 Ok(())
476 }
477
478 pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
480 write_raw_vec(Scalar::write, &self.s, w)?;
481 self.c1.write(w)?;
482 self.D.write(w)
483 }
484
485 pub fn read<R: Read>(decoys: usize, r: &mut R) -> io::Result<Clsag> {
487 Ok(Clsag {
488 s: read_raw_vec(Scalar::read, decoys, r)?,
489 c1: Scalar::read(r)?,
490 D: CompressedPoint::read(r)?,
491 })
492 }
493}