diff --git a/src/main/java/com/gr4vy/sdk/Webhooks.java b/src/main/java/com/gr4vy/sdk/Webhooks.java index 0d8b0e6e..532b81cb 100644 --- a/src/main/java/com/gr4vy/sdk/Webhooks.java +++ b/src/main/java/com/gr4vy/sdk/Webhooks.java @@ -1,6 +1,7 @@ package com.gr4vy.sdk; import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; import java.util.Arrays; import java.util.List; @@ -69,7 +70,11 @@ public static void verifyWebhook(String payload, throw new IllegalArgumentException("Invalid secret"); } - if(!signatures.contains(expectedSignature)) { + // Compare in constant time, so the check doesn't leak how much of one matched. + byte[] expected = expectedSignature.getBytes(StandardCharsets.UTF_8); + boolean matched = signatures.stream() + .anyMatch(signature -> MessageDigest.isEqual(expected, signature.getBytes(StandardCharsets.UTF_8))); + if(!matched) { throw new IllegalArgumentException("No matching signature found"); } } diff --git a/src/test/java/com/gr4vy/sdk/WebhooksTest.java b/src/test/java/com/gr4vy/sdk/WebhooksTest.java index 39c99dd5..78f8fd48 100644 --- a/src/test/java/com/gr4vy/sdk/WebhooksTest.java +++ b/src/test/java/com/gr4vy/sdk/WebhooksTest.java @@ -78,4 +78,23 @@ void testVerifyWebhookMissingTimestampHeader() { assertEquals(thrown.getMessage(), "Missing header values"); } + + @Test + void testVerifyWebhookAcceptsAValidSignatureInAnyPosition() { + String header = "other,78aca0c78005107a654a957b8566fa6e0e5e06aea92d7da72a6da9e5a690d013"; + + assertDoesNotThrow(() -> Webhooks.verifyWebhook(payload, secret, header, 0, timestampHeader)); + } + + @Test + void testVerifyWebhookRejectsANearlyMatchingSignature() { + String header = "78aca0c78005107a654a957b8566fa6e0e5e06aea92d7da72a6da9e5a690d014"; + + IllegalArgumentException thrown = assertThrows( + IllegalArgumentException.class, + () -> Webhooks.verifyWebhook(payload, secret, header, 0, timestampHeader) + ); + + assertEquals(thrown.getMessage(), "No matching signature found"); + } }