@@ -15,7 +15,6 @@ use crate::alloc::string::ToString;
15
15
use crate :: lsps0:: ser:: LSPSDateTime ;
16
16
use crate :: lsps5:: msgs:: WebhookNotification ;
17
17
use crate :: sync:: Mutex ;
18
- use crate :: utils:: time:: TimeProvider ;
19
18
20
19
use lightning:: util:: message_signing;
21
20
@@ -24,32 +23,8 @@ use bitcoin::secp256k1::PublicKey;
24
23
use alloc:: collections:: VecDeque ;
25
24
use alloc:: string:: String ;
26
25
27
- use core:: ops:: Deref ;
28
- use core:: time:: Duration ;
29
-
30
- /// Configuration for signature storage.
31
- #[ derive( Clone , Copy , Debug ) ]
32
- pub struct SignatureStorageConfig {
33
- /// Maximum number of signatures to store.
34
- pub max_signatures : usize ,
35
- /// Retention time for signatures in minutes.
36
- pub retention_minutes : Duration ,
37
- }
38
-
39
- /// Default retention time for signatures in minutes (LSPS5 spec requires min 20 minutes).
40
- pub const DEFAULT_SIGNATURE_RETENTION_MINUTES : u64 = 20 ;
41
-
42
- /// Default maximum number of stored signatures.
43
- pub const DEFAULT_MAX_SIGNATURES : usize = 1000 ;
44
-
45
- impl Default for SignatureStorageConfig {
46
- fn default ( ) -> Self {
47
- Self {
48
- max_signatures : DEFAULT_MAX_SIGNATURES ,
49
- retention_minutes : Duration :: from_secs ( DEFAULT_SIGNATURE_RETENTION_MINUTES * 60 ) ,
50
- }
51
- }
52
- }
26
+ /// Maximum number of recent signatures to track for replay attack prevention.
27
+ pub const MAX_RECENT_SIGNATURES : usize = 5 ;
53
28
54
29
/// A utility for validating webhook notifications from an LSP.
55
30
///
@@ -60,66 +35,26 @@ impl Default for SignatureStorageConfig {
60
35
///
61
36
/// # Core Capabilities
62
37
///
63
- /// - `validate(...)` -> Verifies signature, timestamp, and protects against replay attacks.
64
- ///
65
- /// # Usage
38
+ /// - `validate(...)` -> Verifies signature, and protects against replay attacks.
66
39
///
67
- /// The validator requires a `SignatureStore` to track recently seen signatures
68
- /// to prevent replay attacks. You should create a single `LSPS5Validator` instance
69
- /// and share it across all requests.
40
+ /// The validator stores a [`small number`] of the most recently seen signatures
41
+ /// to protect against replays of the same notification.
70
42
///
43
+ /// [`small number`]: MAX_RECENT_SIGNATURES
71
44
/// [`bLIP-55 / LSPS5 specification`]: https://github.com/lightning/blips/pull/55/files
72
- pub struct LSPS5Validator < TP : Deref , SS : Deref >
73
- where
74
- TP :: Target : TimeProvider ,
75
- SS :: Target : SignatureStore ,
76
- {
77
- time_provider : TP ,
78
- signature_store : SS ,
45
+ pub struct LSPS5Validator {
46
+ recent_signatures : Mutex < VecDeque < String > > ,
79
47
}
80
48
81
- impl < TP : Deref , SS : Deref > LSPS5Validator < TP , SS >
82
- where
83
- TP :: Target : TimeProvider ,
84
- SS :: Target : SignatureStore ,
85
- {
86
- /// Creates a new `LSPS5Validator`.
87
- pub fn new ( time_provider : TP , signature_store : SS ) -> Self {
88
- Self { time_provider, signature_store }
89
- }
90
-
91
- fn verify_notification_signature (
92
- & self , counterparty_node_id : PublicKey , signature_timestamp : & LSPSDateTime ,
93
- signature : & str , notification : & WebhookNotification ,
94
- ) -> Result < ( ) , LSPS5ClientError > {
95
- let now =
96
- LSPSDateTime :: new_from_duration_since_epoch ( self . time_provider . duration_since_epoch ( ) ) ;
97
- let diff = signature_timestamp. abs_diff ( & now) ;
98
- const MAX_TIMESTAMP_DRIFT_SECS : u64 = 600 ;
99
- if diff > MAX_TIMESTAMP_DRIFT_SECS {
100
- return Err ( LSPS5ClientError :: InvalidTimestamp ) ;
101
- }
102
-
103
- let notification_json = serde_json:: to_string ( notification)
104
- . map_err ( |_| LSPS5ClientError :: SerializationError ) ?;
105
- let message = format ! (
106
- "LSPS5: DO NOT SIGN THIS MESSAGE MANUALLY: LSP: At {} I notify {}" ,
107
- signature_timestamp. to_rfc3339( ) ,
108
- notification_json
109
- ) ;
110
-
111
- if message_signing:: verify ( message. as_bytes ( ) , signature, & counterparty_node_id) {
112
- Ok ( ( ) )
113
- } else {
114
- Err ( LSPS5ClientError :: InvalidSignature )
115
- }
49
+ impl LSPS5Validator {
50
+ /// Create a new LSPS5Validator instance.
51
+ pub fn new ( ) -> Self {
52
+ Self { recent_signatures : Mutex :: new ( VecDeque :: with_capacity ( MAX_RECENT_SIGNATURES ) ) }
116
53
}
117
54
118
55
/// Parse and validate a webhook notification received from an LSP.
119
56
///
120
- /// Verifies the webhook delivery by checking the timestamp is within ±10 minutes,
121
- /// ensuring no signature replay within the retention window, and verifying the
122
- /// zbase32 LN-style signature against the LSP's node ID.
57
+ /// 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).
123
58
///
124
59
/// Call this method on your proxy/server before processing any webhook notification
125
60
/// to ensure its authenticity.
@@ -130,91 +65,40 @@ where
130
65
/// - `signature`: The zbase32-encoded LN signature over timestamp+body.
131
66
/// - `notification`: The [`WebhookNotification`] received from the LSP.
132
67
///
133
- /// Returns the validated [`WebhookNotification`] or an error for invalid timestamp,
134
- /// replay attack, or signature verification failure.
68
+ /// Returns the validated [`WebhookNotification`] or an error for signature verification failure or replay attack.
135
69
///
136
70
/// [`WebhookNotification`]: super::msgs::WebhookNotification
71
+ /// [`MAX_RECENT_SIGNATURES`]: MAX_RECENT_SIGNATURES
137
72
pub fn validate (
138
73
& self , counterparty_node_id : PublicKey , timestamp : & LSPSDateTime , signature : & str ,
139
74
notification : & WebhookNotification ,
140
75
) -> Result < WebhookNotification , LSPS5ClientError > {
141
- self . verify_notification_signature (
142
- counterparty_node_id,
143
- timestamp,
144
- signature,
145
- notification,
146
- ) ?;
76
+ let notification_json = serde_json:: to_string ( notification)
77
+ . map_err ( |_| LSPS5ClientError :: SerializationError ) ?;
78
+ let message = format ! (
79
+ "LSPS5: DO NOT SIGN THIS MESSAGE MANUALLY: LSP: At {} I notify {}" ,
80
+ timestamp. to_rfc3339( ) ,
81
+ notification_json
82
+ ) ;
147
83
148
- if self . signature_store . exists ( signature) ? {
149
- return Err ( LSPS5ClientError :: ReplayAttack ) ;
84
+ if !message_signing :: verify ( message . as_bytes ( ) , signature, & counterparty_node_id ) {
85
+ return Err ( LSPS5ClientError :: InvalidSignature ) ;
150
86
}
151
87
152
- self . signature_store . store ( signature) ?;
88
+ self . check_for_replay_attack ( signature) ?;
153
89
154
90
Ok ( notification. clone ( ) )
155
91
}
156
- }
157
-
158
- /// Trait for storing and checking webhook notification signatures to prevent replay attacks.
159
- pub trait SignatureStore {
160
- /// Checks if a signature already exists in the store.
161
- fn exists ( & self , signature : & str ) -> Result < bool , LSPS5ClientError > ;
162
- /// Stores a new signature.
163
- fn store ( & self , signature : & str ) -> Result < ( ) , LSPS5ClientError > ;
164
- }
165
-
166
- /// An in-memory store for webhook notification signatures.
167
- pub struct InMemorySignatureStore < TP : Deref >
168
- where
169
- TP :: Target : TimeProvider ,
170
- {
171
- recent_signatures : Mutex < VecDeque < ( String , LSPSDateTime ) > > ,
172
- config : SignatureStorageConfig ,
173
- time_provider : TP ,
174
- }
175
92
176
- impl < TP : Deref > InMemorySignatureStore < TP >
177
- where
178
- TP :: Target : TimeProvider ,
179
- {
180
- /// Creates a new `InMemorySignatureStore`.
181
- pub fn new ( config : SignatureStorageConfig , time_provider : TP ) -> Self {
182
- Self {
183
- recent_signatures : Mutex :: new ( VecDeque :: with_capacity ( config. max_signatures ) ) ,
184
- config,
185
- time_provider,
186
- }
187
- }
188
- }
189
-
190
- impl < TP : Deref > SignatureStore for InMemorySignatureStore < TP >
191
- where
192
- TP :: Target : TimeProvider ,
193
- {
194
- fn exists ( & self , signature : & str ) -> Result < bool , LSPS5ClientError > {
195
- let recent_signatures = self . recent_signatures . lock ( ) . unwrap ( ) ;
196
- for ( stored_sig, _) in recent_signatures. iter ( ) {
197
- if stored_sig == signature {
198
- return Ok ( true ) ;
199
- }
93
+ fn check_for_replay_attack ( & self , signature : & str ) -> Result < ( ) , LSPS5ClientError > {
94
+ let mut signatures = self . recent_signatures . lock ( ) . unwrap ( ) ;
95
+ if signatures. contains ( & signature. to_string ( ) ) {
96
+ return Err ( LSPS5ClientError :: ReplayAttack ) ;
200
97
}
201
- Ok ( false )
202
- }
203
-
204
- fn store ( & self , signature : & str ) -> Result < ( ) , LSPS5ClientError > {
205
- let now =
206
- LSPSDateTime :: new_from_duration_since_epoch ( self . time_provider . duration_since_epoch ( ) ) ;
207
- let mut recent_signatures = self . recent_signatures . lock ( ) . unwrap ( ) ;
208
-
209
- recent_signatures. push_back ( ( signature. to_string ( ) , now. clone ( ) ) ) ;
210
-
211
- let retention_secs = self . config . retention_minutes . as_secs ( ) ;
212
- recent_signatures. retain ( |( _, ts) | now. abs_diff ( ts) <= retention_secs) ;
213
-
214
- if recent_signatures. len ( ) > self . config . max_signatures {
215
- let excess = recent_signatures. len ( ) - self . config . max_signatures ;
216
- recent_signatures. drain ( 0 ..excess) ;
98
+ if signatures. len ( ) == MAX_RECENT_SIGNATURES {
99
+ signatures. pop_back ( ) ;
217
100
}
101
+ signatures. push_front ( signature. to_string ( ) ) ;
218
102
Ok ( ( ) )
219
103
}
220
104
}
0 commit comments