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