Skip to content

Commit 404f5a7

Browse files
authored
Improve webhook request validation and test coverage (#65)
This PR improves our validation of webhook requests and adds tests for it.
1 parent 03f18c0 commit 404f5a7

File tree

3 files changed

+43
-34
lines changed

3 files changed

+43
-34
lines changed

lightspark/Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,13 @@ client = ["base", "objects", "dep:reqwest"]
2121

2222
[dependencies]
2323
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls", "zstd"], optional = true }
24-
futures = "0.3"
2524
base64 = "0.21.0"
2625
serde_json = "1.0.94"
2726
serde = { version = "1.0.155", features = ["derive"] }
2827
regex = "1.7.1"
2928
chrono = "0.4.35"
3029
aes-gcm = "0.10.1"
3130
rand = "0.8.5"
32-
block-modes = "0.9.1"
3331
os-version = "0.2.0"
3432
version_check = "0.9.4"
3533
hex = "0.4.3"

lightspark/src/objects/webhook_event_type.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use serde_json::Value;
44
use std::fmt;
55

66
/// This is an enum of the potential event types that can be associated with your Lightspark wallets.
7-
#[derive(Debug, Clone, Deserialize, Serialize)]
7+
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
88
pub enum WebhookEventType {
99
#[serde(rename = "PAYMENT_FINISHED")]
1010
PaymentFinished,

lightspark/src/webhooks.rs

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use chrono::{DateTime, Utc};
99

1010
pub const SIGNATURE_HEADER: &str = "lightspark-signature";
1111

12-
#[derive(Clone, Serialize, Deserialize)]
12+
#[derive(Debug, Clone, Serialize, Deserialize)]
1313
pub struct WebhookEvent {
1414
pub event_type: WebhookEventType,
1515
pub event_id: String,
@@ -24,46 +24,36 @@ pub struct WebhookEvent {
2424
impl WebhookEvent {
2525
pub fn verify_and_parse(
2626
data: &[u8],
27-
hexdigest: &str,
27+
hex_digest: &str,
2828
webhook_secret: &str,
2929
) -> Result<WebhookEvent, Error> {
30-
let mut hmac: Hmac<Sha256> =
31-
Hmac::new_from_slice(webhook_secret.as_bytes()).expect("HMAC can take key of any size");
32-
hmac.update(data);
33-
let result = hmac.finalize();
34-
let code_bytes = result.into_bytes();
35-
let hex_string = hex::encode(code_bytes);
36-
if !hex_string
37-
.to_ascii_lowercase()
38-
.eq(&hexdigest.to_ascii_lowercase())
39-
{
40-
return Err(Error::WebhookSignatureError);
41-
}
42-
Self::parse(data)
43-
}
30+
if let Ok(digest_bytes) = hex::decode(hex_digest) {
31+
let hmac: Hmac<Sha256> = Hmac::new_from_slice(webhook_secret.as_bytes())
32+
.expect("HMAC can take key of any size")
33+
.chain_update(data);
4434

45-
fn parse(data: &[u8]) -> Result<WebhookEvent, Error> {
46-
let event: WebhookEvent = serde_json::from_slice(data).map_err(Error::JsonError)?;
47-
Ok(event)
35+
if hmac.verify_slice(digest_bytes.as_slice()).is_ok() {
36+
return serde_json::from_slice(data).map_err(Error::JsonError);
37+
}
38+
}
39+
Err(Error::WebhookSignatureError)
4840
}
4941
}
5042

5143
#[cfg(test)]
5244
mod tests {
45+
5346
#[test]
5447
fn test_verify_and_parse() {
5548
let data = "{\"event_type\": \"NODE_STATUS\", \"event_id\": \"1615c8be5aa44e429eba700db2ed8ca5\", \"timestamp\": \"2023-05-17T23:56:47.874449+00:00\", \"entity_id\": \"lightning_node:01882c25-157a-f96b-0000-362d42b64397\"}";
56-
let hexdigest = "62a8829aeb48b4142533520b1f7f86cdb1ee7d718bf3ea15bc1c662d4c453b74";
49+
let hex_digest = "62a8829aeb48b4142533520b1f7f86cdb1ee7d718bf3ea15bc1c662d4c453b74";
5750
let webhook_secret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX";
5851

5952
let result =
60-
super::WebhookEvent::verify_and_parse(data.as_bytes(), hexdigest, webhook_secret)
53+
super::WebhookEvent::verify_and_parse(data.as_bytes(), hex_digest, webhook_secret)
6154
.expect("Success case");
6255

63-
assert_eq!(
64-
result.event_type.to_string(),
65-
super::WebhookEventType::NodeStatus.to_string()
66-
);
56+
assert_eq!(result.event_type, super::WebhookEventType::NodeStatus);
6757
assert_eq!(result.event_id, "1615c8be5aa44e429eba700db2ed8ca5");
6858
assert_eq!(
6959
result.entity_id,
@@ -75,20 +65,41 @@ mod tests {
7565
);
7666
}
7767

68+
#[test]
69+
fn test_invalid_signature() {
70+
let data = "{\"event_type\": \"NODE_STATUS\", \"event_id\": \"1615c8be5aa44e429eba700db2ed8ca5\", \"timestamp\": \"2023-05-17T23:56:47.874449+00:00\", \"entity_id\": \"lightning_node:01882c25-157a-f96b-0000-362d42b64397\"}";
71+
let hex_digest = "deadbeef";
72+
let webhook_secret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX";
73+
74+
let result =
75+
super::WebhookEvent::verify_and_parse(data.as_bytes(), hex_digest, webhook_secret);
76+
77+
assert!(matches!(result, Err(super::Error::WebhookSignatureError)));
78+
}
79+
80+
#[test]
81+
fn test_invalid_digest_bytes() {
82+
let data = "{\"event_type\": \"NODE_STATUS\", \"event_id\": \"1615c8be5aa44e429eba700db2ed8ca5\", \"timestamp\": \"2023-05-17T23:56:47.874449+00:00\", \"entity_id\": \"lightning_node:01882c25-157a-f96b-0000-362d42b64397\"}";
83+
let hex_digest = "NotAHexDigest";
84+
let webhook_secret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX";
85+
86+
let result =
87+
super::WebhookEvent::verify_and_parse(data.as_bytes(), hex_digest, webhook_secret);
88+
89+
assert!(matches!(result, Err(super::Error::WebhookSignatureError)));
90+
}
91+
7892
#[test]
7993
fn test_remote_signing_webhook() {
8094
let data = "{\"event_type\": \"REMOTE_SIGNING\", \"event_id\": \"1615c8be5aa44e429eba700db2ed8ca5\", \"timestamp\": \"2023-05-17T23:56:47.874449+00:00\", \"entity_id\": \"lightning_node:01882c25-157a-f96b-0000-362d42b64397\", \"data\": {\"sub_event_type\": \"ECDH\", \"public_key\": \"027c4b09ffb985c298afe7e5813266cbfcb7780b480ac294b0b43dc21f2be3d13c\"}}";
81-
let hexdigest = "17db38526ce47682f4052e3182766fe2f23810ac538e32d5f20bbe1deb2e3519";
95+
let hex_digest = "17db38526ce47682f4052e3182766fe2f23810ac538e32d5f20bbe1deb2e3519";
8296
let webhook_secret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX";
8397

8498
let result =
85-
super::WebhookEvent::verify_and_parse(data.as_bytes(), hexdigest, webhook_secret)
99+
super::WebhookEvent::verify_and_parse(data.as_bytes(), hex_digest, webhook_secret)
86100
.expect("Success case");
87101

88-
assert_eq!(
89-
result.event_type.to_string(),
90-
super::WebhookEventType::RemoteSigning.to_string()
91-
);
102+
assert_eq!(result.event_type, super::WebhookEventType::RemoteSigning);
92103

93104
let data = result.data.expect("Data is missing");
94105
let sub_event_type = data["sub_event_type"]

0 commit comments

Comments
 (0)