Prechádzať zdrojové kódy

feat: limit checks (#1657)

* feat: add limit checks to witness and secret.

* feat: migrations to remove large witness and secret

* feat: check input output limit for restore and check

* feat: check limit for description
tsk 1 deň pred
rodič
commit
6710a35a9b

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

@@ -330,6 +330,16 @@ impl Witness {
     }
 }
 
+impl std::fmt::Display for Witness {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "{}",
+            serde_json::to_string(self).map_err(|_| std::fmt::Error)?
+        )
+    }
+}
+
 /// Proofs
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]

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

@@ -344,6 +344,8 @@ pub trait SpendingConditionVerification {
                 if has_sig_all {
                     return Ok(true);
                 }
+            } else if proof.witness.is_some() {
+                return Err(super::nut11::Error::IncorrectWitnessKind);
             }
         }
 

+ 12 - 0
crates/cashu/src/secret.rs

@@ -59,6 +59,18 @@ impl Secret {
         Self(secret)
     }
 
+    /// Length of the secret string in bytes
+    #[inline]
+    pub fn len(&self) -> usize {
+        self.0.len()
+    }
+
+    /// Check if the secret is empty
+    #[inline]
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+
     /// [`Secret`] as bytes
     #[inline]
     pub fn as_bytes(&self) -> &[u8] {

+ 18 - 1
crates/cdk-common/src/error.rs

@@ -215,6 +215,24 @@ pub enum Error {
         /// Maximum allowed outputs
         max: usize,
     },
+    /// Proof content too large (secret or witness exceeds max length)
+    #[error("Proof content too large: {actual} bytes, max {max}")]
+    ProofContentTooLarge {
+        /// Actual size in bytes
+        actual: usize,
+        /// Maximum allowed size in bytes
+        max: usize,
+    },
+    /// Request field content too large (description or extra exceeds max length)
+    #[error("Request field '{field}' too large: {actual} bytes, max {max}")]
+    RequestFieldTooLarge {
+        /// Name of the field that exceeded the limit
+        field: String,
+        /// Actual size in bytes
+        actual: usize,
+        /// Maximum allowed size in bytes
+        max: usize,
+    },
     /// Multiple units provided
     #[error("Cannot have multiple units")]
     MultipleUnits,
@@ -1066,7 +1084,6 @@ pub enum ErrorCode {
     MaxInputsExceeded,
     /// The max number of outputs is exceeded
     MaxOutputsExceeded,
-
     // 12xxx - Keyset errors
     /// Keyset is not known (12001)
     KeysetNotFound,

+ 5 - 0
crates/cdk-sql-common/src/mint/migrations/postgres/20260219000000_clean_oversized_witness_and_secret.sql

@@ -0,0 +1,5 @@
+-- Clean up witness values exceeding 1024 characters
+UPDATE proof SET witness = NULL WHERE LENGTH(witness) > 1024;
+
+-- Clean up secret values exceeding 1024 characters
+UPDATE proof SET secret = '' WHERE LENGTH(secret) > 1024;

+ 5 - 0
crates/cdk-sql-common/src/mint/migrations/sqlite/20260219000000_clean_oversized_witness_and_secret.sql

@@ -0,0 +1,5 @@
+-- Clean up witness values exceeding 1024 characters
+UPDATE proof SET witness = NULL WHERE LENGTH(witness) > 1024;
+
+-- Clean up secret values exceeding 1024 characters
+UPDATE proof SET secret = '' WHERE LENGTH(secret) > 1024;

+ 2 - 2
crates/cdk/src/mint/builder.rs

@@ -71,8 +71,8 @@ impl MintBuilder {
             supported_units: HashMap::new(),
             custom_paths: HashMap::new(),
             use_keyset_v2: None,
-            max_inputs: 100,
-            max_outputs: 100,
+            max_inputs: 1000,
+            max_outputs: 1000,
         }
     }
 

+ 14 - 0
crates/cdk/src/mint/check_spendable.rs

@@ -12,6 +12,20 @@ impl Mint {
         &self,
         check_state: &CheckStateRequest,
     ) -> Result<CheckStateResponse, Error> {
+        // Check max inputs limit
+        let ys_count = check_state.ys.len();
+        if ys_count > self.max_inputs {
+            tracing::warn!(
+                "CheckState request exceeds max inputs limit: {} > {}",
+                ys_count,
+                self.max_inputs
+            );
+            return Err(Error::MaxInputsExceeded {
+                actual: ys_count,
+                max: self.max_inputs,
+            });
+        }
+
         let states = self.localstore.get_proofs_states(&check_state.ys).await?;
 
         if check_state.ys.len() != states.len() {

+ 42 - 0
crates/cdk/src/mint/issue/mod.rs

@@ -17,6 +17,7 @@ use cdk_common::{
 use cdk_prometheus::METRICS;
 use tracing::instrument;
 
+use crate::mint::verification::MAX_REQUEST_FIELD_LEN;
 use crate::mint::Verification;
 use crate::Mint;
 
@@ -271,6 +272,16 @@ impl Mint {
 
             let payment_options = match mint_quote_request {
                 MintQuoteRequest::Bolt11(bolt11_request) => {
+                    if let Some(ref desc) = bolt11_request.description {
+                        if desc.len() > MAX_REQUEST_FIELD_LEN {
+                            return Err(Error::RequestFieldTooLarge {
+                                field: "description".to_string(),
+                                actual: desc.len(),
+                                max: MAX_REQUEST_FIELD_LEN,
+                            });
+                        }
+                    }
+
                     let mint_ttl = self.quote_ttl().await?.mint_ttl;
 
                     let quote_expiry = unix_time() + mint_ttl;
@@ -295,6 +306,16 @@ impl Mint {
                     IncomingPaymentOptions::Bolt11(bolt11_options)
                 }
                 MintQuoteRequest::Bolt12(bolt12_request) => {
+                    if let Some(ref desc) = bolt12_request.description {
+                        if desc.len() > MAX_REQUEST_FIELD_LEN {
+                            return Err(Error::RequestFieldTooLarge {
+                                field: "description".to_string(),
+                                actual: desc.len(),
+                                max: MAX_REQUEST_FIELD_LEN,
+                            });
+                        }
+                    }
+
                     let description = bolt12_request.description;
 
                     let bolt12_options = Bolt12IncomingPaymentOptions {
@@ -306,6 +327,27 @@ impl Mint {
                     IncomingPaymentOptions::Bolt12(Box::new(bolt12_options))
                 }
                 MintQuoteRequest::Custom { method, request } => {
+                    if let Some(ref desc) = request.description {
+                        if desc.len() > MAX_REQUEST_FIELD_LEN {
+                            return Err(Error::RequestFieldTooLarge {
+                                field: "description".to_string(),
+                                actual: desc.len(),
+                                max: MAX_REQUEST_FIELD_LEN,
+                            });
+                        }
+                    }
+
+                    if !request.extra.is_null() {
+                        let extra_str = request.extra.to_string();
+                        if extra_str.len() > MAX_REQUEST_FIELD_LEN {
+                            return Err(Error::RequestFieldTooLarge {
+                                field: "extra".to_string(),
+                                actual: extra_str.len(),
+                                max: MAX_REQUEST_FIELD_LEN,
+                            });
+                        }
+                    }
+
                     let mint_ttl = self.quote_ttl().await?.mint_ttl;
                     let quote_expiry = unix_time() + mint_ttl;
 

+ 12 - 0
crates/cdk/src/mint/melt/mod.rs

@@ -19,6 +19,7 @@ use super::{
     CurrencyUnit, MeltQuote, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest, Mint,
     PaymentMethod,
 };
+use crate::mint::verification::MAX_REQUEST_FIELD_LEN;
 use crate::nuts::MeltQuoteState;
 use crate::types::PaymentProcessorKey;
 use crate::util::unix_time;
@@ -347,6 +348,17 @@ impl Mint {
             extra,
         } = melt_request;
 
+        if !extra.is_null() {
+            let extra_str = extra.to_string();
+            if extra_str.len() > MAX_REQUEST_FIELD_LEN {
+                return Err(Error::RequestFieldTooLarge {
+                    field: "extra".to_string(),
+                    actual: extra_str.len(),
+                    max: MAX_REQUEST_FIELD_LEN,
+                });
+            }
+        }
+
         let ln = self
             .payment_processors
             .get(&PaymentProcessorKey::new(

+ 13 - 0
crates/cdk/src/mint/mod.rs

@@ -945,6 +945,19 @@ impl Mint {
         let result = async {
             let output_len = request.outputs.len();
 
+            // Check max outputs limit
+            if output_len > self.max_outputs {
+                tracing::warn!(
+                    "Restore request exceeds max outputs limit: {} > {}",
+                    output_len,
+                    self.max_outputs
+                );
+                return Err(Error::MaxOutputsExceeded {
+                    actual: output_len,
+                    max: self.max_outputs,
+                });
+            }
+
             let mut outputs = Vec::with_capacity(output_len);
             let mut signatures = Vec::with_capacity(output_len);
 

+ 38 - 0
crates/cdk/src/mint/verification.rs

@@ -6,6 +6,12 @@ use tracing::instrument;
 use super::{Error, Mint};
 use crate::cdk_database;
 
+/// Maximum allowed length in bytes for proof secret or witness content
+const MAX_PROOF_CONTENT_LEN: usize = 1024;
+
+/// Maximum allowed length in bytes for request fields (description, extra)
+pub(crate) const MAX_REQUEST_FIELD_LEN: usize = 1024;
+
 /// Verification result with typed amount
 #[derive(Debug, Clone, Hash, PartialEq, Eq)]
 pub struct Verification {
@@ -206,6 +212,38 @@ impl Mint {
             });
         }
 
+        // Check proof content lengths (secret and witness) are within limits
+        for proof in inputs {
+            let secret_len = proof.secret.len();
+            if secret_len > MAX_PROOF_CONTENT_LEN {
+                tracing::warn!(
+                    "Proof secret exceeds max content length: {} > {}",
+                    secret_len,
+                    MAX_PROOF_CONTENT_LEN
+                );
+                return Err(Error::ProofContentTooLarge {
+                    actual: secret_len,
+                    max: MAX_PROOF_CONTENT_LEN,
+                });
+            }
+
+            if let Some(witness) = &proof.witness {
+                let witness_str = serde_json::to_string(witness)?;
+                let witness_len = witness_str.len();
+                if witness_len > MAX_PROOF_CONTENT_LEN {
+                    tracing::warn!(
+                        "Proof witness exceeds max content length: {} > {}",
+                        witness_len,
+                        MAX_PROOF_CONTENT_LEN
+                    );
+                    return Err(Error::ProofContentTooLarge {
+                        actual: witness_len,
+                        max: MAX_PROOF_CONTENT_LEN,
+                    });
+                }
+            }
+        }
+
         Mint::check_inputs_unique(inputs)?;
         let unit = self.verify_inputs_keyset(inputs).await?;