Skip to content

Commit 01abfe4

Browse files
committed
Improve webhook request validation and test coverage
1 parent 8053c92 commit 01abfe4

File tree

2 files changed

+65
-10
lines changed

2 files changed

+65
-10
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from datetime import datetime
2+
3+
import pytest
4+
5+
import lightspark
6+
7+
8+
class TestWebhooks:
9+
@pytest.mark.parametrize(
10+
"hex_digest",
11+
[
12+
pytest.param("62a8829aeb48b4142533520b1f7f86cdb1ee7d718bf3ea15bc1c662d4c453b74", id="lowercase"),
13+
pytest.param("62A8829AEB48B4142533520B1F7F86CDB1EE7D718BF3EA15BC1C662D4C453B74", id="uppercase"),
14+
],
15+
)
16+
def test_valid_data_parses(self, hex_digest: str):
17+
data = b'{"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"}'
18+
secret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX"
19+
20+
webhook = lightspark.WebhookEvent.verify_and_parse(data, hex_digest, secret)
21+
22+
assert (
23+
webhook.entity_id == "lightning_node:01882c25-157a-f96b-0000-362d42b64397"
24+
)
25+
assert webhook.event_id == "1615c8be5aa44e429eba700db2ed8ca5"
26+
assert webhook.event_type == lightspark.WebhookEventType.NODE_STATUS
27+
assert webhook.timestamp == datetime.fromisoformat(
28+
"2023-05-17T23:56:47.874449+00:00"
29+
)
30+
31+
@pytest.mark.parametrize(
32+
"hex_digest",
33+
[
34+
pytest.param("deadbeef", id="wrong length"),
35+
pytest.param("a" * 64, id="incorrect"),
36+
pytest.param("NotAHexString", id="not hex"),
37+
pytest.param(
38+
"62a8829aeb48b4142533520b1f7f86cdb1ee7d718bf3ea15bc1c662d4c453b74"
39+
+ "qq",
40+
id="extra bytes",
41+
),
42+
],
43+
)
44+
def test_invalid_data_raises_value_error(self, hex_digest: str):
45+
data = b'{"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"}'
46+
secret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX"
47+
48+
with pytest.raises(ValueError):
49+
lightspark.WebhookEvent.verify_and_parse(data, hex_digest, secret)
50+
51+
def test_invalid_data_type_raises_type_error(self):
52+
data = 1
53+
hex_digest = "deadbeef"
54+
secret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX"
55+
56+
with pytest.raises(TypeError):
57+
lightspark.WebhookEvent.verify_and_parse(data, hex_digest, secret) # type: ignore

lightspark/webhooks.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,14 @@ class WebhookEvent:
2222

2323
@classmethod
2424
def verify_and_parse(
25-
cls, data: bytes, hexdigest: str, webhook_secret: str
25+
cls, data: bytes, hex_digest: str, webhook_secret: str
2626
) -> "WebhookEvent":
27-
"""Verifies the signature and parses the message into a
28-
WebhookEvent object.
27+
"""Verifies the signature and parses the message into a WebhookEvent object.
2928
3029
Args:
3130
data: the POST message body received by the webhook.
32-
hexdigest: the message signature sent in the
33-
`lightspark-signature` header.
34-
webhook_secret: the webhook secret configured at the
35-
Lightspark API configuration.
31+
hex_digest: the message signature sent in the `lightspark-signature` header.
32+
webhook_secret: the webhook secret configured in the Lightspark API configuration.
3633
3734
Returns:
3835
A parsed WebhookEvent object.
@@ -43,10 +40,11 @@ def verify_and_parse(
4340
if not isinstance(data, bytes):
4441
raise TypeError(f"'data' should be bytes, got {type(data)}")
4542

46-
sig = hmac.new(
43+
mac = hmac.new(
4744
webhook_secret.encode("ascii"), msg=data, digestmod=hashlib.sha256
4845
)
49-
if sig.hexdigest().lower() != hexdigest.lower():
46+
47+
if not hmac.compare_digest(mac.digest(), bytes.fromhex(hex_digest)):
5048
raise ValueError("Webhook message hash does not match signature")
5149

5250
return cls.parse(data)
@@ -73,5 +71,5 @@ def parse(cls, data: bytes) -> "WebhookEvent":
7371
event_id=event["event_id"],
7472
timestamp=datetime.fromisoformat(event["timestamp"]),
7573
entity_id=event["entity_id"],
76-
wallet_id=event["wallet_id"] if "wallet_id" in event else None,
74+
wallet_id=event.get("wallet_id", None),
7775
)

0 commit comments

Comments
 (0)