@@ -15,25 +15,23 @@ use crate::lsps0::ser::{LSPSDateTime, LSPSMessage, LSPSProtocolMessageHandler, L
1515use crate :: lsps5:: event:: LSPS5ClientEvent ;
1616use crate :: lsps5:: msgs:: {
1717 LSPS5Message , LSPS5Request , LSPS5Response , ListWebhooksRequest , RemoveWebhookRequest ,
18- SetWebhookRequest , WebhookNotification ,
18+ SetWebhookRequest ,
1919} ;
2020
2121use crate :: message_queue:: MessageQueue ;
2222use crate :: prelude:: { new_hash_map, HashMap } ;
2323use crate :: sync:: { Arc , Mutex , RwLock } ;
2424use crate :: utils:: generate_request_id;
2525
26- use super :: msgs:: { LSPS5AppName , LSPS5ClientError , LSPS5Error , LSPS5WebhookUrl } ;
26+ use super :: msgs:: { LSPS5AppName , LSPS5Error , LSPS5WebhookUrl } ;
2727use super :: service:: TimeProvider ;
2828
2929use bitcoin:: secp256k1:: PublicKey ;
3030
3131use lightning:: ln:: msgs:: { ErrorAction , LightningError } ;
3232use lightning:: sign:: EntropySource ;
3333use lightning:: util:: logger:: Level ;
34- use lightning:: util:: message_signing;
3534
36- use alloc:: collections:: VecDeque ;
3735use alloc:: string:: String ;
3836
3937use core:: ops:: Deref ;
@@ -42,45 +40,16 @@ use core::time::Duration;
4240/// Default maximum age in seconds for cached responses (1 hour).
4341pub 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
7145pub 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
7850impl 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
158131pub struct LSPS5ClientHandler < ES : Deref , TP : Deref + Clone >
159132where
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
172144impl < 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
538421impl < ES : Deref , TP : Deref + Clone > LSPSProtocolMessageHandler for LSPS5ClientHandler < ES , TP >
0 commit comments