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#[derive(Clone, Copy, PartialEq, Eq, Debug)]
40#[cfg_attr(feature = "std", derive(thiserror::Error))]
41pub enum ClsagError {
42 #[cfg_attr(feature = "std", error("invalid ring"))]
44 InvalidRing,
45 #[cfg_attr(feature = "std", error("invalid commitment"))]
47 InvalidKey,
48 #[cfg_attr(feature = "std", error("invalid commitment"))]
50 InvalidCommitment,
51 #[cfg_attr(feature = "std", error("invalid key image"))]
53 InvalidImage,
54 #[cfg_attr(feature = "std", error("invalid D"))]
56 InvalidD,
57 #[cfg_attr(feature = "std", error("invalid s"))]
59 InvalidS,
60 #[cfg_attr(feature = "std", error("invalid c1"))]
62 InvalidC1,
63}
64
65#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
67pub struct ClsagContext {
68 commitment: Commitment,
70 decoys: Decoys,
72}
73
74impl ClsagContext {
75 pub fn new(decoys: Decoys, commitment: Commitment) -> Result<ClsagContext, ClsagError> {
77 if decoys.len() > u8::MAX.into() {
78 Err(ClsagError::InvalidRing)?;
79 }
80
81 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
96fn 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 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 let mu_P = keccak256_to_scalar(&to_hash);
141 to_hash[PREFIX_AGG_0_LEN - 1] = b'1';
143 let mu_C = keccak256_to_scalar(&to_hash);
144
145 to_hash.truncate(((2 * n) + 1) * 32);
147 for i in 0 .. ROUND.len() {
148 to_hash[PREFIX.len() + i] = ROUND[i];
149 }
150 to_hash.extend(pseudo_out.compress().to_bytes());
153 to_hash.extend(msg);
154
155 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 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 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 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 c1.conditional_assign(&c, i.ct_eq(&(n - 1)));
222 }
223
224 Ok(((*D_INV_EIGHT, c * mu_P, c * mu_C), c1))
226}
227
228#[derive(Clone, PartialEq, Eq, Debug)]
230pub struct Clsag {
231 pub D: CompressedEdwardsY,
233 pub s: Vec<Scalar>,
235 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 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 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 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 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 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 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 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 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 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 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 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}