This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new cec2df58992f camel-clickup: validate the webhook signature against the 
UTF-8 body (#29696)
cec2df58992f is described below

commit cec2df58992f6c2e23a5af874c387f60e81d9e6a
Author: dhairya05 <[email protected]>
AuthorDate: Thu Nov 12 23:39:29 2026 +0530

    camel-clickup: validate the webhook signature against the UTF-8 body 
(#29696)
---
 .../clickup/service/ClickUpWebhookService.java     | 19 ++++--
 .../apache/camel/component/clickup/util/Utils.java | 20 ++++++-
 .../camel/component/clickup/util/UtilsTest.java    | 69 ++++++++++++++++++++++
 3 files changed, 100 insertions(+), 8 deletions(-)

diff --git 
a/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/service/ClickUpWebhookService.java
 
b/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/service/ClickUpWebhookService.java
index ca0786a9cca5..769abcccac76 100644
--- 
a/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/service/ClickUpWebhookService.java
+++ 
b/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/service/ClickUpWebhookService.java
@@ -18,6 +18,8 @@ package org.apache.camel.component.clickup.service;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
 import java.util.Optional;
 import java.util.Set;
 
@@ -80,23 +82,30 @@ public class ClickUpWebhookService {
             throw new RuntimeException("The webhook HTTP request doesn't 
contain the 'x-signature' header.");
         }
 
-        String messageBody;
+        byte[] messageBytes;
         try (InputStream body = message.getBody(InputStream.class)) {
-            messageBody = new String(body.readAllBytes());
+            // the signature covers the bytes as they arrived: decoding and 
re-encoding them first would turn a
+            // malformed sequence into U+FFFD and change what is hashed
+            messageBytes = body.readAllBytes();
 
-            LOG.debug("Message body {} to be validated against the given 
signature {}", messageBody, signature);
+            LOG.debug("Message body {} to be validated against the given 
signature {}",
+                    new String(messageBytes, StandardCharsets.UTF_8), 
signature);
 
             body.reset();
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
 
-        String calculatedSignature = Utils.computeMessageHMAC(messageBody, 
sharedSecret);
+        String messageBody = new String(messageBytes, StandardCharsets.UTF_8);
+        String calculatedSignature = Utils.computeMessageHMAC(messageBytes, 
sharedSecret);
 
         signature = signature.toLowerCase();
         calculatedSignature = calculatedSignature.toLowerCase();
 
-        boolean signatureMatches = calculatedSignature.equals(signature);
+        // constant time, so that a wrong signature cannot be refined one 
character at a time
+        boolean signatureMatches = MessageDigest.isEqual(
+                calculatedSignature.getBytes(StandardCharsets.US_ASCII),
+                signature.getBytes(StandardCharsets.US_ASCII));
         if (!signatureMatches) {
             LOG.debug("The message signature {} does not match {}.", 
signature, calculatedSignature);
 
diff --git 
a/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/util/Utils.java
 
b/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/util/Utils.java
index d66accfe654d..5b6ce08e0705 100644
--- 
a/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/util/Utils.java
+++ 
b/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/util/Utils.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.component.clickup.util;
 
+import java.nio.charset.StandardCharsets;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 
@@ -35,11 +36,23 @@ public class Utils {
     private static final String HMAC_HASHING_ALGORITHM = "HmacSHA256";
 
     public static String computeMessageHMAC(String message, String key) {
+        // ClickUp signs the raw request body, so neither side of the HMAC may 
depend on the platform default charset
+        return computeMessageHMAC(message.getBytes(StandardCharsets.UTF_8), 
key);
+    }
+
+    /**
+     * Computes the HMAC of the message exactly as it arrived, without 
decoding and re-encoding it first.
+     *
+     * @param  message the raw message bytes, as they were received
+     * @param  key     the webhook's shared secret
+     * @return         the HMAC as an upper-case hexadecimal string
+     */
+    public static String computeMessageHMAC(byte[] message, String key) {
         try {
             Mac mac = Mac.getInstance(HMAC_HASHING_ALGORITHM);
-            mac.init(new SecretKeySpec(key.getBytes(), 
HMAC_HASHING_ALGORITHM));
+            mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), 
HMAC_HASHING_ALGORITHM));
 
-            byte[] hash = mac.doFinal(message.getBytes());
+            byte[] hash = mac.doFinal(message);
 
             return DatatypeConverter.printHexBinary(hash);
         } catch (NoSuchAlgorithmException e) {
@@ -47,7 +60,8 @@ public class Utils {
 
             throw new RuntimeCamelException(e);
         } catch (InvalidKeyException e) {
-            LOG.debug("This exception should never occur: the provided key is 
not valid. {}", key);
+            // the key is the webhook's shared secret, so it must not be logged
+            LOG.debug("This exception should never occur: the provided key is 
not valid.");
 
             throw new RuntimeCamelException(e);
         }
diff --git 
a/components/camel-clickup/src/test/java/org/apache/camel/component/clickup/util/UtilsTest.java
 
b/components/camel-clickup/src/test/java/org/apache/camel/component/clickup/util/UtilsTest.java
new file mode 100644
index 000000000000..4d0c7db5742c
--- /dev/null
+++ 
b/components/camel-clickup/src/test/java/org/apache/camel/component/clickup/util/UtilsTest.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.clickup.util;
+
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The webhook signature has to be reproducible on any JVM: ClickUp computes 
it over the raw request body, so the
+ * expected values below are HMAC-SHA256 over UTF-8 bytes.
+ */
+public class UtilsTest {
+
+    @Test
+    public void shouldComputeTheHmacOfAnAsciiMessage() {
+        assertThat(Utils.computeMessageHMAC("payload", "secret"))
+                
.isEqualTo("B82FCB791ACEC57859B989B430A826488CE2E479FDF92326BD0A2E8375A42BA4");
+    }
+
+    @Test
+    public void shouldComputeTheHmacOverUtf8Bytes() {
+        // "secret" and "payload" with a diaeresis and a tilde: neither is 
representable in US-ASCII, and both
+        // encode differently in ISO-8859-1
+        final String key = "s\u00ebcret";
+        final String message = "p\u00e3yload";
+
+        assertThat(Utils.computeMessageHMAC(message, key))
+                
.isEqualTo("3F449438582E13522059F2FD70C99DDF4BEB084815B88E184F487E3F0F627656");
+    }
+
+    @Test
+    public void shouldHashTheRawBytesAsGiven() {
+        final String key = "s\u00ebcret";
+        final String message = "p\u00e3yload";
+
+        // the byte[] overload is what the webhook check uses, so for valid 
UTF-8 it has to agree with the String one
+        
assertThat(Utils.computeMessageHMAC(message.getBytes(StandardCharsets.UTF_8), 
key))
+                .isEqualTo(Utils.computeMessageHMAC(message, key));
+    }
+
+    @Test
+    public void shouldHashABodyThatIsNotValidUtf8() {
+        final String key = "secret";
+        // a lone 0x80 continuation byte: decoding the body to a String first 
turns it into U+FFFD and changes the
+        // hash, which is why the check has to work on the bytes as they 
arrived
+        final byte[] message = { 'a', (byte) 0x80, 'b' };
+
+        assertThat(Utils.computeMessageHMAC(message, key))
+                .isNotEqualTo(Utils.computeMessageHMAC(new String(message, 
StandardCharsets.UTF_8), key));
+    }
+
+}

Reply via email to