Skip to content

Commit 7aac87e

Browse files
github-actions[bot]pengyingmatthappensalexjweil
authored
fix: updating the webhook signature verification to use constant time check
## (#484) * fix: updating the webhook signature verification to use constant time check (#20617) ## Reason Improve the security of webhook verification by using a constant-time comparison function to prevent timing attacks. ## Overview This PR updates the `verifyAndParseWebhook` function to use Node's `timingSafeEqual` method instead of a simple string comparison when verifying webhook signatures. The implementation now: 1. Computes the expected HMAC digest as a byte array 2. Converts the provided hexadecimal signature to bytes using the imported `hexToBytes` function 3. Compares the two byte arrays using the timing-safe comparison function ## Test Plan Tested the updated webhook verification with both valid and invalid signatures to ensure it correctly accepts legitimate webhooks and rejects those with invalid signatures. GitOrigin-RevId: 24d850239cf56a8f69b6de30158b8031c5e9b47f * chore:adding changeset * indicate slow payout status to bridge users (#20607) ## Reason closes PX-863 Display "slow delivery" status to bridge users ## changes * add RECEIVE_IBAN_SLOW account status * display specific strings in uma card * refactor the ReceiveIbanFailedModal to also show slow payout ![Screenshot 2025-09-21 at 3.23.07 PM.png](https://app.graphite.dev/user-attachments/assets/58191d25-f3e3-4aae-a377-4a0cc51f49b2.png) ![Screenshot 2025-09-21 at 3.23.24 PM.png](https://app.graphite.dev/user-attachments/assets/6df9b620-228e-44a5-ad43-515ce27db38b.png) GitOrigin-RevId: fa02843c3e6e8363a93586028ce1b22c12dcb89e * Improve JS webhook request validation and test coverage (#20624) This PR improves our JS validation of webhook requests and adds tests for it. GitOrigin-RevId: 41ade8e18cb680c8f5d6e94bcd669bb596f289dd * chore:adding changeset --------- Co-authored-by: Peng Ying <[email protected]> Co-authored-by: Matt Davis <[email protected]> Co-authored-by: Alex Weil <[email protected]>
1 parent 40d3786 commit 7aac87e

File tree

6 files changed

+108
-17
lines changed

6 files changed

+108
-17
lines changed

.changeset/full-glasses-guess.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@lightsparkdev/lightspark-sdk": patch
3+
---
4+
5+
Changing webhook signature verification to use a constant time comparison

.changeset/itchy-trees-spend.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@lightsparkdev/ui": patch
3+
---
4+
5+
indicate slow payout status to bridge users
Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,60 @@
1+
import { expect } from "@jest/globals";
12
import WebhookEventType from "../objects/WebhookEventType.js";
23
import { verifyAndParseWebhook } from "../webhooks.js";
34

45
describe("Webhooks", () => {
56
test("should verify and parse webhook data", async () => {
6-
const eventType = WebhookEventType.NODE_STATUS;
7-
const eventId = "1615c8be5aa44e429eba700db2ed8ca5";
8-
const entityId = "lightning_node:01882c25-157a-f96b-0000-362d42b64397";
9-
const timeStamp = new Date("2023-05-17T23:56:47.874449+00:00");
107
const 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"}`;
11-
const hexdigest =
8+
const hexDigest =
129
"62a8829aeb48b4142533520b1f7f86cdb1ee7d718bf3ea15bc1c662d4c453b74";
1310
const webhookSecret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX";
1411

1512
const webhook = await verifyAndParseWebhook(
16-
Buffer.from(data, "utf-8"),
17-
hexdigest,
13+
Buffer.from(data),
14+
hexDigest,
1815
webhookSecret,
1916
);
2017

21-
expect(webhook.entity_id).toBe(entityId);
22-
expect(webhook.event_id).toBe(eventId);
23-
expect(webhook.event_type).toBe(eventType);
24-
expect(webhook.timestamp).toEqual(timeStamp);
18+
expect(webhook.entity_id).toBe(
19+
"lightning_node:01882c25-157a-f96b-0000-362d42b64397",
20+
);
21+
expect(webhook.event_id).toBe("1615c8be5aa44e429eba700db2ed8ca5");
22+
expect(webhook.event_type).toBe(WebhookEventType.NODE_STATUS);
23+
expect(webhook.timestamp).toEqual(
24+
new Date("2023-05-17T23:56:47.874449+00:00"),
25+
);
26+
});
27+
28+
test.each([
29+
["wrong length", "deadbeef"],
30+
["is incorrect", "a".repeat(64)],
31+
["is not hex", "NotAHexValue"],
32+
[
33+
"has extra bytes",
34+
"62a8829aeb48b4142533520b1f7f86cdb1ee7d718bf3ea15bc1c662d4c453b74" + "qq",
35+
],
36+
])("should error when hex digest %s", async (name, digest) => {
37+
const 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"}`;
38+
const webhookSecret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX";
39+
40+
expect.assertions(1);
41+
await expect(
42+
verifyAndParseWebhook(Buffer.from(data), digest, webhookSecret),
43+
).rejects.toThrow("Webhook message hash does not match signature");
44+
});
45+
46+
test("should throw on invalid signature", async () => {
47+
const 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"}`;
48+
const invalidHex =
49+
"62a8829aeb48b4142533520b1f7f86cdb1ee7d718bf3ea15bc1c662d4c453b70";
50+
const webhookSecret = "3gZ5oQQUASYmqQNuEk0KambNMVkOADDItIJjzUlAWjX";
51+
52+
await expect(
53+
verifyAndParseWebhook(
54+
new TextEncoder().encode(data),
55+
invalidHex,
56+
webhookSecret,
57+
),
58+
).rejects.toThrow("Webhook message hash does not match signature");
2559
});
2660
});

packages/lightspark-sdk/src/webhooks.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,24 @@ export interface WebhookEvent {
1414

1515
export const verifyAndParseWebhook = async (
1616
data: Uint8Array,
17-
hexdigest: string,
18-
webhook_secret: string,
17+
hexDigest: string,
18+
webhookSecret: string,
1919
): Promise<WebhookEvent> => {
2020
/* dynamic import to avoid bundling crypto in browser */
21-
const { createHmac } = await import("crypto");
22-
const sig = createHmac("sha256", webhook_secret).update(data).digest("hex");
23-
24-
if (sig.toLowerCase() !== hexdigest.toLowerCase()) {
21+
const { createHmac, timingSafeEqual } = await import("crypto");
22+
const sig = new Uint8Array(
23+
createHmac("sha256", webhookSecret).update(data).digest(),
24+
);
25+
26+
const digestBytes = new Uint8Array(Buffer.from(hexDigest, "hex"));
27+
if (
28+
// Ensure there are no extra chars, since Buffer.from silently drops them.
29+
// Each byte is represented by two hex characters.
30+
digestBytes.length !== hexDigest.length / 2 ||
31+
// timingSafeEqual checks this, but throws a different error.
32+
sig.length !== digestBytes.length ||
33+
!timingSafeEqual(sig, digestBytes)
34+
) {
2535
throw new Error("Webhook message hash does not match signature");
2636
}
2737

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright ©, 2023, Lightspark Group, Inc. - All Rights Reserved
2+
3+
import { type PathProps } from "../types.js";
4+
5+
export function Turtle({
6+
strokeWidth = "2",
7+
strokeLinecap = "round",
8+
strokeLinejoin = "round",
9+
}: PathProps) {
10+
return (
11+
<svg
12+
width="14"
13+
height="14"
14+
viewBox="0 0 14 14"
15+
fill="none"
16+
xmlns="http://www.w3.org/2000/svg"
17+
>
18+
<path
19+
d="M8.1671 2.48013C9.15709 2.48013 10.0039 2.6611 10.7072 3.01903L10.5312 3.37108L11.053 3.63198L11.2079 3.32209C11.5465 3.56151 11.8444 3.85059 12.1 4.19082C12.8677 5.21246 13.2146 6.62677 13.2644 8.31346H13.4171C13.6587 8.31346 13.8546 8.50934 13.8546 8.75096C13.8546 8.99259 13.6587 9.18846 13.4171 9.18846H2.91709C2.67546 9.18846 2.47959 8.99259 2.47959 8.75096C2.47959 8.50934 2.67546 8.31346 2.91709 8.31346H3.06976C3.08347 7.84939 3.11966 7.40593 3.18093 6.9856C3.20097 6.84808 3.22369 6.71304 3.24919 6.58056C3.42902 5.64624 3.74678 4.8394 4.23414 4.19082C4.35651 4.02798 4.48851 3.87679 4.63 3.73716C4.78402 3.58516 4.9493 3.44685 5.12566 3.32209L5.28119 3.63198L6.52989 6.12824L5.43728 8.31346H6.08954L7.03632 6.42104H9.30015L10.2469 8.31346H10.8992L9.80374 6.12425L11.0513 3.63198L10.5295 3.37108L9.29844 5.83201H7.03404L5.803 3.37108L5.62641 3.01903C6.32976 2.66095 7.17689 2.48013 8.1671 2.48013Z"
20+
fill="#F9F9F9"
21+
/>
22+
<path
23+
d="M2.08378 2.04297H1.69009C1.07446 2.04297 0.52086 2.41779 0.292236 2.98939C0.160003 3.32 0.14923 3.68678 0.26183 4.02458L0.311807 4.17451C0.526179 4.81763 1.19907 4.95949 2.04224 4.95964C2.40358 4.95964 2.9784 5.49737 3.06604 5.84793C3.10803 6.0155 3.23826 6.38242 3.18093 6.9856C3.20097 6.84808 3.22369 6.71304 3.24919 6.58056C3.42902 5.64624 3.74678 4.8394 4.23414 4.19082C4.35651 4.02798 4.48851 3.87679 4.63 3.73716C3.50057 2.33464 3.10211 2.04297 2.08378 2.04297Z"
24+
fill="#F9F9F9"
25+
/>
26+
<path
27+
d="M7 8.75V9.1123C7 9.64098 6.87705 10.1629 6.64062 10.6357L6.47852 10.9609C6.24953 11.4188 5.87599 11.7889 5.41602 12.0137L5.22168 12.1084C5.03046 12.2018 4.82023 12.25 4.60742 12.25H4.22559C3.66405 12.25 3.20822 11.7949 3.20801 11.2334C3.20801 10.7958 3.4883 10.4071 3.90332 10.2686L4.08301 10.208C4.60555 10.0338 4.95801 9.54495 4.95801 8.99414V8.75H7Z"
28+
fill="#F9F9F9"
29+
/>
30+
<path
31+
d="M9.91699 8.75V9.1123C9.91699 9.641 10.0399 10.1629 10.2764 10.6357L10.4385 10.9609C10.6675 11.4188 11.041 11.7889 11.501 12.0137L11.6953 12.1084C11.8865 12.2017 12.0969 12.25 12.3096 12.25H12.6914C13.2529 12.25 13.7078 11.7949 13.708 11.2334C13.708 10.7957 13.428 10.407 13.0127 10.2686L12.833 10.208C12.3107 10.0337 11.958 9.54481 11.958 8.99414V8.75H9.91699Z"
32+
fill="#F9F9F9"
33+
/>
34+
</svg>
35+
);
36+
}

packages/ui/src/icons/central/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export { TradingViewCandles as CentralTradingViewCandles } from "./TradingViewCa
131131
export { TrashCan as CentralTrashCan } from "./TrashCan.js";
132132
export { TriangleExclamation as CentralTriangleExclamation } from "./TriangleExclamation.js";
133133
export { TriangleExclamationFilled as CentralTriangleExclamationFilled } from "./TriangleExclamationFilled.js";
134+
export { Turtle as CentralTurtle } from "./Turtle.js";
134135
export { Vignette as CentralVignette } from "./Vignette.js";
135136
export { WeakStrengthIcon as CentralWeakStrengthIcon } from "./WeakStrength.js";
136137
export { Wrench as CentralWrench } from "./Wrench.js";

0 commit comments

Comments
 (0)