Reject case-varied LSPS5 replay signatures
What changed, and why it matters
This commit fixes a replay-protection bypass in LSPS5 webhook signature handling. The system uses zbase32-encoded signatures, which treat uppercase and lowercase letters as the same value. The replay cache, however, stored the raw signature string, so an attacker could resubmit the exact same webhook with only the letter case changed and bypass the replay check. The fix converts the signature to lowercase before checking or storing it in the cache, and a new test confirms the bypass is closed.
Review whether any other caches or equality checks in the LSPS5 validator or related modules compare raw zbase32 strings instead of canonicalized forms, and apply the same lowercase normalization where appropriate.
Security signals we found
Replay-attack bypass due to case-sensitive cache key for case-insensitive encoding
Canonicalization of cache key to match verifier's identity semantics
Regression test added for case-varied signature replay
Evidence from the diff
The LSPS5Validator::check_for_replay_attack method previously keyed its recent-signatures cache with signature.to_string(), preserving the exact header bytes. Because zbase32 decoding is case-insensitive, a case-varied signature decodes to the same bytes and passes cryptographic verification, yet was treated as a new cache entry. The patch canonicalizes the cache key by calling signature.to_ascii_lowercase() before lookup and insertion. A regression test in lsps5_integration_tests.rs submits the uppercase form of a previously seen signature and asserts that LSPS5ClientError::ReplayAttack is returned.
Changed components
lightning-liquidity/src/lsps5/validator.rslightning-liquidity/tests/lsps5_integration_tests.rsInspect captured patch +15 / −3
diff --git a/lightning-liquidity/src/lsps5/validator.rs b/lightning-liquidity/src/lsps5/validator.rs
index 8063ea7..50a36ea 100644
--- a/lightning-liquidity/src/lsps5/validator.rs
+++ b/lightning-liquidity/src/lsps5/validator.rs
@@ -11,7 +11,6 @@
use super::msgs::LSPS5ClientError;
-use crate::alloc::string::ToString;
use crate::lsps0::ser::LSPSDateTime;
use crate::lsps5::msgs::WebhookNotification;
use crate::sync::Mutex;
@@ -91,14 +90,17 @@ impl LSPS5Validator {
}
fn check_for_replay_attack(&self, signature: &str) -> Result<(), LSPS5ClientError> {
+ // zbase32 decoding accepts case aliases, so canonicalize the cache key
+ // to match verification semantics without decoding the signature again.
+ let signature = signature.to_ascii_lowercase();
let mut signatures = self.recent_signatures.lock().unwrap();
- if signatures.contains(&signature.to_string()) {
+ if signatures.contains(&signature) {
return Err(LSPS5ClientError::ReplayAttack);
}
if signatures.len() == MAX_RECENT_SIGNATURES {
signatures.pop_back();
}
- signatures.push_front(signature.to_string());
+ signatures.push_front(signature);
Ok(())
}
}
diff --git a/lightning-liquidity/tests/lsps5_integration_tests.rs b/lightning-liquidity/tests/lsps5_integration_tests.rs
index deed6b2..e4a0f89 100644
--- a/lightning-liquidity/tests/lsps5_integration_tests.rs
+++ b/lightning-liquidity/tests/lsps5_integration_tests.rs
@@ -988,6 +988,16 @@ fn replay_prevention_test() {
assert!(replay_result.is_err(), "Immediate replay attack should be detected");
assert_eq!(replay_result.unwrap_err(), LSPS5ClientError::ReplayAttack);
+ let case_modified_signature = signature.to_ascii_uppercase();
+ assert_ne!(case_modified_signature, signature);
+ let case_modified_replay_result =
+ validator.validate(service_node_id, ×tamp, &case_modified_signature, &body);
+ assert!(
+ case_modified_replay_result.is_err(),
+ "Immediate replay attack should be detected when the signature case changes"
+ );
+ assert_eq!(case_modified_replay_result.unwrap_err(), LSPS5ClientError::ReplayAttack);
+
// Fill up the validator's signature cache to push out the original signature.
for i in 0..MAX_RECENT_SIGNATURES {
// Advance time, allowing for another notification
Why this scored 50/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.