Skip to content

Commit 80ff093

Browse files
authored
Merge pull request #241 from lightsparkdev/push-uzzwoustyzlt
Improve webhook request validation and test coverage
2 parents eecfe5f + daf8861 commit 80ff093

File tree

3 files changed

+53
-44
lines changed

3 files changed

+53
-44
lines changed

lightspark-sdk/src/commonMain/kotlin/com/lightspark/sdk/webhooks/Webhooks.kt

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package com.lightspark.sdk.webhooks
55
import com.lightspark.sdk.core.LightsparkException
66
import com.lightspark.sdk.model.WebhookEventType
77
import com.lightspark.sdk.util.serializerFormat
8+
import java.security.MessageDigest
89
import javax.crypto.Mac
910
import javax.crypto.spec.SecretKeySpec
1011
import kotlinx.datetime.Instant
@@ -39,18 +40,24 @@ const val SIGNATURE_HEADER = "lightspark-signature"
3940

4041
@OptIn(ExperimentalStdlibApi::class)
4142
@Throws(LightsparkException::class)
42-
fun verifyAndParseWebhook(
43-
data: ByteArray,
44-
hexDigest: String,
45-
webhookSecret: String,
46-
): WebhookEvent {
43+
fun verifyAndParseWebhook(data: ByteArray, hexDigest: String, webhookSecret: String): WebhookEvent {
4744
val hmac = Mac.getInstance("HmacSHA256")
4845
val secretKey = SecretKeySpec(webhookSecret.encodeToByteArray(), "HmacSHA256")
4946
hmac.init(secretKey)
5047
hmac.update(data)
5148
val signature = hmac.doFinal()
52-
val verified = signature.contentEquals(hexDigest.hexToByteArray())
53-
if (!verified) {
49+
50+
val digestBytes =
51+
try {
52+
hexDigest.hexToByteArray()
53+
} catch (_ : IllegalArgumentException) {
54+
throw LightsparkException(
55+
"Webhook signature verification failed. Invalid message signature format.",
56+
"webhook_signature_verification_failed",
57+
)
58+
}
59+
60+
if (!MessageDigest.isEqual(signature, digestBytes)) {
5461
throw LightsparkException("Webhook signature verification failed", "webhook_signature_verification_failed")
5562
}
5663
return parseWebhook(data)

lightspark-sdk/src/commonTest/kotlin/com/lightspark/sdk/webhooks/WebhookTests.kt

Lines changed: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,82 +3,85 @@ package com.lightspark.sdk.webhooks
33
import com.lightspark.sdk.core.LightsparkException
44
import com.lightspark.sdk.model.WebhookEventType
55
import kotlin.test.Test
6+
import kotlin.test.assertContains
67
import kotlin.test.assertEquals
7-
import kotlin.test.assertNotNull
8+
import kotlin.test.assertFailsWith
89
import kotlin.test.assertNull
9-
import kotlin.test.assertTrue
10-
import kotlin.test.fail
1110
import kotlinx.coroutines.ExperimentalCoroutinesApi
1211
import kotlinx.coroutines.test.runTest
1312
import kotlinx.datetime.Instant
13+
import kotlinx.serialization.ExperimentalSerializationApi
14+
import kotlinx.serialization.MissingFieldException
1415
import kotlinx.serialization.json.jsonPrimitive
1516

1617
@OptIn(ExperimentalCoroutinesApi::class)
1718
class WebhookTests {
1819
@Test
1920
fun `test valid verifyAndParse`() = runTest {
2021
val eventType = WebhookEventType.NODE_STATUS
21-
val eventId = "1615c8be5aa44e429eba700db2ed8ca5"
22-
val entityId = "lightning_node:01882c25-157a-f96b-0000-362d42b64397"
23-
val timestamp = Instant.parse("2023-05-17T23:56:47.874449+00:00")
2422
val 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"}"""
25-
val hexdigest = "62a8829aeb48b4142533520b1f7f86cdb1ee7d718bf3ea15bc1c662d4c453b74"
23+
val hexDigest = "62a8829aeb48b4142533520b1f7f86cdb1ee7d718bf3ea15bc1c662d4c453b74"
2624
val webhookSecret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX"
2725

28-
val event = verifyAndParseWebhook(data.encodeToByteArray(), hexdigest, webhookSecret)
26+
val event = verifyAndParseWebhook(data.encodeToByteArray(), hexDigest, webhookSecret)
2927
assertEquals(eventType, event.eventType)
30-
assertEquals(eventId, event.eventId)
31-
assertEquals(entityId, event.entityId)
32-
assertEquals(timestamp, event.timestamp)
28+
assertEquals("1615c8be5aa44e429eba700db2ed8ca5", event.eventId)
29+
assertEquals("lightning_node:01882c25-157a-f96b-0000-362d42b64397", event.entityId)
30+
assertEquals(Instant.parse("2023-05-17T23:56:47.874449+00:00"), event.timestamp)
3331
assertNull(event.walletId)
3432
assertNull(event.data)
3533
}
3634

3735
@Test
3836
fun `test invalid signature`() = runTest {
3937
val 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"}"""
40-
val hexdigest = "deadbeef"
38+
val hexDigest = "deadbeef"
4139
val webhookSecret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX"
4240

43-
try {
44-
verifyAndParseWebhook(data.encodeToByteArray(), hexdigest, webhookSecret)
45-
fail("Expected LightsparkException")
46-
} catch (e: LightsparkException) {
47-
assertEquals("webhook_signature_verification_failed", e.errorCode)
48-
}
41+
val err = assertFailsWith<LightsparkException> {
42+
verifyAndParseWebhook(data.encodeToByteArray(), hexDigest, webhookSecret)
43+
}
44+
assertEquals("webhook_signature_verification_failed", err.errorCode)
45+
}
46+
47+
@Test
48+
fun `test invalid digest bytes`() = runTest {
49+
val 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"}"""
50+
val webhookSecret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX"
51+
val hexDigest = "NotAHexDigest"
52+
53+
val err = assertFailsWith<LightsparkException> {
54+
verifyAndParseWebhook(data.encodeToByteArray(), hexDigest, webhookSecret)
55+
}
56+
assertEquals("webhook_signature_verification_failed", err.errorCode)
4957
}
5058

5159
@Test
60+
@OptIn(ExperimentalSerializationApi::class)
5261
fun `test invalid json structure`() = runTest {
5362
val data = """{"event_typeeee": "NODE_STATUS", "event_id": "1615c8be5aa44e429eba700db2ed8ca5", "timestamp": "2023-05-17T23:56:47.874449+00:00", "entity_id": "lightning_node:01882c25-157a-f96b-0000-362d42b64397"}"""
54-
val hexdigest = "4c4232ea3cccf8d40f56f873ef3a353ad8c80f2e6ea3404197d08c4d46274bf4"
63+
val hexDigest = "4c4232ea3cccf8d40f56f873ef3a353ad8c80f2e6ea3404197d08c4d46274bf4"
5564
val webhookSecret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX"
5665

57-
try {
58-
verifyAndParseWebhook(data.encodeToByteArray(), hexdigest, webhookSecret)
59-
fail("Expected Exception")
60-
} catch (e: Exception) {
61-
assertTrue(e.message!!.contains("event_type"))
66+
val err = assertFailsWith<MissingFieldException> {
67+
verifyAndParseWebhook(data.encodeToByteArray(), hexDigest, webhookSecret)
6268
}
69+
assertContains(err.message!!, "event_type")
6370
}
6471

6572
@Test
6673
fun `test valid verifyAndParse with wallet`() = runTest {
6774
val eventType = WebhookEventType.WALLET_INCOMING_PAYMENT_FINISHED
68-
val eventId = "1615c8be5aa44e429eba700db2ed8ca5"
69-
val entityId = "lightning_node:01882c25-157a-f96b-0000-362d42b64397"
70-
val walletId = "wallet:01882c25-157a-f96b-0000-362d42b64397"
71-
val timestamp = Instant.parse("2023-05-17T23:56:47.874449+00:00")
7275
val data = """{"event_type": "WALLET_INCOMING_PAYMENT_FINISHED", "event_id": "1615c8be5aa44e429eba700db2ed8ca5", "timestamp": "2023-05-17T23:56:47.874449+00:00", "entity_id": "lightning_node:01882c25-157a-f96b-0000-362d42b64397", "wallet_id": "wallet:01882c25-157a-f96b-0000-362d42b64397" }"""
73-
val hexdigest = "b4eeb95f18956b3c33b99e9effc61636effc4634f83604cb41de13470c42669a"
76+
val hexDigest = "b4eeb95f18956b3c33b99e9effc61636effc4634f83604cb41de13470c42669a"
7477
val webhookSecret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX"
7578

76-
val event = verifyAndParseWebhook(data.encodeToByteArray(), hexdigest, webhookSecret)
79+
val event = verifyAndParseWebhook(data.encodeToByteArray(), hexDigest, webhookSecret)
7780
assertEquals(eventType, event.eventType)
78-
assertEquals(eventId, event.eventId)
79-
assertEquals(entityId, event.entityId)
80-
assertEquals(walletId, event.walletId)
81-
assertEquals(timestamp, event.timestamp)
81+
assertEquals("1615c8be5aa44e429eba700db2ed8ca5", event.eventId)
82+
assertEquals("lightning_node:01882c25-157a-f96b-0000-362d42b64397", event.entityId)
83+
assertEquals("wallet:01882c25-157a-f96b-0000-362d42b64397", event.walletId)
84+
assertEquals(Instant.parse("2023-05-17T23:56:47.874449+00:00"), event.timestamp)
8285
assertNull(event.data)
8386
}
8487

@@ -87,7 +90,6 @@ class WebhookTests {
8790
val data = """{"event_type": "REMOTE_SIGNING", "event_id": "8be9c360a68e420b9126b43ff6007a32", "timestamp": "2023-08-10T02:14:27.559234+00:00", "entity_id": "node_with_server_signing:0189d6bc-558d-88df-0000-502f04e71816", "data": {"sub_event_type": "GET_PER_COMMITMENT_POINT", "bitcoin_network": "TESTNET", "derivation_path": "m/3/2104864975", "per_commitment_point_idx": 281474976710654}}"""
8891
val event = parseWebhook(data.encodeToByteArray())
8992
assertEquals(WebhookEventType.REMOTE_SIGNING, event.eventType)
90-
assertNotNull(event.data)
9193
assertEquals("GET_PER_COMMITMENT_POINT", event.data!!["sub_event_type"]?.jsonPrimitive?.content)
9294
}
9395
}

remotesignerdemo/src/main/kotlin/com/lightspark/WebhookHandler.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ suspend fun handleWebhookRequest(
2222
val webhookEvent = try {
2323
val bodyBytes = call.receiveText().toByteArray()
2424
verifyAndParseWebhook(bodyBytes, signature, config.webhookSecret)
25-
} catch (e: Exception) {
25+
} catch (_: Exception) {
2626
call.respond(HttpStatusCode.BadRequest, "Invalid webhook request.")
2727
return "Invalid webhook request or bad signature."
2828
}
2929

3030
val response = when (webhookEvent.eventType) {
3131
WebhookEventType.REMOTE_SIGNING -> try {
3232
handleRemoteSigningEvent(client, webhookEvent, config.masterSeed)
33-
} catch (e: Exception) {
33+
} catch (_: Exception) {
3434
call.respond(HttpStatusCode.InternalServerError, "Error handling remote signing event.")
3535
return "Error handling remote signing event."
3636
}

0 commit comments

Comments
 (0)