Skip to content

Commit b920cc7

Browse files
fixup: Refactor LSPS5 cryptographic operations to use Lightning's message_signing
- Replace custom cryptographic implementation with Lightning's built-in message_signing module - Make signature verification a static method that doesn't require instance state - Simplify webhook notification parsing and validation - Remove unnecessary imports related to cryptographic operations
1 parent 404ebfa commit b920cc7

File tree

2 files changed

+46
-88
lines changed

2 files changed

+46
-88
lines changed

lightning-liquidity/src/lsps5/client.rs

Lines changed: 13 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,9 @@ use crate::lsps5::msgs::{
1818
use crate::message_queue::MessageQueue;
1919
use crate::prelude::*;
2020

21-
use bitcoin::hashes::{sha256, Hash};
22-
use bitcoin::hex::{DisplayHex, FromHex};
23-
use bitcoin::secp256k1::{PublicKey, Secp256k1};
21+
use bitcoin::secp256k1::PublicKey;
2422
use lightning::ln::msgs::{ErrorAction, LightningError};
23+
use lightning::util::message_signing;
2524

2625
use crate::sync::{Arc, Mutex, RwLock};
2726
use core::ops::Deref;
@@ -501,7 +500,7 @@ where
501500
/// * On success: `true` if the signature is valid
502501
/// * On error: LightningError with error description
503502
pub fn verify_notification_signature(
504-
&self, counterparty_node_id: PublicKey, timestamp: &str, signature: &str,
503+
counterparty_node_id: PublicKey, timestamp: &str, signature: &str,
505504
notification: &WebhookNotification,
506505
) -> Result<bool, LightningError> {
507506
// Check timestamp format
@@ -540,91 +539,19 @@ where
540539
},
541540
};
542541

542+
// Create the message in the same format as used in sign_notification
543543
let message = format!(
544544
"LSPS5: DO NOT SIGN THIS MESSAGE MANUALLY: LSP: At {} I notify {}",
545545
timestamp, notification_json
546546
);
547547

548-
// Remove the "lspsig:" prefix if present
549-
let signature_data =
550-
if signature.starts_with("lspsig:") { &signature[7..] } else { signature };
551-
552-
// Decode the hex signature
553-
let signature_bytes: Vec<u8> = match FromHex::from_hex(signature_data) {
554-
Ok(bytes) => bytes,
555-
Err(e) => {
556-
return Err(LightningError {
557-
err: format!("Failed to decode signature from hex: {}", e),
558-
action: ErrorAction::IgnoreAndLog(Level::Error),
559-
});
560-
},
561-
};
562-
563-
if signature_bytes.len() != 65 {
564-
return Err(LightningError {
565-
err: format!("Invalid signature length: {}, expected 65", signature_bytes.len()),
566-
action: ErrorAction::IgnoreAndLog(Level::Error),
567-
});
568-
}
569-
570-
// Extract recovery ID and signature data
571-
let recovery_id =
572-
match bitcoin::secp256k1::ecdsa::RecoveryId::from_i32(signature_bytes[0] as i32) {
573-
Ok(id) => id,
574-
Err(e) => {
575-
return Err(LightningError {
576-
err: format!("Invalid recovery ID: {}", e),
577-
action: ErrorAction::IgnoreAndLog(Level::Error),
578-
});
579-
},
580-
};
581-
582-
// Create recoverable signature
583-
let mut sig_data = [0u8; 64];
584-
sig_data.copy_from_slice(&signature_bytes[1..65]);
585-
586-
let recoverable_sig = match bitcoin::secp256k1::ecdsa::RecoverableSignature::from_compact(
587-
&sig_data,
588-
recovery_id,
589-
) {
590-
Ok(sig) => sig,
591-
Err(e) => {
592-
return Err(LightningError {
593-
err: format!("Invalid recoverable signature: {}", e),
594-
action: ErrorAction::IgnoreAndLog(Level::Error),
595-
});
596-
},
597-
};
598-
599-
// Hash the message
600-
let message_hash = sha256::Hash::hash(message.as_bytes());
601-
let message_to_verify =
602-
match bitcoin::secp256k1::Message::from_digest_slice(message_hash.as_ref()) {
603-
Ok(msg) => msg,
604-
Err(e) => {
605-
return Err(LightningError {
606-
err: format!("Invalid message hash: {}", e),
607-
action: ErrorAction::IgnoreAndLog(Level::Error),
608-
});
609-
},
610-
};
611-
612-
// Verify signature and recover key
613-
let secp = Secp256k1::verification_only();
614-
match secp.recover_ecdsa(&message_to_verify, &recoverable_sig) {
615-
Ok(recovered_key) => {
616-
if recovered_key != counterparty_node_id {
617-
return Err(LightningError {
618-
err: "Recovered key does not match LSP key".to_string(),
619-
action: ErrorAction::IgnoreAndLog(Level::Error),
620-
});
621-
}
622-
Ok(true)
623-
},
624-
Err(e) => Err(LightningError {
625-
err: format!("Failed to recover key: {}", e),
548+
if message_signing::verify(message.as_bytes(), signature, &counterparty_node_id) {
549+
Ok(true)
550+
} else {
551+
Err(LightningError {
552+
err: "Invalid signature".to_string(),
626553
action: ErrorAction::IgnoreAndLog(Level::Error),
627-
}),
554+
})
628555
}
629556
}
630557

@@ -643,8 +570,7 @@ where
643570
/// * On success: The parsed webhook notification
644571
/// * On error: LightningError with error description
645572
pub fn parse_webhook_notification(
646-
&self, counterparty_node_id: PublicKey, timestamp: &str, signature: &str,
647-
notification_json: &str,
573+
counterparty_node_id: PublicKey, timestamp: &str, signature: &str, notification_json: &str,
648574
) -> Result<WebhookNotification, LightningError> {
649575
// Parse the notification JSON
650576
let notification: WebhookNotification = match serde_json::from_str(notification_json) {
@@ -657,7 +583,7 @@ where
657583
},
658584
};
659585
// Verify signature
660-
match self.verify_notification_signature(
586+
match Self::verify_notification_signature(
661587
counterparty_node_id,
662588
timestamp,
663589
signature,
@@ -689,7 +615,7 @@ mod tests {
689615
use crate::{
690616
lsps0::ser::LSPSRequestId, lsps5::msgs::SetWebhookResponse, tests::utils::TestEntropy,
691617
};
692-
use bitcoin::secp256k1::SecretKey;
618+
use bitcoin::{key::Secp256k1, secp256k1::SecretKey};
693619

694620
fn setup_test_client() -> (
695621
LSPS5ClientHandler<Arc<TestEntropy>>,

lightning-liquidity/src/lsps5/service.rs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use crate::prelude::*;
2424
use bitcoin::secp256k1::{PublicKey, SecretKey};
2525
use lightning::ln::msgs::{ErrorAction, LightningError};
2626
use lightning::util::logger::Level;
27+
use lightning::util::message_signing;
2728

2829
use crate::sync::{Arc, Mutex};
2930
use serde_json::json;
@@ -604,7 +605,7 @@ impl LSPS5ServiceHandler {
604605

605606
// Sign the notification using our utility function
606607
let signature_hex =
607-
sign_notification(&notification_json, &timestamp, &self.config.signing_key)?;
608+
Self::sign_notification(&notification_json, &timestamp, &self.config.signing_key)?;
608609

609610
// Store the signature to prevent replay attacks
610611
// According to spec: "MUST remember the signature for at least 20 minutes"
@@ -636,6 +637,37 @@ impl LSPS5ServiceHandler {
636637
Ok(())
637638
}
638639

640+
/// Sign a webhook notification with an LSP's signing key
641+
///
642+
/// This function takes a notification body and timestamp and returns a signature
643+
/// in the format required by the LSPS5 specification.
644+
///
645+
/// # Arguments
646+
///
647+
/// * `body` - The serialized notification JSON
648+
/// * `timestamp` - The ISO8601 timestamp string
649+
/// * `signing_key` - The LSP private key used for signing
650+
///
651+
/// # Returns
652+
///
653+
/// * The zbase32 encoded signature as specified in LSPS0, or an error if signing fails
654+
pub fn sign_notification(
655+
body: &str, timestamp: &str, signing_key: &SecretKey,
656+
) -> Result<String, LightningError> {
657+
// Create the message to sign
658+
// According to spec:
659+
// The message to be signed is: "LSPS5: DO NOT SIGN THIS MESSAGE MANUALLY: LSP: At {} I notify {}",
660+
let message = format!(
661+
"LSPS5: DO NOT SIGN THIS MESSAGE MANUALLY: LSP: At {} I notify {}",
662+
timestamp, body
663+
);
664+
665+
// Use the canonical message signing implementation from lightning::util::message_signing
666+
let signature = message_signing::sign(message.as_bytes(), signing_key);
667+
668+
Ok(signature)
669+
}
670+
639671
/// Store a signature with timestamp for replay attack prevention
640672
fn store_signature(&self, signature: String) {
641673
let mut recent_signatures = self.recent_signatures.lock().unwrap();

0 commit comments

Comments
 (0)