diff --git a/lightning-liquidity/src/lsps5/msgs.rs b/lightning-liquidity/src/lsps5/msgs.rs index 6d3743a9a5a..ef0aaf4f5f4 100644 --- a/lightning-liquidity/src/lsps5/msgs.rs +++ b/lightning-liquidity/src/lsps5/msgs.rs @@ -161,11 +161,6 @@ pub enum LSPS5ClientError { /// The cryptographic signature from the LSP node doesn't validate. InvalidSignature, - /// Notification timestamp is too old or too far in the future. - /// - /// LSPS5 requires timestamps to be within ±10 minutes of current time. - InvalidTimestamp, - /// Detected a reused notification signature. /// /// Indicates a potential replay attack where a previously seen @@ -183,8 +178,7 @@ impl LSPS5ClientError { use LSPS5ClientError::*; match self { InvalidSignature => Self::BASE + 1, - InvalidTimestamp => Self::BASE + 2, - ReplayAttack => Self::BASE + 3, + ReplayAttack => Self::BASE + 2, SerializationError => LSPS5_SERIALIZATION_ERROR_CODE, } } @@ -193,7 +187,6 @@ impl LSPS5ClientError { use LSPS5ClientError::*; match self { InvalidSignature => "Invalid signature", - InvalidTimestamp => "Timestamp out of range", ReplayAttack => "Replay attack detected", SerializationError => "Error serializing LSPS5 webhook notification", } diff --git a/lightning-liquidity/src/lsps5/validator.rs b/lightning-liquidity/src/lsps5/validator.rs index 97c8560eb63..8063ea743b7 100644 --- a/lightning-liquidity/src/lsps5/validator.rs +++ b/lightning-liquidity/src/lsps5/validator.rs @@ -15,7 +15,6 @@ use crate::alloc::string::ToString; use crate::lsps0::ser::LSPSDateTime; use crate::lsps5::msgs::WebhookNotification; use crate::sync::Mutex; -use crate::utils::time::TimeProvider; use lightning::util::message_signing; @@ -24,32 +23,8 @@ use bitcoin::secp256k1::PublicKey; use alloc::collections::VecDeque; use alloc::string::String; -use core::ops::Deref; -use core::time::Duration; - -/// Configuration for signature storage. -#[derive(Clone, Copy, Debug)] -pub struct SignatureStorageConfig { - /// Maximum number of signatures to store. - pub max_signatures: usize, - /// Retention time for signatures in minutes. - pub retention_minutes: Duration, -} - -/// Default retention time for signatures in minutes (LSPS5 spec requires min 20 minutes). -pub const DEFAULT_SIGNATURE_RETENTION_MINUTES: u64 = 20; - -/// Default maximum number of stored signatures. -pub const DEFAULT_MAX_SIGNATURES: usize = 1000; - -impl Default for SignatureStorageConfig { - fn default() -> Self { - Self { - max_signatures: DEFAULT_MAX_SIGNATURES, - retention_minutes: Duration::from_secs(DEFAULT_SIGNATURE_RETENTION_MINUTES * 60), - } - } -} +/// Maximum number of recent signatures to track for replay attack prevention. +pub const MAX_RECENT_SIGNATURES: usize = 5; /// A utility for validating webhook notifications from an LSP. /// @@ -60,66 +35,26 @@ impl Default for SignatureStorageConfig { /// /// # Core Capabilities /// -/// - `validate(...)` -> Verifies signature, timestamp, and protects against replay attacks. -/// -/// # Usage +/// - `validate(...)` -> Verifies signature, and protects against replay attacks. /// -/// The validator requires a `SignatureStore` to track recently seen signatures -/// to prevent replay attacks. You should create a single `LSPS5Validator` instance -/// and share it across all requests. +/// The validator stores a [`small number`] of the most recently seen signatures +/// to protect against replays of the same notification. /// +/// [`small number`]: MAX_RECENT_SIGNATURES /// [`bLIP-55 / LSPS5 specification`]: https://github.com/lightning/blips/pull/55/files -pub struct LSPS5Validator -where - TP::Target: TimeProvider, - SS::Target: SignatureStore, -{ - time_provider: TP, - signature_store: SS, +pub struct LSPS5Validator { + recent_signatures: Mutex>, } -impl LSPS5Validator -where - TP::Target: TimeProvider, - SS::Target: SignatureStore, -{ - /// Creates a new `LSPS5Validator`. - pub fn new(time_provider: TP, signature_store: SS) -> Self { - Self { time_provider, signature_store } - } - - fn verify_notification_signature( - &self, counterparty_node_id: PublicKey, signature_timestamp: &LSPSDateTime, - signature: &str, notification: &WebhookNotification, - ) -> Result<(), LSPS5ClientError> { - let now = - LSPSDateTime::new_from_duration_since_epoch(self.time_provider.duration_since_epoch()); - let diff = signature_timestamp.abs_diff(&now); - const MAX_TIMESTAMP_DRIFT_SECS: u64 = 600; - if diff > MAX_TIMESTAMP_DRIFT_SECS { - return Err(LSPS5ClientError::InvalidTimestamp); - } - - let notification_json = serde_json::to_string(notification) - .map_err(|_| LSPS5ClientError::SerializationError)?; - let message = format!( - "LSPS5: DO NOT SIGN THIS MESSAGE MANUALLY: LSP: At {} I notify {}", - signature_timestamp.to_rfc3339(), - notification_json - ); - - if message_signing::verify(message.as_bytes(), signature, &counterparty_node_id) { - Ok(()) - } else { - Err(LSPS5ClientError::InvalidSignature) - } +impl LSPS5Validator { + /// Create a new LSPS5Validator instance. + pub fn new() -> Self { + Self { recent_signatures: Mutex::new(VecDeque::with_capacity(MAX_RECENT_SIGNATURES)) } } /// Parse and validate a webhook notification received from an LSP. /// - /// Verifies the webhook delivery by checking the timestamp is within ±10 minutes, - /// ensuring no signature replay within the retention window, and verifying the - /// zbase32 LN-style signature against the LSP's node ID. + /// Verifies the webhook delivery by verifying the zbase32 LN-style signature against the LSP's node ID and ensuring that the signature is not a replay of a previously seen notification (within the last [`MAX_RECENT_SIGNATURES`] notifications). /// /// Call this method on your proxy/server before processing any webhook notification /// to ensure its authenticity. @@ -130,91 +65,40 @@ where /// - `signature`: The zbase32-encoded LN signature over timestamp+body. /// - `notification`: The [`WebhookNotification`] received from the LSP. /// - /// Returns the validated [`WebhookNotification`] or an error for invalid timestamp, - /// replay attack, or signature verification failure. + /// Returns the validated [`WebhookNotification`] or an error for signature verification failure or replay attack. /// /// [`WebhookNotification`]: super::msgs::WebhookNotification + /// [`MAX_RECENT_SIGNATURES`]: MAX_RECENT_SIGNATURES pub fn validate( &self, counterparty_node_id: PublicKey, timestamp: &LSPSDateTime, signature: &str, notification: &WebhookNotification, ) -> Result { - self.verify_notification_signature( - counterparty_node_id, - timestamp, - signature, - notification, - )?; + let notification_json = serde_json::to_string(notification) + .map_err(|_| LSPS5ClientError::SerializationError)?; + let message = format!( + "LSPS5: DO NOT SIGN THIS MESSAGE MANUALLY: LSP: At {} I notify {}", + timestamp.to_rfc3339(), + notification_json + ); - if self.signature_store.exists(signature)? { - return Err(LSPS5ClientError::ReplayAttack); + if !message_signing::verify(message.as_bytes(), signature, &counterparty_node_id) { + return Err(LSPS5ClientError::InvalidSignature); } - self.signature_store.store(signature)?; + self.check_for_replay_attack(signature)?; Ok(notification.clone()) } -} - -/// Trait for storing and checking webhook notification signatures to prevent replay attacks. -pub trait SignatureStore { - /// Checks if a signature already exists in the store. - fn exists(&self, signature: &str) -> Result; - /// Stores a new signature. - fn store(&self, signature: &str) -> Result<(), LSPS5ClientError>; -} - -/// An in-memory store for webhook notification signatures. -pub struct InMemorySignatureStore -where - TP::Target: TimeProvider, -{ - recent_signatures: Mutex>, - config: SignatureStorageConfig, - time_provider: TP, -} -impl InMemorySignatureStore -where - TP::Target: TimeProvider, -{ - /// Creates a new `InMemorySignatureStore`. - pub fn new(config: SignatureStorageConfig, time_provider: TP) -> Self { - Self { - recent_signatures: Mutex::new(VecDeque::with_capacity(config.max_signatures)), - config, - time_provider, - } - } -} - -impl SignatureStore for InMemorySignatureStore -where - TP::Target: TimeProvider, -{ - fn exists(&self, signature: &str) -> Result { - let recent_signatures = self.recent_signatures.lock().unwrap(); - for (stored_sig, _) in recent_signatures.iter() { - if stored_sig == signature { - return Ok(true); - } + fn check_for_replay_attack(&self, signature: &str) -> Result<(), LSPS5ClientError> { + let mut signatures = self.recent_signatures.lock().unwrap(); + if signatures.contains(&signature.to_string()) { + return Err(LSPS5ClientError::ReplayAttack); } - Ok(false) - } - - fn store(&self, signature: &str) -> Result<(), LSPS5ClientError> { - let now = - LSPSDateTime::new_from_duration_since_epoch(self.time_provider.duration_since_epoch()); - let mut recent_signatures = self.recent_signatures.lock().unwrap(); - - recent_signatures.push_back((signature.to_string(), now.clone())); - - let retention_secs = self.config.retention_minutes.as_secs(); - recent_signatures.retain(|(_, ts)| now.abs_diff(ts) <= retention_secs); - - if recent_signatures.len() > self.config.max_signatures { - let excess = recent_signatures.len() - self.config.max_signatures; - recent_signatures.drain(0..excess); + if signatures.len() == MAX_RECENT_SIGNATURES { + signatures.pop_back(); } + signatures.push_front(signature.to_string()); Ok(()) } } diff --git a/lightning-liquidity/tests/lsps5_integration_tests.rs b/lightning-liquidity/tests/lsps5_integration_tests.rs index 079bcf4acc5..aa85cf0a1d8 100644 --- a/lightning-liquidity/tests/lsps5_integration_tests.rs +++ b/lightning-liquidity/tests/lsps5_integration_tests.rs @@ -21,28 +21,20 @@ use lightning_liquidity::lsps5::service::LSPS5ServiceConfig; use lightning_liquidity::lsps5::service::{ MIN_WEBHOOK_RETENTION_DAYS, PRUNE_STALE_WEBHOOKS_INTERVAL_DAYS, }; -use lightning_liquidity::lsps5::validator::{ - InMemorySignatureStore, LSPS5Validator, SignatureStorageConfig, -}; +use lightning_liquidity::lsps5::validator::{LSPS5Validator, MAX_RECENT_SIGNATURES}; use lightning_liquidity::utils::time::{DefaultTimeProvider, TimeProvider}; use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; use std::sync::{Arc, RwLock}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::Duration; /// Default maximum number of webhooks allowed per client. pub(crate) const DEFAULT_MAX_WEBHOOKS_PER_CLIENT: u32 = 10; /// Default notification cooldown time in hours. pub(crate) const DEFAULT_NOTIFICATION_COOLDOWN_HOURS: Duration = Duration::from_secs(24 * 60 * 60); -type TestValidator = LSPS5Validator< - Arc, - Arc>>, ->; - pub(crate) fn lsps5_test_setup<'a, 'b, 'c>( nodes: Vec>, time_provider: Arc, - max_signatures: Option, -) -> (LSPSNodes<'a, 'b, 'c>, TestValidator) { +) -> (LSPSNodes<'a, 'b, 'c>, LSPS5Validator) { let lsps5_service_config = LSPS5ServiceConfig { max_webhooks_per_client: DEFAULT_MAX_WEBHOOKS_PER_CLIENT, notification_cooldown_hours: DEFAULT_NOTIFICATION_COOLDOWN_HOURS, @@ -70,14 +62,7 @@ pub(crate) fn lsps5_test_setup<'a, 'b, 'c>( Arc::clone(&time_provider), ); - let mut signature_config = SignatureStorageConfig::default(); - if let Some(max_signatures) = max_signatures { - signature_config.max_signatures = max_signatures; - } - - let signature_store = - Arc::new(InMemorySignatureStore::new(signature_config, Arc::clone(&time_provider))); - let validator = LSPS5Validator::new(time_provider, signature_store); + let validator = LSPS5Validator::new(); (lsps_nodes, validator) } @@ -95,11 +80,6 @@ impl MockTimeProvider { let mut time = self.current_time.write().unwrap(); *time += Duration::from_secs(seconds); } - - fn rewind_time(&self, seconds: u64) { - let mut time = self.current_time.write().unwrap(); - *time = time.checked_sub(Duration::from_secs(seconds)).unwrap_or_default(); - } } impl TimeProvider for MockTimeProvider { @@ -126,7 +106,7 @@ fn webhook_registration_flow() { let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let (lsps_nodes, _) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider), None); + let (lsps_nodes, _) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider)); let LSPSNodes { service_node, client_node } = lsps_nodes; let service_node_id = service_node.inner.node.get_our_node_id(); let client_node_id = client_node.inner.node.get_our_node_id(); @@ -316,7 +296,7 @@ fn webhook_error_handling_test() { let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let (lsps_nodes, _) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider), None); + let (lsps_nodes, _) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider)); let LSPSNodes { service_node, client_node } = lsps_nodes; let service_node_id = service_node.inner.node.get_our_node_id(); let client_node_id = client_node.inner.node.get_our_node_id(); @@ -440,7 +420,7 @@ fn webhook_notification_delivery_test() { let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let (lsps_nodes, validator) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider), None); + let (lsps_nodes, validator) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider)); let LSPSNodes { service_node, client_node } = lsps_nodes; let service_node_id = service_node.inner.node.get_our_node_id(); let client_node_id = client_node.inner.node.get_our_node_id(); @@ -548,7 +528,7 @@ fn multiple_webhooks_notification_test() { let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let (lsps_nodes, _) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider), None); + let (lsps_nodes, _) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider)); let LSPSNodes { service_node, client_node } = lsps_nodes; let service_node_id = service_node.inner.node.get_our_node_id(); let client_node_id = client_node.inner.node.get_our_node_id(); @@ -648,7 +628,7 @@ fn idempotency_set_webhook_test() { let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let (lsps_nodes, _) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider), None); + let (lsps_nodes, _) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider)); let LSPSNodes { service_node, client_node } = lsps_nodes; let service_node_id = service_node.inner.node.get_our_node_id(); let client_node_id = client_node.inner.node.get_our_node_id(); @@ -748,7 +728,7 @@ fn replay_prevention_test() { let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let (lsps_nodes, validator) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider), None); + let (lsps_nodes, validator) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider)); let LSPSNodes { service_node, client_node } = lsps_nodes; let service_node_id = service_node.inner.node.get_our_node_id(); let client_node_id = client_node.inner.node.get_our_node_id(); @@ -788,17 +768,42 @@ fn replay_prevention_test() { _ => panic!("Expected SendWebhookNotification event"), }; + // First validation should succeed let result = validator.validate(service_node_id, ×tamp, &signature, &body); assert!(result.is_ok(), "First verification should succeed"); - // Try again with same timestamp and signature (simulate replay attack) + // Replaying the same signature immediately should fail let replay_result = validator.validate(service_node_id, ×tamp, &signature, &body); + assert!(replay_result.is_err(), "Immediate replay attack should be detected"); + assert_eq!(replay_result.unwrap_err(), LSPS5ClientError::ReplayAttack); - // This should now fail since we've implemented replay prevention - assert!(replay_result.is_err(), "Replay attack should be detected and rejected"); + // Fill up the validator's signature cache to push out the original signature. + for i in 0..MAX_RECENT_SIGNATURES { + let timeout_block = 700000 + i as u32; + let _ = service_handler.notify_expiry_soon(client_node_id, timeout_block); + let event = service_node.liquidity_manager.next_event().unwrap(); + if let LiquidityEvent::LSPS5Service(LSPS5ServiceEvent::SendWebhookNotification { + headers, + notification, + .. + }) = event + { + let (ts, sig) = extract_ts_sig(&headers); + let res = validator.validate(service_node_id, &ts, &sig, ¬ification); + assert!(res.is_ok(), "Validation of unique signature #{} should succeed", i); + } else { + panic!("Expected SendWebhookNotification event"); + } + } - let err = replay_result.unwrap_err(); - assert_eq!(err, LSPS5ClientError::ReplayAttack); + // The original signature should now be evicted from the cache. Replaying it again should now succeed. + let replay_after_eviction_result = + validator.validate(service_node_id, ×tamp, &signature, &body); + + assert!( + replay_after_eviction_result.is_ok(), + "Replay attack should succeed after original signature is evicted from cache" + ); } #[test] @@ -809,7 +814,7 @@ fn stale_webhooks() { let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let (lsps_nodes, _) = lsps5_test_setup(nodes, time_provider, None); + let (lsps_nodes, _) = lsps5_test_setup(nodes, time_provider); let LSPSNodes { service_node, client_node } = lsps_nodes; let service_node_id = service_node.inner.node.get_our_node_id(); let client_node_id = client_node.inner.node.get_our_node_id(); @@ -875,7 +880,7 @@ fn test_all_notifications() { let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let (lsps_nodes, validator) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider), None); + let (lsps_nodes, validator) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider)); let LSPSNodes { service_node, client_node } = lsps_nodes; let service_node_id = service_node.inner.node.get_our_node_id(); let client_node_id = client_node.inner.node.get_our_node_id(); @@ -934,7 +939,7 @@ fn test_tampered_notification() { let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let (lsps_nodes, validator) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider), None); + let (lsps_nodes, validator) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider)); let LSPSNodes { service_node, client_node } = lsps_nodes; let service_node_id = service_node.inner.node.get_our_node_id(); let client_node_id = client_node.inner.node.get_our_node_id(); @@ -987,7 +992,7 @@ fn test_bad_signature_notification() { let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let (lsps_nodes, validator) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider), None); + let (lsps_nodes, validator) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider)); let LSPSNodes { service_node, client_node } = lsps_nodes; let service_node_id = service_node.inner.node.get_our_node_id(); let client_node_id = client_node.inner.node.get_our_node_id(); @@ -1029,87 +1034,13 @@ fn test_bad_signature_notification() { assert!(client_node.liquidity_manager.next_event().is_none()); } -#[test] -fn test_timestamp_notification_window_validation() { - let mock_time_provider = Arc::new(MockTimeProvider::new( - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time before Unix epoch") - .as_secs(), - )); - let time_provider = Arc::::clone(&mock_time_provider); - let chanmon_cfgs = create_chanmon_cfgs(2); - let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); - let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let (lsps_nodes, validator) = lsps5_test_setup(nodes, time_provider, None); - let LSPSNodes { service_node, client_node } = lsps_nodes; - let service_node_id = service_node.inner.node.get_our_node_id(); - let client_node_id = client_node.inner.node.get_our_node_id(); - - let client_handler = client_node.liquidity_manager.lsps5_client_handler().unwrap(); - let service_handler = service_node.liquidity_manager.lsps5_service_handler().unwrap(); - - let app_name = "OnionApp"; - let webhook_url = "https://www.example.org/onion"; - let _ = client_handler - .set_webhook(service_node_id, app_name.to_string(), webhook_url.to_string()) - .expect("Register webhook request should succeed"); - let set_req = get_lsps_message!(client_node, service_node_id); - service_node.liquidity_manager.handle_custom_message(set_req, client_node_id).unwrap(); - - // consume initial SendWebhookNotification - let _ = service_node.liquidity_manager.next_event().unwrap(); - - let _ = service_handler.notify_onion_message_incoming(client_node_id); - - let expected_method = WebhookNotificationMethod::LSPS5OnionMessageIncoming; - - let event = service_node.liquidity_manager.next_event().unwrap(); - if let LiquidityEvent::LSPS5Service(LSPS5ServiceEvent::SendWebhookNotification { - url, - notification, - headers, - .. - }) = event - { - assert_eq!(url.as_str(), webhook_url); - assert_eq!(notification.method, expected_method); - let (timestamp, signature) = extract_ts_sig(&headers); - - // 1) past timestamp (current time advanced) - mock_time_provider.advance_time(60 * 60); - let err_past = - validator.validate(service_node_id, ×tamp, &signature, ¬ification).unwrap_err(); - assert!( - matches!(err_past, LSPS5ClientError::InvalidTimestamp), - "Expected InvalidTimestamp error variant, got {:?}", - err_past - ); - - // 2) future timestamp - mock_time_provider.rewind_time(60 * 60 * 2); - let err_future = - validator.validate(service_node_id, ×tamp, &signature, ¬ification).unwrap_err(); - assert!( - matches!(err_future, LSPS5ClientError::InvalidTimestamp), - "Expected InvalidTimestamp error variant, got {:?}", - err_future - ); - } else { - panic!("Unexpected event: {:?}", event); - } - - assert!(client_node.liquidity_manager.next_event().is_none()); -} - #[test] fn test_notify_without_webhooks_does_nothing() { let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let (lsps_nodes, _) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider), Some(0)); + let (lsps_nodes, _) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider)); let LSPSNodes { service_node, client_node } = lsps_nodes; let client_node_id = client_node.inner.node.get_our_node_id(); @@ -1122,57 +1053,3 @@ fn test_notify_without_webhooks_does_nothing() { let _ = service_handler.notify_onion_message_incoming(client_node_id); assert!(service_node.liquidity_manager.next_event().is_none()); } - -#[test] -fn no_replay_error_when_signature_storage_is_disabled() { - let chanmon_cfgs = create_chanmon_cfgs(2); - let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); - let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let (lsps_nodes, validator) = lsps5_test_setup(nodes, Arc::new(DefaultTimeProvider), Some(0)); - let LSPSNodes { service_node, client_node } = lsps_nodes; - let service_node_id = service_node.inner.node.get_our_node_id(); - let client_node_id = client_node.inner.node.get_our_node_id(); - - let client_handler = client_node.liquidity_manager.lsps5_client_handler().unwrap(); - let service_handler = service_node.liquidity_manager.lsps5_service_handler().unwrap(); - - let app_name = "test app"; - let webhook_url = "https://www.example.org/webhook?token=replay123"; - - let _ = client_handler - .set_webhook(service_node_id, app_name.to_string(), webhook_url.to_string()) - .expect("Register webhook request should succeed"); - let request = get_lsps_message!(client_node, service_node_id); - service_node.liquidity_manager.handle_custom_message(request, client_node_id).unwrap(); - - // consume initial SendWebhookNotification - let _ = service_node.liquidity_manager.next_event().unwrap(); - - let response = get_lsps_message!(service_node, client_node_id); - client_node.liquidity_manager.handle_custom_message(response, service_node_id).unwrap(); - - let _ = client_node.liquidity_manager.next_event().unwrap(); - - let _ = service_handler.notify_payment_incoming(client_node_id); - - let notification_event = service_node.liquidity_manager.next_event().unwrap(); - let (timestamp, signature, body) = match notification_event { - LiquidityEvent::LSPS5Service(LSPS5ServiceEvent::SendWebhookNotification { - headers, - notification, - .. - }) => { - let (timestamp, signature) = extract_ts_sig(&headers); - (timestamp, signature, notification) - }, - _ => panic!("Expected SendWebhookNotification event"), - }; - - // max_signatures is set to 0, so there is no replay attack prevention - // and the same notification can be parsed multiple times without error - for _ in 0..4 { - let result = validator.validate(service_node_id, ×tamp, &signature, &body); - assert!(result.is_ok(), "Verification should succeed because storage is disabled"); - } -}