Skip to main content

cashu/nuts/
nut12.rs

1//! NUT-12: Offline ecash signature validation
2//!
3//! <https://github.com/cashubtc/nuts/blob/main/12.md>
4
5use core::ops::Deref;
6
7use bitcoin::secp256k1::hashes::{hmac, sha256, Hash, HashEngine, HmacEngine};
8use bitcoin::secp256k1::{self, Scalar};
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12use super::nut00::{BlindSignature, Proof};
13use super::nut01::{PublicKey, SecretKey};
14use super::nut02::Id;
15use crate::dhke::{hash_e, hash_to_curve};
16use crate::{Amount, SECP256K1};
17
18/// NUT12 Error
19#[derive(Debug, Error)]
20pub enum Error {
21    /// Missing DLEQ Proof
22    #[error("No DLEQ proof provided")]
23    MissingDleqProof,
24    /// Incomplete DLEQ Proof
25    #[error("Incomplete DLEQ proof")]
26    IncompleteDleqProof,
27    /// Invalid DLEQ Proof
28    #[error("Invalid DLEQ proof")]
29    InvalidDleqProof,
30    /// Could not derive deterministic DLEQ nonce
31    #[error("Could not derive deterministic DLEQ nonce")]
32    CouldNotDeriveDleqNonce,
33    /// DHKE error
34    #[error(transparent)]
35    DHKE(#[from] crate::dhke::Error),
36    /// NUT01 Error
37    #[error(transparent)]
38    NUT01(#[from] crate::nuts::nut01::Error),
39    /// SECP256k1 Error
40    #[error(transparent)]
41    Secp256k1(#[from] secp256k1::Error),
42}
43
44/// Blinded Signature on Dleq
45///
46/// Defined in [NUT12](https://github.com/cashubtc/nuts/blob/main/12.md)
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct BlindSignatureDleq {
49    /// e
50    pub e: SecretKey,
51    /// s
52    pub s: SecretKey,
53}
54
55/// Proof Dleq
56///
57/// Defined in [NUT12](https://github.com/cashubtc/nuts/blob/main/12.md)
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct ProofDleq {
60    /// e
61    pub e: SecretKey,
62    /// s
63    pub s: SecretKey,
64    /// Blinding factor
65    pub r: SecretKey,
66}
67
68impl ProofDleq {
69    /// Create new [`ProofDleq`]
70    pub fn new(e: SecretKey, s: SecretKey, r: SecretKey) -> Self {
71        Self { e, s, r }
72    }
73}
74
75/// Verify DLEQ
76fn verify_dleq(
77    blinded_message: PublicKey,   // B'
78    blinded_signature: PublicKey, // C'
79    e: &SecretKey,
80    s: &SecretKey,
81    mint_pubkey: PublicKey, // A
82) -> Result<(), Error> {
83    let e_bytes: [u8; 32] = e.to_secret_bytes();
84    let e: Scalar = e.as_scalar();
85
86    // a = e*A
87    let a: PublicKey = mint_pubkey.mul_tweak(&SECP256K1, &e)?.into();
88
89    // R1 = s*G - a
90    let a: PublicKey = a.negate(&SECP256K1).into();
91    let r1: PublicKey = s.public_key().combine(&a)?.into(); // s*G + (-a)
92
93    // b = s*B'
94    let s: Scalar = Scalar::from(s.deref().to_owned());
95    let b: PublicKey = blinded_message.mul_tweak(&SECP256K1, &s)?.into();
96
97    // c = e*C'
98    let c: PublicKey = blinded_signature.mul_tweak(&SECP256K1, &e)?.into();
99
100    // R2 = b - c
101    let c: PublicKey = c.negate(&SECP256K1).into();
102    let r2: PublicKey = b.combine(&c)?.into();
103
104    // hash(R1,R2,A,C')
105    let hash_e: [u8; 32] = hash_e([r1, r2, mint_pubkey, blinded_signature]);
106
107    if e_bytes != hash_e {
108        tracing::warn!("DLEQ on signature failed");
109        tracing::debug!("e_bytes: {:?}, hash_e: {:?}", e_bytes, hash_e);
110        return Err(Error::InvalidDleqProof);
111    }
112
113    Ok(())
114}
115
116fn derive_deterministic_nonce(
117    blinded_signature: PublicKey, // C'
118    blinded_message: &PublicKey,  // B'
119    mint_secret_key: &SecretKey,  // a
120) -> Result<SecretKey, Error> {
121    for counter in u8::MIN..=u8::MAX {
122        let mut message = Vec::with_capacity(16 + (3 * 65) + 1);
123        message.extend_from_slice(b"Cashu_DLEQ_R_v1");
124        message.extend_from_slice(&mint_secret_key.public_key().to_uncompressed_bytes());
125        message.extend_from_slice(&blinded_message.to_uncompressed_bytes());
126        message.extend_from_slice(&blinded_signature.to_uncompressed_bytes());
127        message.push(counter);
128
129        let mut engine = HmacEngine::<sha256::Hash>::new(mint_secret_key.as_secret_bytes());
130        engine.input(&message);
131        let hmac_result = hmac::Hmac::<sha256::Hash>::from_engine(engine);
132        let result_bytes = hmac_result.to_byte_array();
133
134        match SecretKey::from_slice(&result_bytes) {
135            Ok(nonce) => return Ok(nonce),
136            Err(_) => continue,
137        }
138    }
139
140    Err(Error::CouldNotDeriveDleqNonce)
141}
142
143fn calculate_dleq(
144    blinded_signature: PublicKey, // C'
145    blinded_message: &PublicKey,  // B'
146    mint_secret_key: &SecretKey,  // a
147) -> Result<BlindSignatureDleq, Error> {
148    let r: SecretKey =
149        derive_deterministic_nonce(blinded_signature, blinded_message, mint_secret_key)?;
150
151    // R1 = r*G
152    let r1 = r.public_key();
153
154    // R2 = r*B'
155    let r_scal: Scalar = r.as_scalar();
156    let r2: PublicKey = blinded_message.mul_tweak(&SECP256K1, &r_scal)?.into();
157
158    // e = hash(R1,R2,A,C')
159    let e: [u8; 32] = hash_e([r1, r2, mint_secret_key.public_key(), blinded_signature]);
160    let e_sk: SecretKey = SecretKey::from_slice(&e)?;
161
162    // s1 = e*a
163    let s1: SecretKey = e_sk.mul_tweak(&mint_secret_key.as_scalar())?.into();
164
165    // s = r + s1
166    let s: SecretKey = r.add_tweak(&s1.to_scalar())?.into();
167
168    Ok(BlindSignatureDleq { e: e_sk, s })
169}
170
171impl Proof {
172    /// Verify proof Dleq
173    pub fn verify_dleq(&self, mint_pubkey: PublicKey) -> Result<(), Error> {
174        match &self.dleq {
175            Some(dleq) => {
176                let y = hash_to_curve(self.secret.as_bytes())?;
177
178                let r: Scalar = dleq.r.as_scalar();
179                let bs1: PublicKey = mint_pubkey.mul_tweak(&SECP256K1, &r)?.into();
180
181                let blinded_signature: PublicKey = self.c.combine(&bs1)?.into();
182                let blinded_message: PublicKey = y.combine(&dleq.r.public_key())?.into();
183
184                verify_dleq(
185                    blinded_message,
186                    blinded_signature,
187                    &dleq.e,
188                    &dleq.s,
189                    mint_pubkey,
190                )
191            }
192            None => Err(Error::MissingDleqProof),
193        }
194    }
195}
196
197impl BlindSignature {
198    /// New DLEQ
199    #[inline]
200    pub fn new(
201        amount: Amount,
202        blinded_signature: PublicKey,
203        keyset_id: Id,
204        blinded_message: &PublicKey,
205        mint_secretkey: &SecretKey,
206    ) -> Result<Self, Error> {
207        Ok(Self {
208            amount,
209            keyset_id,
210            c: blinded_signature,
211            dleq: Some(calculate_dleq(
212                blinded_signature,
213                blinded_message,
214                mint_secretkey,
215            )?),
216        })
217    }
218
219    /// Verify dleq on proof
220    #[inline]
221    pub fn verify_dleq(
222        &self,
223        mint_pubkey: PublicKey,
224        blinded_message: PublicKey,
225    ) -> Result<(), Error> {
226        match &self.dleq {
227            Some(dleq) => verify_dleq(blinded_message, self.c, &dleq.e, &dleq.s, mint_pubkey),
228            None => Err(Error::MissingDleqProof),
229        }
230    }
231
232    /// Add Dleq to proof
233    /*
234    r = HMAC-SHA256(key=a, data="Cashu_DLEQ_R_v1" || A || B' || C' || ctr)
235    R1 = r*G
236    R2 = r*B'
237    e = hash(R1,R2,A,C')
238    s = (r + e*a) mod n
239    */
240    pub fn add_dleq_proof(
241        &mut self,
242        blinded_message: &PublicKey,
243        mint_secretkey: &SecretKey,
244    ) -> Result<(), Error> {
245        let dleq: BlindSignatureDleq = calculate_dleq(self.c, blinded_message, mint_secretkey)?;
246        self.dleq = Some(dleq);
247        Ok(())
248    }
249}
250
251#[cfg(test)]
252mod tests {
253
254    use std::str::FromStr;
255
256    use super::*;
257
258    #[test]
259    fn test_blind_signature_dleq() {
260        let blinded_sig = r#"{"amount":8,"id":"00882760bfa2eb41","C_":"02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2","dleq":{"e":"9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9","s":"9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73da"}}"#;
261
262        let blinded: BlindSignature = serde_json::from_str(blinded_sig).unwrap();
263
264        let secret_key =
265            SecretKey::from_hex("0000000000000000000000000000000000000000000000000000000000000001")
266                .unwrap();
267
268        let mint_key = secret_key.public_key();
269
270        let blinded_secret = PublicKey::from_str(
271            "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
272        )
273        .unwrap();
274
275        blinded.verify_dleq(mint_key, blinded_secret).unwrap()
276    }
277
278    #[test]
279    fn test_blind_signature_dleq_deterministic_nonce_vector() {
280        let mint_secret_key =
281            SecretKey::from_hex("0000000000000000000000000000000000000000000000000000000000000002")
282                .expect("valid mint secret key");
283        let blinded_message = PublicKey::from_str(
284            "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
285        )
286        .expect("valid blinded message");
287        let blinded_signature = PublicKey::from_str(
288            "0244eccfc7a348274458bb38044c7f3c389b3c2086c7ec18b5812d2877ab937787",
289        )
290        .expect("valid blinded signature");
291
292        let blind_signature = BlindSignature::new(
293            Amount::from(1),
294            blinded_signature,
295            Id::from_str("00882760bfa2eb41").expect("valid keyset id"),
296            &blinded_message,
297            &mint_secret_key,
298        )
299        .expect("deterministic DLEQ proof");
300        let dleq = blind_signature.dleq.expect("DLEQ proof");
301
302        assert_eq!(
303            dleq.e.to_secret_hex(),
304            "2a16ffee280aff3c429045607f9b8e0bf8b35910c44c1b20b9dfaf01b263d7b3"
305        );
306        assert_eq!(
307            dleq.s.to_secret_hex(),
308            "9df27731238334718d120d4f74611a7c668233f988e687ac3fb188f0a34a2dab"
309        );
310        assert!(verify_dleq(
311            blinded_message,
312            blinded_signature,
313            &dleq.e,
314            &dleq.s,
315            mint_secret_key.public_key(),
316        )
317        .is_ok());
318    }
319
320    #[test]
321    fn test_proof_dleq() {
322        let proof = r#"{"amount": 1,"id": "00882760bfa2eb41","secret": "daf4dd00a2b68a0858a80450f52c8a7d2ccf87d375e43e216e0c571f089f63e9","C": "024369d2d22a80ecf78f3937da9d5f30c1b9f74f0c32684d583cca0fa6a61cdcfc","dleq": {"e": "b31e58ac6527f34975ffab13e70a48b6d2b0d35abc4b03f0151f09ee1a9763d4","s": "8fbae004c59e754d71df67e392b6ae4e29293113ddc2ec86592a0431d16306d8","r": "a6d13fcd7a18442e6076f5e1e7c887ad5de40a019824bdfa9fe740d302e8d861"}}"#;
323
324        let proof: Proof = serde_json::from_str(proof).unwrap();
325
326        // A
327        let a: PublicKey = PublicKey::from_str(
328            "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
329        )
330        .unwrap();
331
332        assert!(proof.verify_dleq(a).is_ok());
333    }
334
335    /// Tests that verify_dleq correctly rejects verification with a wrong mint key.
336    ///
337    /// This test is critical for security - if the verification function doesn't properly
338    /// check the mint key, an attacker could forge proofs using any key.
339    ///
340    /// Mutant testing: Catches mutations that replace verify_dleq with Ok(()) or remove
341    /// the verification logic.
342    #[test]
343    fn test_proof_dleq_wrong_mint_key() {
344        let proof = r#"{"amount": 1,"id": "00882760bfa2eb41","secret": "daf4dd00a2b68a0858a80450f52c8a7d2ccf87d375e43e216e0c571f089f63e9","C": "024369d2d22a80ecf78f3937da9d5f30c1b9f74f0c32684d583cca0fa6a61cdcfc","dleq": {"e": "b31e58ac6527f34975ffab13e70a48b6d2b0d35abc4b03f0151f09ee1a9763d4","s": "8fbae004c59e754d71df67e392b6ae4e29293113ddc2ec86592a0431d16306d8","r": "a6d13fcd7a18442e6076f5e1e7c887ad5de40a019824bdfa9fe740d302e8d861"}}"#;
345
346        let proof: Proof = serde_json::from_str(proof).unwrap();
347
348        // Wrong mint key - different from the one used to create the proof
349        let wrong_key: PublicKey = PublicKey::from_str(
350            "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5",
351        )
352        .unwrap();
353
354        // Verification should fail with wrong key
355        assert!(proof.verify_dleq(wrong_key).is_err());
356    }
357
358    /// Tests that verify_dleq correctly rejects proofs with missing DLEQ data.
359    ///
360    /// This test ensures that proofs without DLEQ data are rejected when DLEQ
361    /// verification is required.
362    ///
363    /// Mutant testing: Catches mutations that replace verify_dleq with Ok(()) or
364    /// remove the None check.
365    #[test]
366    fn test_proof_dleq_missing() {
367        let proof = r#"{"amount": 1,"id": "00882760bfa2eb41","secret": "daf4dd00a2b68a0858a80450f52c8a7d2ccf87d375e43e216e0c571f089f63e9","C": "024369d2d22a80ecf78f3937da9d5f30c1b9f74f0c32684d583cca0fa6a61cdcfc"}"#;
368
369        let proof: Proof = serde_json::from_str(proof).unwrap();
370
371        let a: PublicKey = PublicKey::from_str(
372            "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
373        )
374        .unwrap();
375
376        // Verification should fail when DLEQ is missing
377        let result = proof.verify_dleq(a);
378        assert!(result.is_err());
379        assert!(matches!(result.unwrap_err(), Error::MissingDleqProof));
380    }
381
382    /// Tests that BlindSignature::verify_dleq correctly rejects verification with wrong mint key.
383    ///
384    /// This test ensures that blind signature DLEQ verification properly validates the mint key.
385    ///
386    /// Mutant testing: Catches mutations that replace BlindSignature::verify_dleq with Ok(())
387    /// or remove the verification logic.
388    #[test]
389    fn test_blind_signature_dleq_wrong_key() {
390        let blinded_sig = r#"{"amount":8,"id":"00882760bfa2eb41","C_":"02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2","dleq":{"e":"9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9","s":"9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73da"}}"#;
391
392        let blinded: BlindSignature = serde_json::from_str(blinded_sig).unwrap();
393
394        // Wrong secret key - different from the one used to create the signature
395        let wrong_key =
396            SecretKey::from_hex("0000000000000000000000000000000000000000000000000000000000000002")
397                .unwrap();
398
399        let blinded_secret = PublicKey::from_str(
400            "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
401        )
402        .unwrap();
403
404        // Verification should fail with wrong key
405        assert!(blinded
406            .verify_dleq(wrong_key.public_key(), blinded_secret)
407            .is_err());
408    }
409
410    /// Tests that BlindSignature::verify_dleq correctly rejects verification with tampered DLEQ data.
411    ///
412    /// This test ensures that tampering with the 'e' or 's' values in the DLEQ proof
413    /// causes verification to fail.
414    ///
415    /// Mutant testing: Catches mutations that replace verify_dleq with Ok(()) or
416    /// weaken the cryptographic checks.
417    #[test]
418    fn test_blind_signature_dleq_tampered() {
419        // Tampered DLEQ data - 'e' and 's' values have been modified to wrong (but valid) values
420        let tampered_sig = r#"{"amount":8,"id":"00882760bfa2eb41","C_":"02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2","dleq":{"e":"0000000000000000000000000000000000000000000000000000000000000001","s":"0000000000000000000000000000000000000000000000000000000000000002"}}"#;
421
422        let blinded: BlindSignature = serde_json::from_str(tampered_sig).unwrap();
423
424        let secret_key =
425            SecretKey::from_hex("0000000000000000000000000000000000000000000000000000000000000001")
426                .unwrap();
427
428        let blinded_secret = PublicKey::from_str(
429            "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
430        )
431        .unwrap();
432
433        // Verification should fail with tampered data
434        assert!(blinded
435            .verify_dleq(secret_key.public_key(), blinded_secret)
436            .is_err());
437    }
438
439    /// Tests that BlindSignature::add_dleq_proof properly generates DLEQ data.
440    ///
441    /// This test ensures that add_dleq_proof actually adds the DLEQ proof and doesn't
442    /// just return Ok(()) without doing anything.
443    ///
444    /// Mutant testing: Catches mutations that replace add_dleq_proof with Ok(())
445    /// without actually adding the proof.
446    #[test]
447    fn test_add_dleq_proof() {
448        use crate::nuts::nut02::Id;
449
450        let secret_key =
451            SecretKey::from_hex("0000000000000000000000000000000000000000000000000000000000000001")
452                .unwrap();
453
454        let blinded_message = PublicKey::from_str(
455            "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
456        )
457        .unwrap();
458
459        let blinded_signature = PublicKey::from_str(
460            "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
461        )
462        .unwrap();
463
464        let mut blind_sig = BlindSignature {
465            amount: Amount::from(1),
466            keyset_id: Id::from_str("00882760bfa2eb41").unwrap(),
467            c: blinded_signature,
468            dleq: None,
469        };
470
471        // Initially, DLEQ should be None
472        assert!(blind_sig.dleq.is_none());
473
474        // Add DLEQ proof
475        blind_sig
476            .add_dleq_proof(&blinded_message, &secret_key)
477            .unwrap();
478
479        // After adding, DLEQ should be Some
480        assert!(blind_sig.dleq.is_some());
481
482        // Verify the added DLEQ is valid
483        assert!(blind_sig
484            .verify_dleq(secret_key.public_key(), blinded_message)
485            .is_ok());
486    }
487}