Selaa lähdekoodia

Fix p2pk (#996)

* fix: refund keys must sign after locktime

* fix: custom tags

* refactor: locktime verification and refund
thesimplekid 2 kuukautta sitten
vanhempi
säilyke
809d9c8012
1 muutettua tiedostoa jossa 140 lisäystä ja 32 poistoa
  1. 140 32
      crates/cashu/src/nuts/nut11/mod.rs

+ 140 - 32
crates/cashu/src/nuts/nut11/mod.rs

@@ -146,6 +146,45 @@ impl Proof {
         let witness_signatures = witness_signatures.ok_or(Error::SignaturesNotProvided)?;
 
         let mut pubkeys = spending_conditions.pubkeys.clone().unwrap_or_default();
+        // NUT-11 enforcement per spec:
+        // - If locktime has passed and refund keys are present, spend must be authorized by
+        //   refund pubkeys (n_sigs_refund-of-refund). This supersedes normal pubkey enforcement
+        //   after expiry.
+        // - If locktime has passed and no refund keys are present, proof becomes spendable
+        //   without further key checks (anyone-can-spend behavior).
+        // - Otherwise (before locktime), enforce normal multisig on the set of authorized
+        //   pubkeys: Secret.data plus optional `pubkeys` tag, requiring n_sigs unique signers.
+
+        let now = unix_time();
+
+        if let Some(locktime) = spending_conditions.locktime {
+            if now >= locktime {
+                if let Some(refund_keys) = spending_conditions.refund_keys.clone() {
+                    let needed_refund_sigs =
+                        spending_conditions.num_sigs_refund.unwrap_or(1) as usize;
+                    let mut valid_pubkeys = HashSet::new();
+
+                    // After locktime, require signatures from refund keys
+                    for s in witness_signatures.iter() {
+                        let sig = Signature::from_str(s).map_err(|_| Error::InvalidSignature)?;
+                        for v in &refund_keys {
+                            if v.verify(msg, &sig).is_ok() {
+                                valid_pubkeys.insert(v);
+                                if valid_pubkeys.len() >= needed_refund_sigs {
+                                    return Ok(());
+                                }
+                            }
+                        }
+                    }
+
+                    // If locktime and refund keys were specified they must sign after locktime
+                    return Err(Error::SpendConditionsNotMet);
+                } else {
+                    // If only locktime is specified, consider it spendable after locktime
+                    return Ok(());
+                }
+            }
+        }
 
         if secret.kind().eq(&Kind::P2PK) {
             pubkeys.push(PublicKey::from_str(secret.secret_data().data())?);
@@ -175,34 +214,6 @@ impl Proof {
             return Ok(());
         }
 
-        if let (Some(locktime), Some(refund_keys)) = (
-            spending_conditions.locktime,
-            spending_conditions.refund_keys,
-        ) {
-            let needed_refund_sigs = spending_conditions.num_sigs_refund.unwrap_or(1) as usize;
-
-            let mut valid_pubkeys = HashSet::new();
-
-            // If lock time has passed check if refund witness signature is valid
-            if locktime.lt(&unix_time()) {
-                for s in witness_signatures.iter() {
-                    for v in &refund_keys {
-                        let sig = Signature::from_str(s).map_err(|_| Error::InvalidSignature)?;
-
-                        if v.verify(msg, &sig).is_ok() {
-                            if !valid_pubkeys.insert(v) {
-                                return Err(Error::DuplicateSignature);
-                            }
-
-                            if valid_pubkeys.len() >= needed_refund_sigs {
-                                return Ok(());
-                            }
-                        }
-                    }
-                }
-            }
-        }
-
         Err(Error::SpendConditionsNotMet)
     }
 }
@@ -502,7 +513,7 @@ impl From<Conditions> for Vec<Vec<String>> {
             refund_keys,
             num_sigs,
             sig_flag,
-            num_sigs_refund: _,
+            num_sigs_refund,
         } = conditions;
 
         let mut tags = Vec::new();
@@ -522,6 +533,11 @@ impl From<Conditions> for Vec<Vec<String>> {
         if let Some(refund_keys) = refund_keys {
             tags.push(Tag::Refund(refund_keys).as_vec())
         }
+
+        if let Some(num_sigs_refund) = num_sigs_refund {
+            tags.push(Tag::NSigsRefund(num_sigs_refund).as_vec())
+        }
+
         tags.push(Tag::SigFlag(sig_flag).as_vec());
         tags
     }
@@ -577,13 +593,22 @@ impl TryFrom<Vec<Vec<String>>> for Conditions {
             None
         };
 
+        let num_sigs_refund = if let Some(tag) = tags.get(&TagKind::NSigsRefund) {
+            match tag {
+                Tag::NSigsRefund(num_sigs) => Some(*num_sigs),
+                _ => None,
+            }
+        } else {
+            None
+        };
+
         Ok(Conditions {
             locktime,
             pubkeys,
             refund_keys,
             num_sigs,
             sig_flag,
-            num_sigs_refund: None,
+            num_sigs_refund,
         })
     }
 }
@@ -619,7 +644,7 @@ impl fmt::Display for TagKind {
             Self::Refund => write!(f, "refund"),
             Self::Pubkeys => write!(f, "pubkeys"),
             Self::NSigsRefund => write!(f, "n_sigs_refund"),
-            Self::Custom(kind) => write!(f, "{kind}"),
+            Self::Custom(c) => write!(f, "{c}"),
         }
     }
 }
@@ -635,6 +660,7 @@ where
             "locktime" => Self::Locktime,
             "refund" => Self::Refund,
             "pubkeys" => Self::Pubkeys,
+            "n_sigs_refund" => Self::NSigsRefund,
             t => Self::Custom(t.to_owned()),
         }
     }
@@ -741,6 +767,10 @@ pub enum Tag {
     Refund(Vec<PublicKey>),
     /// Pubkeys [`Tag`]
     PubKeys(Vec<PublicKey>),
+    /// Number of Sigs refund [`Tag`]
+    NSigsRefund(u64),
+    /// Custom tag
+    Custom(String, Vec<String>),
 }
 
 impl Tag {
@@ -752,6 +782,8 @@ impl Tag {
             Self::LockTime(_) => TagKind::Locktime,
             Self::Refund(_) => TagKind::Refund,
             Self::PubKeys(_) => TagKind::Pubkeys,
+            Self::NSigsRefund(_) => TagKind::NSigsRefund,
+            Self::Custom(tag, _) => TagKind::Custom(tag.to_string()),
         }
     }
 
@@ -792,7 +824,16 @@ where
 
                 Ok(Self::PubKeys(pubkeys))
             }
-            _ => Err(Error::UnknownTag),
+            TagKind::NSigsRefund => Ok(Tag::NSigsRefund(tag[1].as_ref().parse()?)),
+            TagKind::Custom(name) => {
+                let tags = tag
+                    .iter()
+                    .skip(1)
+                    .map(|p| p.as_ref().to_string())
+                    .collect::<Vec<String>>();
+
+                Ok(Self::Custom(name, tags))
+            }
         }
     }
 }
@@ -818,6 +859,18 @@ impl From<Tag> for Vec<String> {
                 }
                 tag
             }
+            Tag::NSigsRefund(num_sigs) => {
+                vec![TagKind::NSigsRefund.to_string(), num_sigs.to_string()]
+            }
+            Tag::Custom(name, c) => {
+                let mut tag = vec![name];
+
+                for t in c {
+                    tag.push(t);
+                }
+
+                tag
+            }
         }
     }
 }
@@ -1360,6 +1413,61 @@ mod tests {
         assert!(invalid_proof.verify_p2pk().is_err());
     }
 
+    #[test]
+    fn sig_with_non_refund_keys_after_locktime() {
+        let secret_key =
+            SecretKey::from_str("99590802251e78ee1051648439eedb003dc539093a48a44e7b8f2642c909ea37")
+                .unwrap();
+
+        let signing_key_two =
+            SecretKey::from_str("0000000000000000000000000000000000000000000000000000000000000001")
+                .unwrap();
+
+        let signing_key_three =
+            SecretKey::from_str("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f")
+                .unwrap();
+        let v_key: PublicKey = secret_key.public_key();
+        let v_key_two: PublicKey = signing_key_two.public_key();
+        let v_key_three: PublicKey = signing_key_three.public_key();
+
+        let conditions = Conditions {
+            locktime: Some(21),
+            pubkeys: Some(vec![v_key_three]),
+            refund_keys: Some(vec![v_key, v_key_two]),
+            num_sigs: None,
+            sig_flag: SigFlag::SigInputs,
+            num_sigs_refund: Some(2),
+        };
+
+        let secret: Secret = Nut10Secret::new(Kind::P2PK, v_key.to_string(), Some(conditions))
+            .try_into()
+            .unwrap();
+
+        let mut proof = Proof {
+            keyset_id: Id::from_str("009a1f293253e41e").unwrap(),
+            amount: Amount::ZERO,
+            secret,
+            c: PublicKey::from_str(
+                "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904",
+            )
+            .unwrap(),
+            witness: Some(Witness::P2PKWitness(P2PKWitness { signatures: vec![] })),
+            dleq: None,
+        };
+
+        proof.sign_p2pk(signing_key_three.clone()).unwrap();
+
+        assert!(proof.verify_p2pk().is_err());
+
+        proof.witness = None;
+
+        proof.sign_p2pk(secret_key).unwrap();
+        assert!(proof.verify_p2pk().is_err());
+        proof.sign_p2pk(signing_key_two).unwrap();
+
+        assert!(proof.verify_p2pk().is_ok());
+    }
+
     // Helper functions for melt request tests
     fn create_test_proof(secret: Secret, pubkey: PublicKey, id: &str) -> Proof {
         Proof {