-
Notifications
You must be signed in to change notification settings - Fork 0
Add cross-SDK request validator test vectors and fix URL normalization #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a2b450a
8b56e4f
26c3aca
dd31056
d69bc01
e4c42c7
8d96133
cd93d57
3d51db3
1ea06a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,6 +4,8 @@ | |||||||||||||||
| 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; | ||||||||||||||||
|
|
||||||||||||||||
|
|
@@ -21,22 +23,27 @@ public boolean validate(String url, Map<String, String> 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.length == 0) { | ||||||||||||||||
| return false; | ||||||||||||||||
| } | ||||||||||||||||
| return MessageDigest.isEqual(expectedBytes, signatureBytes); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| private String validSignature(String url, Map<String, String> payload) { | ||||||||||||||||
| private byte[] computeHmac(String url, Map<String, String> payload) { | ||||||||||||||||
| TreeMap<String, String> sorted = new TreeMap<>(payload); | ||||||||||||||||
| StringBuilder data = new StringBuilder(normalizeUrl(url)); | ||||||||||||||||
| for (Map.Entry<String, String> 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) { | ||||||||||||||||
| try { | ||||||||||||||||
| URI uri = URI.create(url); | ||||||||||||||||
| String scheme = uri.getScheme(); | ||||||||||||||||
| String scheme = uri.getScheme().toLowerCase(Locale.ROOT); | ||||||||||||||||
|
||||||||||||||||
| String scheme = uri.getScheme().toLowerCase(Locale.ROOT); | |
| String scheme = uri.getScheme(); | |
| if (scheme == null) { | |
| uri = URI.create("http://" + url); | |
| scheme = uri.getScheme(); | |
| } | |
| scheme = scheme.toLowerCase(Locale.ROOT); |
Copilot
AI
Mar 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, the // NOSONAR suppression on the thrown exception makes it harder to understand what is being suppressed and why. If Sonar is flagging the exception type/message, it’s better to address the underlying finding (e.g., narrowing the caught exceptions or using a more specific exception type) rather than suppressing it inline.
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -1,14 +1,26 @@ | ||||||||
| package com.didww.sdk.callback; | ||||||||
|
|
||||||||
| 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; | ||||||||
| import java.util.Map; | ||||||||
| import java.util.stream.Stream; | ||||||||
|
|
||||||||
| import static org.assertj.core.api.Assertions.assertThat; | ||||||||
|
|
||||||||
| class RequestValidatorTest { | ||||||||
|
|
||||||||
| private static Map<String, String> ordersPayload() { | ||||||||
| Map<String, String> 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"); | ||||||||
|
|
@@ -25,44 +37,74 @@ void testSandbox() { | |||||||
| @Test | ||||||||
| void testValidRequest() { | ||||||||
| RequestValidator validator = new RequestValidator("SOMEAPIKEY"); | ||||||||
| Map<String, String> 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<String, String> 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<String, String> 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"); | ||||||||
|
|
||||||||
| 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<String, String> payload = new LinkedHashMap<>(); | ||||||||
| payload.put("id", "bf2cee72-6caa-4ae2-917e-bea01945691e"); | ||||||||
| 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(url, payload, "30f66e9d72eb5e193051fd02952f70d8e934b4ff")).isTrue(); | ||||||||
| } | ||||||||
|
|
||||||||
| static Stream<Arguments> urlNormalizationVectors() { | ||||||||
| return Stream.of( | ||||||||
| 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("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/bar", "f26a771c302319a7094accbe2989bad67fff2928"), | |
| Arguments.of("https://foo.com/bar", "f26a771c302319a7094accbe2989bad67fff2928"), | |
| Arguments.of("HTTPS://foo.com/bar", "f26a771c302319a7094accbe2989bad67fff2928"), |
Copilot
AI
Mar 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR description mentions 'Added 15 hardcoded cross-SDK test vectors', but this method currently defines 14 Arguments.of(...) entries. Either add the missing vector or adjust the PR description so it matches the code.
Copilot
AI
Mar 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR description mentions “15 hardcoded … test vectors”, but urlNormalizationVectors() currently contains 14 entries. Either add the missing vector or update the PR description so it matches what’s being tested.
Copilot
AI
Mar 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The payload setup here is duplicated across multiple tests in this class (same keys/values with only ordering differences). Extracting a small helper (e.g., a method that returns the common payload map) would reduce repetition and make it easier to update the test data consistently if the signature inputs change again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To fully benefit from a timing-safe comparison and to keep the validator strict, consider rejecting signatures that decode to a different byte length than the computed HMAC (HmacSHA1 is 20 bytes). As written,
MessageDigest.isEqualwill early-return on length mismatch, and non-standard-length hex signatures (e.g., 2/4/100 hex chars) will be accepted as valid input formats rather than being treated as invalid upfront.