Browse Source

feat(NUT12): verify DLEQ on blinded signatures

thesimplekid 11 months ago
parent
commit
a6e77c62af

+ 1 - 0
README.md

@@ -21,6 +21,7 @@ Experimental bindings for this project can be found in the [cashu-crab-bindings]
 - :heavy_check_mark: [NUT-09](https://github.com/cashubtc/nuts/blob/main/09.md)
 - :heavy_check_mark: [NUT-10](https://github.com/cashubtc/nuts/blob/main/10.md)
 - :heavy_check_mark: [NUT-11](https://github.com/cashubtc/nuts/blob/main/11.md)
+- :heavy_check_mark: [NUT-12](https://github.com/cashubtc/nuts/blob/main/12.md)
 - :heavy_check_mark: [NUT-13](https://github.com/cashubtc/nuts/blob/main/13.md)
 
 

+ 2 - 1
crates/cashu-sdk/Cargo.toml

@@ -14,12 +14,13 @@ default = ["mint", "wallet", "all-nuts", "redb"]
 mint = ["cashu/mint"]
 wallet = ["cashu/wallet", "dep:minreq", "dep:once_cell"]
 gloo = ["dep:gloo"]
-all-nuts = ["nut07", "nut08", "nut09", "nut10", "nut11", "nut13"]
+all-nuts = ["nut07", "nut08", "nut09", "nut10", "nut11", "nut12", "nut13"]
 nut07 = ["cashu/nut07"]
 nut08 = ["cashu/nut08"]
 nut09 = ["cashu/nut07", "cashu/nut09"]
 nut10 = ["cashu/nut10"]
 nut11 = ["cashu/nut11"]
+nut12 = ["cashu/nut12"]
 nut13 = ["cashu/nut13"]
 redb = ["dep:redb"]
 

+ 2 - 0
crates/cashu-sdk/src/mint/mod.rs

@@ -362,6 +362,8 @@ impl Mint {
             amount: *amount,
             c: c.into(),
             keyset_id: keyset.id,
+            #[cfg(feature = "nut12")]
+            dleq: None,
         })
     }
 

+ 9 - 0
crates/cashu-sdk/src/wallet/mod.rs

@@ -338,6 +338,15 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
 
         let keys = self.get_keyset_keys(&mint_url, active_keyset_id).await?;
 
+        #[cfg(feature = "nut12")]
+        {
+            for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) {
+                let keys = self.localstore.get_keys(&sig.keyset_id).await?.unwrap();
+                let key = keys.amount_key(sig.amount).unwrap();
+                sig.verify_dleq(&key, &premint.blinded_message.b).unwrap();
+            }
+        }
+
         let proofs = construct_proofs(
             mint_res.signatures,
             premint_secrets.rs(),

+ 2 - 1
crates/cashu/Cargo.toml

@@ -15,12 +15,13 @@ description = "Cashu rust wallet and mint library"
 default = ["mint", "wallet", "all-nuts"]
 mint = []
 wallet = []
-all-nuts = ["nut07", "nut08", "nut09", "nut10", "nut11", "nut13"]
+all-nuts = ["nut07", "nut08", "nut09", "nut10", "nut11", "nut12", "nut13"]
 nut07 = []
 nut08 = []
 nut09 = []
 nut10 = []
 nut11 = ["nut10"]
+nut12 = []
 nut13 = ["dep:bip39", "dep:bip32", "nut09"]
 
 

+ 95 - 43
crates/cashu/src/dhke.rs

@@ -1,6 +1,7 @@
 //! Diffie-Hellmann key exchange
 
 use bitcoin::hashes::{sha256, Hash};
+use k256::elliptic_curve::sec1::ToEncodedPoint;
 #[cfg(feature = "mint")]
 pub use mint::{sign_message, verify_message};
 #[cfg(feature = "wallet")]
@@ -39,6 +40,18 @@ pub fn hash_to_curve(message: &[u8]) -> Result<k256::PublicKey, Error> {
     Err(Error::NoValidPoint)
 }
 
+pub fn hash_e(pubkeys: Vec<k256::PublicKey>) -> Vec<u8> {
+    let mut e = "".to_string();
+
+    for pubkey in pubkeys {
+        let uncompressed_point = pubkey.to_encoded_point(false).to_bytes();
+
+        e.push_str(&hex::encode(uncompressed_point));
+    }
+
+    sha256::Hash::hash(e.as_bytes()).to_byte_array().to_vec()
+}
+
 #[cfg(feature = "wallet")]
 mod wallet {
     use std::ops::Mul;
@@ -104,12 +117,16 @@ mod wallet {
 
             let unblinded_signature = unblind_message(blinded_c, r.into(), a)?;
 
-            let proof = Proof::new(
-                blinded_signature.amount,
-                blinded_signature.keyset_id,
+            let proof = Proof {
+                amount: blinded_signature.amount,
+                keyset_id: blinded_signature.keyset_id,
                 secret,
-                unblinded_signature,
-            );
+                c: unblinded_signature,
+                #[cfg(feature = "nut11")]
+                witness: None,
+                #[cfg(feature = "nut12")]
+                dleq: blinded_signature.dleq,
+            };
 
             proofs.push(proof);
         }
@@ -163,57 +180,92 @@ mod mint {
 
 #[cfg(test)]
 mod tests {
+    use std::str::FromStr;
+
     use hex::decode;
     use k256::elliptic_curve::scalar::ScalarPrimitive;
 
     use super::*;
+    use crate::nuts::PublicKey;
+
+    #[test]
+    fn test_hash_to_curve() {
+        let secret = "0000000000000000000000000000000000000000000000000000000000000000";
+        let sec_hex = decode(secret).unwrap();
+
+        let y = hash_to_curve(&sec_hex).unwrap();
+        let expected_y = k256::PublicKey::from_sec1_bytes(
+            &hex::decode("024cce997d3b518f739663b757deaec95bcd9473c30a14ac2fd04023a739d1a725")
+                .unwrap(),
+        )
+        .unwrap();
+        println!("{}", hex::encode(y.to_sec1_bytes()));
+        assert_eq!(y, expected_y);
+
+        let secret = "0000000000000000000000000000000000000000000000000000000000000001";
+        let sec_hex = decode(secret).unwrap();
+        let y = hash_to_curve(&sec_hex).unwrap();
+        let expected_y = k256::PublicKey::from_sec1_bytes(
+            &hex::decode("022e7158e11c9506f1aa4248bf531298daa7febd6194f003edcd9b93ade6253acf")
+                .unwrap(),
+        )
+        .unwrap();
+        println!("{}", hex::encode(y.to_sec1_bytes()));
+        assert_eq!(y, expected_y);
+        // Note that this message will take a few iterations of the loop before finding
+        // a valid point
+        let secret = "0000000000000000000000000000000000000000000000000000000000000002";
+        let sec_hex = decode(secret).unwrap();
+        let y = hash_to_curve(&sec_hex).unwrap();
+        let expected_y = k256::PublicKey::from_sec1_bytes(
+            &hex::decode("026cdbe15362df59cd1dd3c9c11de8aedac2106eca69236ecd9fbe117af897be4f")
+                .unwrap(),
+        )
+        .unwrap();
+        println!("{}", hex::encode(y.to_sec1_bytes()));
+        assert_eq!(y, expected_y);
+    }
+
+    #[test]
+    fn test_hash_e() {
+        let c = PublicKey::from_str(
+            "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
+        )
+        .unwrap();
+
+        let k = PublicKey::from_str(
+            "020000000000000000000000000000000000000000000000000000000000000001",
+        )
+        .unwrap();
+
+        let r1 = PublicKey::from_str(
+            "020000000000000000000000000000000000000000000000000000000000000001",
+        )
+        .unwrap();
+
+        let r2 = PublicKey::from_str(
+            "020000000000000000000000000000000000000000000000000000000000000001",
+        )
+        .unwrap();
+
+        let e = hash_e(vec![r1.into(), r2.into(), k.into(), c.into()]);
+        let e_hex = hex::encode(e);
+
+        assert_eq!(
+            "a4dc034b74338c28c6bc3ea49731f2a24440fc7c4affc08b31a93fc9fbe6401e",
+            e_hex
+        )
+    }
 
     #[cfg(feature = "wallet")]
     mod wallet_tests {
+
         use k256::SecretKey;
 
         use super::*;
         use crate::nuts::PublicKey;
 
         #[test]
-        fn test_hash_to_curve() {
-            let secret = "0000000000000000000000000000000000000000000000000000000000000000";
-            let sec_hex = decode(secret).unwrap();
-
-            let y = hash_to_curve(&sec_hex).unwrap();
-            let expected_y = k256::PublicKey::from_sec1_bytes(
-                &hex::decode("024cce997d3b518f739663b757deaec95bcd9473c30a14ac2fd04023a739d1a725")
-                    .unwrap(),
-            )
-            .unwrap();
-            println!("{}", hex::encode(y.to_sec1_bytes()));
-            assert_eq!(y, expected_y);
-
-            let secret = "0000000000000000000000000000000000000000000000000000000000000001";
-            let sec_hex = decode(secret).unwrap();
-            let y = hash_to_curve(&sec_hex).unwrap();
-            let expected_y = k256::PublicKey::from_sec1_bytes(
-                &hex::decode("022e7158e11c9506f1aa4248bf531298daa7febd6194f003edcd9b93ade6253acf")
-                    .unwrap(),
-            )
-            .unwrap();
-            println!("{}", hex::encode(y.to_sec1_bytes()));
-            assert_eq!(y, expected_y);
-            // Note that this message will take a few iterations of the loop before finding
-            // a valid point
-            let secret = "0000000000000000000000000000000000000000000000000000000000000002";
-            let sec_hex = decode(secret).unwrap();
-            let y = hash_to_curve(&sec_hex).unwrap();
-            let expected_y = k256::PublicKey::from_sec1_bytes(
-                &hex::decode("026cdbe15362df59cd1dd3c9c11de8aedac2106eca69236ecd9fbe117af897be4f")
-                    .unwrap(),
-            )
-            .unwrap();
-            println!("{}", hex::encode(y.to_sec1_bytes()));
-            assert_eq!(y, expected_y);
-        }
-
-        #[test]
         fn test_blind_message() {
             let message = "d341ee4871f1f889041e63cf0d3823c713eea6aff01e80f1719f08f9e5be98f6";
             let sec: crate::nuts::SecretKey = crate::nuts::SecretKey::from_hex(

+ 4 - 0
crates/cashu/src/nuts/mod.rs

@@ -15,6 +15,8 @@ pub mod nut09;
 pub mod nut10;
 #[cfg(feature = "nut11")]
 pub mod nut11;
+#[cfg(feature = "nut12")]
+pub mod nut12;
 #[cfg(feature = "nut13")]
 pub mod nut13;
 
@@ -45,5 +47,7 @@ pub use nut09::{RestoreRequest, RestoreResponse};
 pub use nut10::{Kind, Secret as Nut10Secret, SecretData};
 #[cfg(feature = "nut11")]
 pub use nut11::{P2PKConditions, SigFlag, Signatures, SigningKey, VerifyingKey};
+#[cfg(feature = "nut12")]
+pub use nut12::DleqProof;
 
 pub type Proofs = Vec<Proof>;

+ 10 - 0
crates/cashu/src/nuts/nut00.rs

@@ -7,6 +7,8 @@ use std::str::FromStr;
 
 use serde::{Deserialize, Serialize};
 
+#[cfg(feature = "nut12")]
+use super::DleqProof;
 use super::{Id, Proofs, PublicKey};
 use crate::error::Error;
 #[cfg(feature = "nut11")]
@@ -420,6 +422,9 @@ pub struct BlindedSignature {
     /// blinded signature (C_) on the secret message `B_` of [BlindedMessage]
     #[serde(rename = "C_")]
     pub c: PublicKey,
+    /// DLEQ Proof
+    #[cfg(feature = "nut12")]
+    pub dleq: Option<DleqProof>,
 }
 
 /// Proofs [NUT-00]
@@ -443,6 +448,9 @@ pub struct Proof {
     #[serde(serialize_with = "witness_serialize")]
     #[serde(deserialize_with = "witness_deserialize")]
     pub witness: Option<Signatures>,
+    /// DLEQ Proof
+    #[cfg(feature = "nut12")]
+    pub dleq: Option<DleqProof>,
 }
 
 impl Proof {
@@ -454,6 +462,8 @@ impl Proof {
             c,
             #[cfg(feature = "nut11")]
             witness: None,
+            #[cfg(feature = "nut12")]
+            dleq: None,
         }
     }
 }

+ 2 - 0
crates/cashu/src/nuts/nut11.rs

@@ -758,6 +758,8 @@ mod tests {
             )
             .unwrap(),
             witness: Some(Signatures { signatures: vec![] }),
+            #[cfg(feature = "nut12")]
+            dleq: None,
         };
 
         proof.sign_p2pk(secret_key).unwrap();

+ 137 - 0
crates/cashu/src/nuts/nut12.rs

@@ -0,0 +1,137 @@
+//! NUT-12: Offline ecash signature validation
+//! https://github.com/cashubtc/nuts/blob/main/12.md
+use std::ops::Mul;
+
+use k256::Scalar;
+use log::{debug, warn};
+use serde::{Deserialize, Serialize};
+
+use super::{BlindedSignature, Proof, PublicKey, SecretKey};
+use crate::dhke::{hash_e, hash_to_curve};
+use crate::error::Error;
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct DleqProof {
+    e: SecretKey,
+    s: SecretKey,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    r: Option<SecretKey>,
+}
+
+fn verify_dleq(
+    blinded_message: k256::PublicKey,
+    blinded_signature: k256::PublicKey,
+    e: k256::SecretKey,
+    s: k256::SecretKey,
+    mint_pubkey: k256::PublicKey,
+) -> Result<(), Error> {
+    let r1 = s.public_key().to_projective()
+        - mint_pubkey
+            .as_affine()
+            .mul(Scalar::from(e.as_scalar_primitive()));
+
+    let r2 = blinded_message
+        .as_affine()
+        .mul(Scalar::from(s.as_scalar_primitive()))
+        - blinded_signature
+            .as_affine()
+            .mul(Scalar::from(e.as_scalar_primitive()));
+
+    let e_bytes = e.to_bytes().to_vec();
+
+    let hash_e = hash_e(vec![
+        k256::PublicKey::try_from(r1)?,
+        k256::PublicKey::try_from(r2)?,
+        mint_pubkey,
+        blinded_signature,
+    ]);
+
+    if e_bytes.ne(&hash_e) {
+        warn!("DLEQ on signature failed");
+        debug!("e_bytes: {:?}, Hash e: {:?}", e_bytes, hash_e);
+        // TODO: fix error
+        return Err(Error::TokenSpent);
+    }
+
+    Ok(())
+}
+
+impl Proof {
+    pub fn verify_dleq(
+        &self,
+        mint_pubkey: PublicKey,
+        blinding_factor: SecretKey,
+    ) -> Result<(), Error> {
+        let (e, s): (k256::SecretKey, k256::SecretKey) = if let Some(dleq) = &self.dleq {
+            (dleq.e.clone().into(), dleq.s.clone().into())
+        } else {
+            // TODO: fix error
+            return Err(Error::AmountKey);
+        };
+
+        let c: k256::PublicKey = (&self.c).into();
+        let mint_pubkey: k256::PublicKey = mint_pubkey.into();
+        let blinding_factor: k256::SecretKey = blinding_factor.into();
+
+        let y = hash_to_curve(self.secret.0.as_bytes())?;
+        let blinded_signature = c.to_projective()
+            + mint_pubkey
+                .as_affine()
+                .mul(Scalar::from(blinding_factor.as_scalar_primitive()));
+        let blinded_message = y.to_projective() + blinding_factor.public_key().to_projective();
+
+        let blinded_signature = k256::PublicKey::try_from(blinded_signature)?;
+        let blinded_message = k256::PublicKey::try_from(blinded_message)?;
+
+        verify_dleq(blinded_message, blinded_signature, e, s, mint_pubkey)
+    }
+}
+
+impl BlindedSignature {
+    pub fn verify_dleq(
+        &self,
+        mint_pubkey: &PublicKey,
+        blinded_message: &PublicKey,
+    ) -> Result<(), Error> {
+        let (e, s): (k256::SecretKey, k256::SecretKey) = if let Some(dleq) = &self.dleq {
+            (dleq.e.clone().into(), dleq.s.clone().into())
+        } else {
+            // TODO: fix error
+            return Err(Error::AmountKey);
+        };
+
+        let mint_pubkey: k256::PublicKey = mint_pubkey.into();
+        let blinded_message: k256::PublicKey = blinded_message.into();
+
+        let c: k256::PublicKey = (&self.c).into();
+        verify_dleq(blinded_message, c, e, s, mint_pubkey)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+
+    use std::str::FromStr;
+
+    use super::*;
+
+    #[test]
+    fn test_blind_signature_dleq() {
+        let blinded_sig = r#"{"amount":8,"id":"00882760bfa2eb41","C_":"02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2","dleq":{"e":"9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9","s":"9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73da"}}"#;
+
+        let blinded: BlindedSignature = serde_json::from_str(blinded_sig).unwrap();
+
+        let secret_key =
+            SecretKey::from_hex("0000000000000000000000000000000000000000000000000000000000000001")
+                .unwrap();
+
+        let mint_key = secret_key.public_key();
+
+        let blinded_secret = PublicKey::from_str(
+            "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2",
+        )
+        .unwrap();
+
+        blinded.verify_dleq(&mint_key, &blinded_secret).unwrap()
+    }
+}