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