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::biased_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#[derive(Clone, Copy, PartialEq, Eq, Debug, thiserror::Error)]
40pub enum ClsagError {
41 #[error("invalid ring")]
43 InvalidRing,
44 #[error("invalid commitment")]
46 InvalidKey,
47 #[error("invalid commitment")]
49 InvalidCommitment,
50 #[error("invalid key image")]
52 InvalidImage,
53 #[error("invalid D")]
55 InvalidD,
56 #[error("invalid s")]
58 InvalidS,
59 #[error("invalid c1")]
61 InvalidC1,
62}
63
64#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
66pub struct ClsagContext {
67 commitment: Commitment,
69 decoys: Decoys,
71}
72
73impl ClsagContext {
74 pub fn new(decoys: Decoys, commitment: Commitment) -> Result<ClsagContext, ClsagError> {
76 if decoys.len() > u8::MAX.into() {
77 Err(ClsagError::InvalidRing)?;
78 }
79
80 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: CompressedPoint },
93}
94
95fn 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 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.to_bytes());
148 }
149 }
150 to_hash.extend(pseudo_out.compress().to_bytes());
151 let mu_P = keccak256_to_scalar(&to_hash);
153 to_hash[PREFIX_AGG_0_LEN - 1] = b'1';
155 let mu_C = keccak256_to_scalar(&to_hash);
156
157 to_hash.truncate(((2 * n) + 1) * 32);
159 for i in 0 .. ROUND.len() {
160 to_hash[PREFIX.len() + i] = ROUND[i];
161 }
162 to_hash.extend(pseudo_out.compress().to_bytes());
165 to_hash.extend(msg_hash);
166
167 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 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 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 = biased_hash_to_point(P[i].compress().0);
205
206 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 c1.conditional_assign(&c, i.ct_eq(&(n - 1)));
226 }
227
228 ((D_inv_eight, c * mu_P, c * mu_C), c1)
230}
231
232#[derive(Clone, PartialEq, Eq, Debug)]
234pub struct Clsag {
235 pub D: CompressedPoint,
237 pub s: Vec<Scalar>,
239 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 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 = biased_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: CompressedPoint::from(D.compress()), s, c1 },
285 pseudo_out,
286 key_challenge: c_p,
287 challenged_mask: c_c * mask_delta,
288 }
289 }
290
291 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 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 if (ED25519_BASEPOINT_TABLE * input.0.deref()) != key {
326 Err(ClsagError::InvalidKey)?;
327 }
328
329 let key_image_generator = biased_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 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 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 inputs[i].0.zeroize();
366 nonce.zeroize();
367
368 debug_assert!(clsag
369 .verify(
370 inputs[i]
371 .1
372 .decoys
373 .ring()
374 .iter()
375 .map(|r| [r[0].compress().into(), r[1].compress().into()])
376 .collect(),
377 &key_images[i].compress().into(),
378 &pseudo_out.compress().into(),
379 &msg_hash
380 )
381 .is_ok());
382
383 res.push((clsag, pseudo_out));
384 }
385
386 Ok(res)
387 }
388
389 pub fn verify(
395 &self,
396 ring: Vec<[CompressedPoint; 2]>,
397 I: &CompressedPoint,
398 pseudo_out: &CompressedPoint,
399 msg_hash: &[u8; 32],
400 ) -> Result<(), ClsagError> {
401 if ring.is_empty() {
404 Err(ClsagError::InvalidRing)?;
405 }
406 if ring.len() != self.s.len() {
407 Err(ClsagError::InvalidS)?;
408 }
409
410 let I = I.decompress().ok_or(ClsagError::InvalidImage)?;
411 if I.is_identity() || (!I.is_torsion_free()) {
412 Err(ClsagError::InvalidImage)?;
413 }
414
415 let Some(pseudo_out) = pseudo_out.decompress() else {
416 return Err(ClsagError::InvalidCommitment);
417 };
418 let Some(D) = self.D.decompress() else {
419 return Err(ClsagError::InvalidD);
420 };
421 let D_torsion_free = D.mul_by_cofactor();
422 if D_torsion_free.is_identity() {
423 Err(ClsagError::InvalidD)?;
424 }
425
426 let ring = ring
427 .into_iter()
428 .map(|r| Some([r[0].decompress()?, r[1].decompress()?]))
429 .collect::<Option<Vec<_>>>()
430 .ok_or(ClsagError::InvalidRing)?;
431
432 let (_, c1) = core(
433 &ring,
434 &I,
435 &pseudo_out,
436 msg_hash,
437 &D_torsion_free,
438 &self.s,
439 &Mode::Verify { c1: self.c1, D_serialized: self.D },
440 );
441 if c1 != self.c1 {
442 Err(ClsagError::InvalidC1)?;
443 }
444 Ok(())
445 }
446
447 pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
449 write_raw_vec(write_scalar, &self.s, w)?;
450 w.write_all(&self.c1.to_bytes())?;
451 self.D.write(w)
452 }
453
454 pub fn read<R: Read>(decoys: usize, r: &mut R) -> io::Result<Clsag> {
456 Ok(Clsag {
457 s: read_raw_vec(read_scalar, decoys, r)?,
458 c1: read_scalar(r)?,
459 D: CompressedPoint::read(r)?,
460 })
461 }
462}