Skip to content

Commit f3891eb

Browse files
fixup: split client into client and validator.
move all logic related to verifying notifications and storing signatures into a new validator module. Splitting client responsibilities creates a clearer, simpler API: webhook registration and management remain with the client handler (used by mobile apps), while notification validation moves to the validator module (used by server/proxy), ensuring notifications are validated before forwarding.
1 parent 3ce2a63 commit f3891eb

File tree

5 files changed

+283
-198
lines changed

5 files changed

+283
-198
lines changed

lightning-liquidity/src/lsps5/client.rs

Lines changed: 10 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,23 @@ use crate::lsps0::ser::{LSPSDateTime, LSPSMessage, LSPSProtocolMessageHandler, L
1515
use crate::lsps5::event::LSPS5ClientEvent;
1616
use crate::lsps5::msgs::{
1717
LSPS5Message, LSPS5Request, LSPS5Response, ListWebhooksRequest, RemoveWebhookRequest,
18-
SetWebhookRequest, WebhookNotification,
18+
SetWebhookRequest,
1919
};
2020

2121
use crate::message_queue::MessageQueue;
2222
use crate::prelude::{new_hash_map, HashMap};
2323
use crate::sync::{Arc, Mutex, RwLock};
2424
use crate::utils::generate_request_id;
2525

26-
use super::msgs::{LSPS5AppName, LSPS5ClientError, LSPS5Error, LSPS5WebhookUrl};
26+
use super::msgs::{LSPS5AppName, LSPS5Error, LSPS5WebhookUrl};
2727
use super::service::TimeProvider;
2828

2929
use bitcoin::secp256k1::PublicKey;
3030

3131
use lightning::ln::msgs::{ErrorAction, LightningError};
3232
use lightning::sign::EntropySource;
3333
use lightning::util::logger::Level;
34-
use lightning::util::message_signing;
3534

36-
use alloc::collections::VecDeque;
3735
use alloc::string::String;
3836

3937
use core::ops::Deref;
@@ -42,45 +40,16 @@ use core::time::Duration;
4240
/// Default maximum age in seconds for cached responses (1 hour).
4341
pub const DEFAULT_RESPONSE_MAX_AGE_SECS: u64 = 3600;
4442

45-
/// Default retention time for signatures in minutes (LSPS5 spec requires min 20 minutes).
46-
pub const DEFAULT_SIGNATURE_RETENTION_MINUTES: u64 = 20;
47-
48-
/// Default maximum number of stored signatures.
49-
pub const DEFAULT_MAX_SIGNATURES: usize = 1000;
50-
51-
/// Configuration for signature storage.
52-
#[derive(Clone, Copy, Debug)]
53-
pub struct SignatureStorageConfig {
54-
/// Maximum number of signatures to store.
55-
pub max_signatures: usize,
56-
/// Retention time for signatures in minutes.
57-
pub retention_minutes: Duration,
58-
}
59-
60-
impl Default for SignatureStorageConfig {
61-
fn default() -> Self {
62-
Self {
63-
max_signatures: DEFAULT_MAX_SIGNATURES,
64-
retention_minutes: Duration::from_secs(DEFAULT_SIGNATURE_RETENTION_MINUTES * 60),
65-
}
66-
}
67-
}
68-
6943
#[derive(Debug, Clone)]
7044
/// Configuration for the LSPS5 client
7145
pub struct LSPS5ClientConfig {
7246
/// Maximum age in seconds for cached responses (default: 3600 - 1 hour).
7347
pub response_max_age_secs: Duration,
74-
/// Configuration for signature storage.
75-
pub signature_config: SignatureStorageConfig,
7648
}
7749

7850
impl Default for LSPS5ClientConfig {
7951
fn default() -> Self {
80-
Self {
81-
response_max_age_secs: Duration::from_secs(DEFAULT_RESPONSE_MAX_AGE_SECS),
82-
signature_config: SignatureStorageConfig::default(),
83-
}
52+
Self { response_max_age_secs: Duration::from_secs(DEFAULT_RESPONSE_MAX_AGE_SECS) }
8453
}
8554
}
8655

@@ -141,20 +110,24 @@ where
141110
/// Client-side handler for the LSPS5 (bLIP-55) webhook registration protocol.
142111
///
143112
/// `LSPS5ClientHandler` is the primary interface for LSP clients
144-
/// to register, list, and remove webhook endpoints with an LSP, and to parse
145-
/// and validate incoming signed notifications.
113+
/// to register, list, and remove webhook endpoints with an LSP.
114+
///
115+
/// This handler is intended for use on the client-side (e.g., a mobile app)
116+
/// which has access to the node's keys and can send/receive peer messages.
117+
///
118+
/// For validating incoming webhook notifications on a server, see [`LSPS5Validator`].
146119
///
147120
/// # Core Capabilities
148121
///
149122
/// - `set_webhook(peer, app_name, url)` -> register or update a webhook [`lsps5.set_webhook`]
150123
/// - `list_webhooks(peer)` -> retrieve all registered webhooks [`lsps5.list_webhooks`]
151124
/// - `remove_webhook(peer, name)` -> delete a webhook [`lsps5.remove_webhook`]
152-
/// - `parse_webhook_notification(...)` -> verify signature, timestamp, replay, and emit event
153125
///
154126
/// [`bLIP-55 / LSPS5 specification`]: https://github.com/lightning/blips/pull/55/files
155127
/// [`lsps5.set_webhook`]: super::msgs::LSPS5Request::SetWebhook
156128
/// [`lsps5.list_webhooks`]: super::msgs::LSPS5Request::ListWebhooks
157129
/// [`lsps5.remove_webhook`]: super::msgs::LSPS5Request::RemoveWebhook
130+
/// [`LSPS5Validator`]: super::validator::LSPS5Validator
158131
pub struct LSPS5ClientHandler<ES: Deref, TP: Deref + Clone>
159132
where
160133
ES::Target: EntropySource,
@@ -166,7 +139,6 @@ where
166139
per_peer_state: RwLock<HashMap<PublicKey, Mutex<PeerState<TP>>>>,
167140
config: LSPS5ClientConfig,
168141
time_provider: TP,
169-
recent_signatures: Mutex<VecDeque<(String, LSPSDateTime)>>,
170142
}
171143

172144
impl<ES: Deref, TP: Deref + Clone> LSPS5ClientHandler<ES, TP>
@@ -179,15 +151,13 @@ where
179151
entropy_source: ES, pending_messages: Arc<MessageQueue>, pending_events: Arc<EventQueue>,
180152
config: LSPS5ClientConfig, time_provider: TP,
181153
) -> Self {
182-
let max_signatures = config.signature_config.max_signatures;
183154
Self {
184155
pending_messages,
185156
pending_events,
186157
entropy_source,
187158
per_peer_state: RwLock::new(new_hash_map()),
188159
config,
189160
time_provider,
190-
recent_signatures: Mutex::new(VecDeque::with_capacity(max_signatures)),
191161
}
192162
}
193163

@@ -446,93 +416,6 @@ where
446416
self.with_peer_state(*counterparty_node_id, handle_response);
447417
result
448418
}
449-
450-
fn verify_notification_signature(
451-
&self, counterparty_node_id: PublicKey, signature_timestamp: &LSPSDateTime,
452-
signature: &str, notification: &WebhookNotification,
453-
) -> Result<(), LSPS5ClientError> {
454-
let now =
455-
LSPSDateTime::new_from_duration_since_epoch(self.time_provider.duration_since_epoch());
456-
let diff = signature_timestamp.abs_diff(&now);
457-
const MAX_TIMESTAMP_DRIFT_SECS: u64 = 600;
458-
if diff > MAX_TIMESTAMP_DRIFT_SECS {
459-
return Err(LSPS5ClientError::InvalidTimestamp);
460-
}
461-
462-
let message = format!(
463-
"LSPS5: DO NOT SIGN THIS MESSAGE MANUALLY: LSP: At {} I notify {:?}",
464-
signature_timestamp.to_rfc3339(),
465-
notification
466-
);
467-
468-
if message_signing::verify(message.as_bytes(), signature, &counterparty_node_id) {
469-
Ok(())
470-
} else {
471-
Err(LSPS5ClientError::InvalidSignature)
472-
}
473-
}
474-
475-
fn check_signature_exists(&self, signature: &str) -> Result<(), LSPS5ClientError> {
476-
let recent_signatures = self.recent_signatures.lock().unwrap();
477-
478-
for (stored_sig, _) in recent_signatures.iter() {
479-
if stored_sig == signature {
480-
return Err(LSPS5ClientError::ReplayAttack);
481-
}
482-
}
483-
484-
Ok(())
485-
}
486-
487-
fn store_signature(&self, signature: String) {
488-
let now =
489-
LSPSDateTime::new_from_duration_since_epoch(self.time_provider.duration_since_epoch());
490-
let mut recent_signatures = self.recent_signatures.lock().unwrap();
491-
492-
recent_signatures.push_back((signature, now.clone()));
493-
494-
let retention_secs = self.config.signature_config.retention_minutes.as_secs();
495-
recent_signatures.retain(|(_, ts)| now.abs_diff(&ts) <= retention_secs);
496-
if recent_signatures.len() > self.config.signature_config.max_signatures {
497-
recent_signatures.truncate(self.config.signature_config.max_signatures);
498-
}
499-
}
500-
501-
/// Parse and validate a webhook notification received from an LSP.
502-
///
503-
/// Verifies the webhook delivery by parsing the notification JSON-RPC 2.0 format,
504-
/// checking the timestamp is within ±10 minutes, ensuring no signature replay within the retention window,
505-
/// and verifying the zbase32 LN-style signature against the LSP's node ID.
506-
///
507-
/// # Parameters
508-
/// - `counterparty_node_id`: the LSP's public key, used to verify the signature.
509-
/// - `timestamp`: ISO8601 time when the LSP created the notification.
510-
/// - `signature`: the zbase32-encoded LN signature over timestamp+body.
511-
/// - `notification`: the [`WebhookNotification`] received from the LSP.
512-
///
513-
/// Returns the validated [`WebhookNotification`] or an error for invalid timestamp,
514-
/// replay attack, or signature verification failure.
515-
///
516-
/// Call this method before processing any webhook notification to ensure authenticity.
517-
///
518-
/// [`WebhookNotification`]: super::msgs::WebhookNotification
519-
pub fn parse_webhook_notification(
520-
&self, counterparty_node_id: PublicKey, timestamp: &LSPSDateTime, signature: &str,
521-
notification: &WebhookNotification,
522-
) -> Result<WebhookNotification, LSPS5ClientError> {
523-
self.verify_notification_signature(
524-
counterparty_node_id,
525-
timestamp,
526-
signature,
527-
&notification,
528-
)?;
529-
530-
self.check_signature_exists(signature)?;
531-
532-
self.store_signature(signature.to_string());
533-
534-
Ok(notification.clone())
535-
}
536419
}
537420

538421
impl<ES: Deref, TP: Deref + Clone> LSPSProtocolMessageHandler for LSPS5ClientHandler<ES, TP>

lightning-liquidity/src/lsps5/event.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ pub enum LSPS5ServiceEvent {
3636
///
3737
/// The notification is signed using the LSP's node ID to ensure authenticity
3838
/// when received by the client. The client verifies this signature using
39-
/// [`parse_webhook_notification`], which guards against replay attacks and tampering.
39+
/// [`validate`], which guards against replay attacks and tampering.
4040
///
41-
/// [`parse_webhook_notification`]: super::client::LSPS5ClientHandler::parse_webhook_notification
41+
/// [`validate`]: super::validator::LSPS5Validator::validate
4242
/// [`notification_cooldown_hours`]: super::service::LSPS5ServiceConfig::notification_cooldown_hours
4343
/// [`url`]: super::msgs::LSPS5WebhookUrl
4444
/// [`notification`]: super::msgs::WebhookNotification

lightning-liquidity/src/lsps5/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ pub mod event;
2020
pub mod msgs;
2121
pub mod service;
2222
pub mod url_utils;
23+
pub mod validator;

0 commit comments

Comments
 (0)