From a2b450a240a88ee3567475b42c4946aa9e7dc1a7 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 11 Mar 2026 00:17:43 +0100 Subject: [PATCH 01/10] Add URL normalization test vectors and fix no-schema URL handling --- .../didww/sdk/callback/RequestValidator.java | 3 ++ .../sdk/callback/RequestValidatorTest.java | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/main/java/com/didww/sdk/callback/RequestValidator.java b/src/main/java/com/didww/sdk/callback/RequestValidator.java index 648137b..4623937 100644 --- a/src/main/java/com/didww/sdk/callback/RequestValidator.java +++ b/src/main/java/com/didww/sdk/callback/RequestValidator.java @@ -35,6 +35,9 @@ private String validSignature(String url, Map payload) { private String normalizeUrl(String url) { try { + if (!url.matches("^[a-zA-Z]+://.*")) { + url = "http://" + url; + } URI uri = URI.create(url); String scheme = uri.getScheme(); String userInfo = uri.getUserInfo() != null ? uri.getUserInfo() + "@" : ""; diff --git a/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java b/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java index e983d17..a7feb46 100644 --- a/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java +++ b/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java @@ -1,9 +1,12 @@ package com.didww.sdk.callback; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import java.util.LinkedHashMap; import java.util.Map; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -65,4 +68,36 @@ void testInvalidSignatureRequest() { assertThat(validator.validate("http://example.com/callbacks", payload, "fbdb1d1b18aa6c08324b7d64b71fb76370690e1d")).isFalse(); } + + static Stream urlNormalizationVectors() { + return Stream.of( + new Object[]{"http://foo.com/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"}, + new Object[]{"http://foo.com:80/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"}, + new Object[]{"http://foo.com:443/bar", "904eaa65c0759afac0e4d8912de424e2dfb96ea1"}, + new Object[]{"http://foo.com:8182/bar", "eb8fcfb3d7ed4b4c2265d73cf93c31ba614384d1"}, + new Object[]{"foo.com/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"}, + new Object[]{"http://foo.com/bar?baz=boo", "78b00717a86ce9df06abf45ff818aa94537e1729"}, + new Object[]{"http://user:pass@foo.com/bar", "88615a11a78c021c1da2e1e0bfb8cc165170afc5"}, + new Object[]{"http://foo.com/bar#test", "b1c4391fcdab7c0521bb5b9eb4f41f08529b8418"}, + new Object[]{"https://foo.com/bar", "f26a771c302319a7094accbe2989bad67fff2928"}, + new Object[]{"https://foo.com:443/bar", "f26a771c302319a7094accbe2989bad67fff2928"}, + new Object[]{"https://foo.com:80/bar", "bd45af5253b72f6383c6af7dc75250f12b73a4e1"}, + new Object[]{"https://foo.com:8384/bar", "9c9fec4b7ebd6e1c461cb8e4ffe4f2987a19a5d3"}, + new Object[]{"https://foo.com/bar?qwe=asd", "4a0e98ddf286acadd1d5be1b0ed85a4e541c3137"}, + new Object[]{"https://qwe:asd@foo.com/bar", "7a8cd4a6c349910dfecaf9807e56a63787250bbd"}, + new Object[]{"https://foo.com/bar#baz", "5024919770ea5ca2e3ccc07cb940323d79819508"} + ); + } + + @ParameterizedTest + @MethodSource("urlNormalizationVectors") + void testUrlNormalization(String url, String expectedSignature) { + RequestValidator validator = new RequestValidator("SOMEAPIKEY"); + Map payload = new LinkedHashMap<>(); + payload.put("id", "1dd7a68b-e235-402b-8912-fe73ee14243a"); + payload.put("status", "completed"); + payload.put("type", "orders"); + + assertThat(validator.validate(url, payload, expectedSignature)).isTrue(); + } } From 8b56e4fce180aa743ab6fd893b878b8db9fdd433 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 11 Mar 2026 08:53:51 +0100 Subject: [PATCH 02/10] Fix scheme case sensitivity and avoid regex recompilation in RequestValidator - Lowercase the URI scheme after parsing to handle mixed-case schemes correctly - Replace url.matches() with a precompiled static Pattern to avoid recompiling the regex on every call --- src/main/java/com/didww/sdk/callback/RequestValidator.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/didww/sdk/callback/RequestValidator.java b/src/main/java/com/didww/sdk/callback/RequestValidator.java index 4623937..f067be2 100644 --- a/src/main/java/com/didww/sdk/callback/RequestValidator.java +++ b/src/main/java/com/didww/sdk/callback/RequestValidator.java @@ -6,9 +6,12 @@ import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.TreeMap; +import java.util.regex.Pattern; public class RequestValidator { + private static final Pattern SCHEME_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9+.-]*://"); + public static final String HEADER_NAME = "X-DIDWW-Signature"; private final String apiKey; @@ -35,11 +38,11 @@ private String validSignature(String url, Map payload) { private String normalizeUrl(String url) { try { - if (!url.matches("^[a-zA-Z]+://.*")) { + if (!SCHEME_PATTERN.matcher(url).find()) { url = "http://" + url; } URI uri = URI.create(url); - String scheme = uri.getScheme(); + String scheme = uri.getScheme().toLowerCase(); String userInfo = uri.getUserInfo() != null ? uri.getUserInfo() + "@" : ""; String host = uri.getHost(); From 26c3aca671bee2ff2f0afcc12fcc9426e60e00d3 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 11 Mar 2026 10:57:23 +0100 Subject: [PATCH 03/10] Fix locale-sensitive toLowerCase and use Stream in tests - Use toLowerCase(Locale.ROOT) for scheme normalization to avoid locale-dependent behavior (e.g. Turkish locale) - Restore SCHEME_PATTERN and no-scheme URL handling - Change Stream to Stream with Arguments.of() for type-safe JUnit 5 parameterized tests - Restore URL normalization test vectors --- .../didww/sdk/callback/RequestValidator.java | 3 +- .../sdk/callback/RequestValidatorTest.java | 33 ++++++++++--------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/didww/sdk/callback/RequestValidator.java b/src/main/java/com/didww/sdk/callback/RequestValidator.java index f067be2..12cc970 100644 --- a/src/main/java/com/didww/sdk/callback/RequestValidator.java +++ b/src/main/java/com/didww/sdk/callback/RequestValidator.java @@ -4,6 +4,7 @@ import javax.crypto.spec.SecretKeySpec; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.Locale; import java.util.Map; import java.util.TreeMap; import java.util.regex.Pattern; @@ -42,7 +43,7 @@ private String normalizeUrl(String url) { url = "http://" + url; } URI uri = URI.create(url); - String scheme = uri.getScheme().toLowerCase(); + String scheme = uri.getScheme().toLowerCase(Locale.ROOT); String userInfo = uri.getUserInfo() != null ? uri.getUserInfo() + "@" : ""; String host = uri.getHost(); diff --git a/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java b/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java index a7feb46..2fd87ac 100644 --- a/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java +++ b/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import java.util.LinkedHashMap; @@ -69,23 +70,23 @@ void testInvalidSignatureRequest() { assertThat(validator.validate("http://example.com/callbacks", payload, "fbdb1d1b18aa6c08324b7d64b71fb76370690e1d")).isFalse(); } - static Stream urlNormalizationVectors() { + static Stream urlNormalizationVectors() { return Stream.of( - new Object[]{"http://foo.com/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"}, - new Object[]{"http://foo.com:80/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"}, - new Object[]{"http://foo.com:443/bar", "904eaa65c0759afac0e4d8912de424e2dfb96ea1"}, - new Object[]{"http://foo.com:8182/bar", "eb8fcfb3d7ed4b4c2265d73cf93c31ba614384d1"}, - new Object[]{"foo.com/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"}, - new Object[]{"http://foo.com/bar?baz=boo", "78b00717a86ce9df06abf45ff818aa94537e1729"}, - new Object[]{"http://user:pass@foo.com/bar", "88615a11a78c021c1da2e1e0bfb8cc165170afc5"}, - new Object[]{"http://foo.com/bar#test", "b1c4391fcdab7c0521bb5b9eb4f41f08529b8418"}, - new Object[]{"https://foo.com/bar", "f26a771c302319a7094accbe2989bad67fff2928"}, - new Object[]{"https://foo.com:443/bar", "f26a771c302319a7094accbe2989bad67fff2928"}, - new Object[]{"https://foo.com:80/bar", "bd45af5253b72f6383c6af7dc75250f12b73a4e1"}, - new Object[]{"https://foo.com:8384/bar", "9c9fec4b7ebd6e1c461cb8e4ffe4f2987a19a5d3"}, - new Object[]{"https://foo.com/bar?qwe=asd", "4a0e98ddf286acadd1d5be1b0ed85a4e541c3137"}, - new Object[]{"https://qwe:asd@foo.com/bar", "7a8cd4a6c349910dfecaf9807e56a63787250bbd"}, - new Object[]{"https://foo.com/bar#baz", "5024919770ea5ca2e3ccc07cb940323d79819508"} + Arguments.of("http://foo.com/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"), + Arguments.of("http://foo.com:80/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"), + Arguments.of("http://foo.com:443/bar", "904eaa65c0759afac0e4d8912de424e2dfb96ea1"), + Arguments.of("http://foo.com:8182/bar", "eb8fcfb3d7ed4b4c2265d73cf93c31ba614384d1"), + Arguments.of("foo.com/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"), + Arguments.of("http://foo.com/bar?baz=boo", "78b00717a86ce9df06abf45ff818aa94537e1729"), + Arguments.of("http://user:pass@foo.com/bar", "88615a11a78c021c1da2e1e0bfb8cc165170afc5"), + Arguments.of("http://foo.com/bar#test", "b1c4391fcdab7c0521bb5b9eb4f41f08529b8418"), + Arguments.of("https://foo.com/bar", "f26a771c302319a7094accbe2989bad67fff2928"), + Arguments.of("https://foo.com:443/bar", "f26a771c302319a7094accbe2989bad67fff2928"), + Arguments.of("https://foo.com:80/bar", "bd45af5253b72f6383c6af7dc75250f12b73a4e1"), + Arguments.of("https://foo.com:8384/bar", "9c9fec4b7ebd6e1c461cb8e4ffe4f2987a19a5d3"), + Arguments.of("https://foo.com/bar?qwe=asd", "4a0e98ddf286acadd1d5be1b0ed85a4e541c3137"), + Arguments.of("https://qwe:asd@foo.com/bar", "7a8cd4a6c349910dfecaf9807e56a63787250bbd"), + Arguments.of("https://foo.com/bar#baz", "5024919770ea5ca2e3ccc07cb940323d79819508") ); } From dd31056af27dc9f1aa8bbe268146f779f52ee31c Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 11 Mar 2026 12:49:28 +0100 Subject: [PATCH 04/10] Remove no-schema URL handling and clean up related code - Remove SCHEME_PATTERN constant and the no-schema URL prepend logic from RequestValidator; http/https URLs are always expected - Remove the schema-less test vector ("foo.com/bar") from urlNormalizationVectors - Add NOSONAR comments on String.format and RuntimeException catch in hmacSha1 --- .../java/com/didww/sdk/callback/RequestValidator.java | 10 ++-------- .../com/didww/sdk/callback/RequestValidatorTest.java | 1 - 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/didww/sdk/callback/RequestValidator.java b/src/main/java/com/didww/sdk/callback/RequestValidator.java index 12cc970..aac0ece 100644 --- a/src/main/java/com/didww/sdk/callback/RequestValidator.java +++ b/src/main/java/com/didww/sdk/callback/RequestValidator.java @@ -7,12 +7,9 @@ import java.util.Locale; import java.util.Map; import java.util.TreeMap; -import java.util.regex.Pattern; public class RequestValidator { - private static final Pattern SCHEME_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9+.-]*://"); - public static final String HEADER_NAME = "X-DIDWW-Signature"; private final String apiKey; @@ -39,9 +36,6 @@ private String validSignature(String url, Map payload) { private String normalizeUrl(String url) { try { - if (!SCHEME_PATTERN.matcher(url).find()) { - url = "http://" + url; - } URI uri = URI.create(url); String scheme = uri.getScheme().toLowerCase(Locale.ROOT); String userInfo = uri.getUserInfo() != null ? uri.getUserInfo() + "@" : ""; @@ -73,11 +67,11 @@ private static String hmacSha1(String data, String key) { byte[] rawHmac = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : rawHmac) { - sb.append(String.format("%02x", b)); + sb.append(String.format("%02x", b)); // NOSONAR } return sb.toString(); } catch (Exception e) { - throw new RuntimeException("Failed to compute HMAC-SHA1", e); + throw new RuntimeException("Failed to compute HMAC-SHA1", e); // NOSONAR } } } diff --git a/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java b/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java index 2fd87ac..0ecbf37 100644 --- a/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java +++ b/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java @@ -76,7 +76,6 @@ static Stream urlNormalizationVectors() { Arguments.of("http://foo.com:80/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"), Arguments.of("http://foo.com:443/bar", "904eaa65c0759afac0e4d8912de424e2dfb96ea1"), Arguments.of("http://foo.com:8182/bar", "eb8fcfb3d7ed4b4c2265d73cf93c31ba614384d1"), - Arguments.of("foo.com/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"), Arguments.of("http://foo.com/bar?baz=boo", "78b00717a86ce9df06abf45ff818aa94537e1729"), Arguments.of("http://user:pass@foo.com/bar", "88615a11a78c021c1da2e1e0bfb8cc165170afc5"), Arguments.of("http://foo.com/bar#test", "b1c4391fcdab7c0521bb5b9eb4f41f08529b8418"), From d69bc01ff3d804e9ba384390a208e02430a3d4d9 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Wed, 11 Mar 2026 17:19:20 +0100 Subject: [PATCH 05/10] Extract common payload helper in RequestValidatorTest to reduce duplication --- .../sdk/callback/RequestValidatorTest.java | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java b/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java index 0ecbf37..5941777 100644 --- a/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java +++ b/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java @@ -13,6 +13,14 @@ class RequestValidatorTest { + private static Map ordersPayload() { + Map payload = new LinkedHashMap<>(); + payload.put("status", "completed"); + payload.put("id", "1dd7a68b-e235-402b-8912-fe73ee14243a"); + payload.put("type", "orders"); + return payload; + } + @Test void testSandbox() { RequestValidator validator = new RequestValidator("SOMEAPIKEY"); @@ -29,45 +37,29 @@ void testSandbox() { @Test void testValidRequest() { RequestValidator validator = new RequestValidator("SOMEAPIKEY"); - Map payload = new LinkedHashMap<>(); - payload.put("status", "completed"); - payload.put("id", "1dd7a68b-e235-402b-8912-fe73ee14243a"); - payload.put("type", "orders"); - assertThat(validator.validate("http://example.com/callbacks", payload, "fe99e416c3547f2f59002403ec856ea386d05b2f")).isTrue(); + assertThat(validator.validate("http://example.com/callbacks", ordersPayload(), "fe99e416c3547f2f59002403ec856ea386d05b2f")).isTrue(); } @Test void testValidRequestWithQueryAndFragment() { RequestValidator validator = new RequestValidator("OTHERAPIKEY"); - Map payload = new LinkedHashMap<>(); - payload.put("status", "completed"); - payload.put("id", "1dd7a68b-e235-402b-8912-fe73ee14243a"); - payload.put("type", "orders"); - assertThat(validator.validate("http://example.com/callbacks?foo=bar#baz", payload, "32754ba93ac1207e540c0cf90371e7786b3b1cde")).isTrue(); + assertThat(validator.validate("http://example.com/callbacks?foo=bar#baz", ordersPayload(), "32754ba93ac1207e540c0cf90371e7786b3b1cde")).isTrue(); } @Test void testEmptySignatureRequest() { RequestValidator validator = new RequestValidator("SOMEAPIKEY"); - Map payload = new LinkedHashMap<>(); - payload.put("status", "completed"); - payload.put("id", "1dd7a68b-e235-402b-8912-fe73ee14243a"); - payload.put("type", "orders"); - assertThat(validator.validate("http://example.com/callbacks", payload, "")).isFalse(); + assertThat(validator.validate("http://example.com/callbacks", ordersPayload(), "")).isFalse(); } @Test void testInvalidSignatureRequest() { RequestValidator validator = new RequestValidator("SOMEAPIKEY"); - Map payload = new LinkedHashMap<>(); - payload.put("status", "completed"); - payload.put("id", "1dd7a68b-e235-402b-8912-fe73ee14243a"); - payload.put("type", "orders"); - assertThat(validator.validate("http://example.com/callbacks", payload, "fbdb1d1b18aa6c08324b7d64b71fb76370690e1d")).isFalse(); + assertThat(validator.validate("http://example.com/callbacks", ordersPayload(), "fbdb1d1b18aa6c08324b7d64b71fb76370690e1d")).isFalse(); } static Stream urlNormalizationVectors() { @@ -93,11 +85,7 @@ static Stream urlNormalizationVectors() { @MethodSource("urlNormalizationVectors") void testUrlNormalization(String url, String expectedSignature) { RequestValidator validator = new RequestValidator("SOMEAPIKEY"); - Map payload = new LinkedHashMap<>(); - payload.put("id", "1dd7a68b-e235-402b-8912-fe73ee14243a"); - payload.put("status", "completed"); - payload.put("type", "orders"); - assertThat(validator.validate(url, payload, expectedSignature)).isTrue(); + assertThat(validator.validate(url, ordersPayload(), expectedSignature)).isTrue(); } } From e4c42c77991021811ebd6d95cb02e470e0a63be7 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Thu, 12 Mar 2026 00:25:23 +0100 Subject: [PATCH 06/10] Add extra URL normalization vectors and remove no-scheme support Add IPv6, empty path, percent-encoded path vectors from Go SDK. Remove no-scheme URL support. --- .../java/com/didww/sdk/callback/RequestValidator.java | 2 +- .../com/didww/sdk/callback/RequestValidatorTest.java | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/didww/sdk/callback/RequestValidator.java b/src/main/java/com/didww/sdk/callback/RequestValidator.java index aac0ece..903d493 100644 --- a/src/main/java/com/didww/sdk/callback/RequestValidator.java +++ b/src/main/java/com/didww/sdk/callback/RequestValidator.java @@ -50,7 +50,7 @@ private String normalizeUrl(String url) { port = 80; } - String path = uri.getRawPath() != null ? uri.getRawPath() : ""; + String path = (uri.getRawPath() != null && !uri.getRawPath().isEmpty()) ? uri.getRawPath() : "/"; String query = uri.getRawQuery() != null ? "?" + uri.getRawQuery() : ""; String fragment = uri.getRawFragment() != null ? "#" + uri.getRawFragment() : ""; diff --git a/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java b/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java index 5941777..6c9e0ac 100644 --- a/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java +++ b/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java @@ -77,7 +77,15 @@ static Stream urlNormalizationVectors() { Arguments.of("https://foo.com:8384/bar", "9c9fec4b7ebd6e1c461cb8e4ffe4f2987a19a5d3"), Arguments.of("https://foo.com/bar?qwe=asd", "4a0e98ddf286acadd1d5be1b0ed85a4e541c3137"), Arguments.of("https://qwe:asd@foo.com/bar", "7a8cd4a6c349910dfecaf9807e56a63787250bbd"), - Arguments.of("https://foo.com/bar#baz", "5024919770ea5ca2e3ccc07cb940323d79819508") + Arguments.of("https://foo.com/bar#baz", "5024919770ea5ca2e3ccc07cb940323d79819508"), + Arguments.of("http://[::1]/bar", "e0e9b83e4046d097f54b3ae64b08cbb4a539f601"), + Arguments.of("http://[::1]:80/bar", "e0e9b83e4046d097f54b3ae64b08cbb4a539f601"), + Arguments.of("http://[::1]:9090/bar", "ebec110ec5debd0e0fd086ff2f02e48ca665b543"), + Arguments.of("https://[::1]/bar", "f3cfe6f523fdf1d4eaadc310fcd3ed92e1e324b0"), + Arguments.of("http://foo.com", "6e9bb224f621d9bf735e80b45d69af688900e7d2"), + Arguments.of("http://foo.com/", "6e9bb224f621d9bf735e80b45d69af688900e7d2"), + Arguments.of("http://foo.com/hello%20world", "eb64035b2e8f356ff1442898a39ec94d5c3e2fc8"), + Arguments.of("http://foo.com/foo%2Fbar", "db24428442b012fa0972a453ba1ba98e755bba10") ); } From 8d96133e1410479a683db70db213a24c30cd7417 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Thu, 12 Mar 2026 00:40:04 +0100 Subject: [PATCH 07/10] Use timing-safe comparison in RequestValidator --- .../didww/sdk/callback/RequestValidator.java | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/didww/sdk/callback/RequestValidator.java b/src/main/java/com/didww/sdk/callback/RequestValidator.java index 903d493..e55cb97 100644 --- a/src/main/java/com/didww/sdk/callback/RequestValidator.java +++ b/src/main/java/com/didww/sdk/callback/RequestValidator.java @@ -4,6 +4,7 @@ import javax.crypto.spec.SecretKeySpec; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; import java.util.Locale; import java.util.Map; import java.util.TreeMap; @@ -22,16 +23,21 @@ public boolean validate(String url, Map payload, String signatur if (signature == null || signature.isEmpty()) { return false; } - return validSignature(url, payload).equals(signature); + byte[] expectedBytes = computeHmac(url, payload); + byte[] signatureBytes = hexToBytes(signature); + if (signatureBytes == null) { + return false; + } + return MessageDigest.isEqual(expectedBytes, signatureBytes); } - private String validSignature(String url, Map payload) { + private byte[] computeHmac(String url, Map payload) { TreeMap sorted = new TreeMap<>(payload); StringBuilder data = new StringBuilder(normalizeUrl(url)); for (Map.Entry entry : sorted.entrySet()) { data.append(entry.getKey()).append(entry.getValue()); } - return hmacSha1(data.toString(), apiKey); + return hmacSha1Bytes(data.toString(), apiKey); } private String normalizeUrl(String url) { @@ -60,18 +66,29 @@ private String normalizeUrl(String url) { } } - private static String hmacSha1(String data, String key) { + private static byte[] hmacSha1Bytes(String data, String key) { try { Mac mac = Mac.getInstance("HmacSHA1"); mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA1")); - byte[] rawHmac = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); - StringBuilder sb = new StringBuilder(); - for (byte b : rawHmac) { - sb.append(String.format("%02x", b)); // NOSONAR - } - return sb.toString(); + return mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); } catch (Exception e) { throw new RuntimeException("Failed to compute HMAC-SHA1", e); // NOSONAR } } + + private static byte[] hexToBytes(String hex) { + if (hex.length() % 2 != 0) { + return null; + } + byte[] bytes = new byte[hex.length() / 2]; + for (int i = 0; i < bytes.length; i++) { + int hi = Character.digit(hex.charAt(i * 2), 16); + int lo = Character.digit(hex.charAt(i * 2 + 1), 16); + if (hi == -1 || lo == -1) { + return null; + } + bytes[i] = (byte) ((hi << 4) | lo); + } + return bytes; + } } From cd93d5751097a2f1812ccd67ac1f48967dba7b45 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Thu, 12 Mar 2026 00:41:39 +0100 Subject: [PATCH 08/10] Revert empty path normalization to match server behavior --- src/main/java/com/didww/sdk/callback/RequestValidator.java | 2 +- src/test/java/com/didww/sdk/callback/RequestValidatorTest.java | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/com/didww/sdk/callback/RequestValidator.java b/src/main/java/com/didww/sdk/callback/RequestValidator.java index e55cb97..6223cd5 100644 --- a/src/main/java/com/didww/sdk/callback/RequestValidator.java +++ b/src/main/java/com/didww/sdk/callback/RequestValidator.java @@ -56,7 +56,7 @@ private String normalizeUrl(String url) { port = 80; } - String path = (uri.getRawPath() != null && !uri.getRawPath().isEmpty()) ? uri.getRawPath() : "/"; + String path = uri.getRawPath(); String query = uri.getRawQuery() != null ? "?" + uri.getRawQuery() : ""; String fragment = uri.getRawFragment() != null ? "#" + uri.getRawFragment() : ""; diff --git a/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java b/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java index 6c9e0ac..5827fd5 100644 --- a/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java +++ b/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java @@ -82,8 +82,6 @@ static Stream urlNormalizationVectors() { Arguments.of("http://[::1]:80/bar", "e0e9b83e4046d097f54b3ae64b08cbb4a539f601"), Arguments.of("http://[::1]:9090/bar", "ebec110ec5debd0e0fd086ff2f02e48ca665b543"), Arguments.of("https://[::1]/bar", "f3cfe6f523fdf1d4eaadc310fcd3ed92e1e324b0"), - Arguments.of("http://foo.com", "6e9bb224f621d9bf735e80b45d69af688900e7d2"), - Arguments.of("http://foo.com/", "6e9bb224f621d9bf735e80b45d69af688900e7d2"), Arguments.of("http://foo.com/hello%20world", "eb64035b2e8f356ff1442898a39ec94d5c3e2fc8"), Arguments.of("http://foo.com/foo%2Fbar", "db24428442b012fa0972a453ba1ba98e755bba10") ); From 3d51db3a94faf35cba5acde1f881217660e0260c Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Thu, 12 Mar 2026 09:33:02 +0100 Subject: [PATCH 09/10] Fix SonarCloud S1168: return empty array instead of null in hexToBytes --- src/main/java/com/didww/sdk/callback/RequestValidator.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/didww/sdk/callback/RequestValidator.java b/src/main/java/com/didww/sdk/callback/RequestValidator.java index 6223cd5..77d8c1c 100644 --- a/src/main/java/com/didww/sdk/callback/RequestValidator.java +++ b/src/main/java/com/didww/sdk/callback/RequestValidator.java @@ -25,7 +25,7 @@ public boolean validate(String url, Map payload, String signatur } byte[] expectedBytes = computeHmac(url, payload); byte[] signatureBytes = hexToBytes(signature); - if (signatureBytes == null) { + if (signatureBytes.length == 0) { return false; } return MessageDigest.isEqual(expectedBytes, signatureBytes); @@ -78,14 +78,14 @@ private static byte[] hmacSha1Bytes(String data, String key) { private static byte[] hexToBytes(String hex) { if (hex.length() % 2 != 0) { - return null; + return new byte[0]; } byte[] bytes = new byte[hex.length() / 2]; for (int i = 0; i < bytes.length; i++) { int hi = Character.digit(hex.charAt(i * 2), 16); int lo = Character.digit(hex.charAt(i * 2 + 1), 16); if (hi == -1 || lo == -1) { - return null; + return new byte[0]; } bytes[i] = (byte) ((hi << 4) | lo); } From 1ea06a16ced521f4d952ead90e093b6e5b0c4053 Mon Sep 17 00:00:00 2001 From: Igor Fedoronchuk Date: Thu, 12 Mar 2026 09:54:49 +0100 Subject: [PATCH 10/10] Add documentation example test vector --- .../didww/sdk/callback/RequestValidatorTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java b/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java index 5827fd5..1f67cb4 100644 --- a/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java +++ b/src/test/java/com/didww/sdk/callback/RequestValidatorTest.java @@ -62,6 +62,19 @@ void testInvalidSignatureRequest() { assertThat(validator.validate("http://example.com/callbacks", ordersPayload(), "fbdb1d1b18aa6c08324b7d64b71fb76370690e1d")).isFalse(); } + // https://doc.didww.com/api3/2022-05-10/callbacks-details.html#algorithm-implementation-details + @Test + void testDocumentationExample() { + RequestValidator validator = new RequestValidator("szrdgh6547umt7tht7xbqhj6g9gdbyp7"); + String url = "https://mycompany.com/didww_callbacks?opaque=123"; + Map payload = new LinkedHashMap<>(); + payload.put("id", "bf2cee72-6caa-4ae2-917e-bea01945691e"); + payload.put("status", "completed"); + payload.put("type", "orders"); + + assertThat(validator.validate(url, payload, "30f66e9d72eb5e193051fd02952f70d8e934b4ff")).isTrue(); + } + static Stream urlNormalizationVectors() { return Stream.of( Arguments.of("http://foo.com/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"),