gnodet-bot commented on code in PR #27719:
URL: https://github.com/apache/camel/pull/27719#discussion_r4247491316


##########
components/camel-lra/src/main/java/org/apache/camel/service/lra/LRAClient.java:
##########
@@ -191,6 +207,16 @@ protected HttpRequest.Builder prepareRequest(URI uri, 
Exchange exchange) {
         return HttpRequest.newBuilder().uri(uri);
     }
 
+    /**
+     * The query parameters of the participant callback URLs of a step, as the 
participant receives them back.
+     */
+    private static Map<String, String> callbackParameters(LRASagaStep step) {
+        Map<String, String> parameters = new HashMap<>(step.getOptions());
+        step.getCompensation().ifPresent(endpoint -> 
parameters.put(URL_COMPENSATION_KEY, endpoint.getEndpointUri()));
+        step.getCompletion().ifPresent(endpoint -> 
parameters.put(URL_COMPLETION_KEY, endpoint.getEndpointUri()));
+        return parameters;
+    }

Review Comment:
   ⚠️ **Missing guard against reserved key collision.** `step.getOptions()` is 
copied verbatim. If a saga option key matches `URL_COMPENSATION_KEY`, 
`URL_COMPLETION_KEY`, or `URL_SIGNATURE_KEY`, `LRAUrlBuilder.query()` will 
append both values, producing a duplicate query parameter. When the coordinator 
calls back, `parseQuery()` uses `Collectors.toMap()` which throws 
`IllegalStateException` on duplicates — caught by the `catch` block and 
converted to `parameters = null` → `verify()` returns `false` → 403.
   
   Consider filtering reserved keys from `step.getOptions()` or throwing early 
at registration time:
   
   ```suggestion
       private static Map<String, String> callbackParameters(LRASagaStep step) {
           Set<String> reserved = Set.of(URL_COMPENSATION_KEY, 
URL_COMPLETION_KEY, URL_SIGNATURE_KEY);
           Map<String, String> parameters = 
step.getOptions().entrySet().stream()
                   .filter(e -> !reserved.contains(e.getKey()))
                   .collect(Collectors.toMap(Map.Entry::getKey, 
Map.Entry::getValue));
           step.getCompensation().ifPresent(endpoint -> 
parameters.put(URL_COMPENSATION_KEY, endpoint.getEndpointUri()));
           step.getCompletion().ifPresent(endpoint -> 
parameters.put(URL_COMPLETION_KEY, endpoint.getEndpointUri()));
           return parameters;
       }
   ```



##########
components/camel-lra/src/main/java/org/apache/camel/service/lra/LRACallbackSigner.java:
##########
@@ -0,0 +1,93 @@
+/*
+ * 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.service.lra;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.util.Base64;
+import java.util.Map;
+import java.util.StringJoiner;
+import java.util.TreeMap;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Signs the participant callback URLs enlisted with the LRA coordinator, and 
verifies the signature when the
+ * coordinator calls them back, using HMAC-SHA256 with the callback secret of 
the LRA saga service.
+ * <p>
+ * The signature covers the callback path and every query parameter of the 
URL: the step endpoint URIs and the saga
+ * options. Parameters are sorted and encoded individually, so the signature 
does not depend on the order or the
+ * encoding used by the coordinator when it calls the URL back.
+ */
+final class LRACallbackSigner {
+
+    private static final String ALGORITHM = "HmacSHA256";
+
+    private final SecretKeySpec key;
+
+    LRACallbackSigner(String secret) {
+        this.key = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), 
ALGORITHM);
+    }
+
+    /**
+     * Computes the signature of a callback URL.
+     *
+     * @param  callbackPath the participant path the URL points to (compensate 
or complete)
+     * @param  parameters   the query parameters of the URL, without the 
signature
+     * @return              the signature, Base64 URL-safe encoded without 
padding
+     */
+    String sign(String callbackPath, Map<String, String> parameters) {
+        try {
+            Mac mac = Mac.getInstance(ALGORITHM);
+            mac.init(key);
+            byte[] digest = mac.doFinal(canonicalForm(callbackPath, 
parameters).getBytes(StandardCharsets.UTF_8));
+            return 
Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
+        } catch (GeneralSecurityException e) {
+            throw new IllegalStateException("Cannot compute the signature of 
the LRA participant callback URL", e);
+        }
+    }
+
+    /**
+     * Checks the signature of a callback URL, in constant time.
+     *
+     * @param  callbackPath the participant path that was called (compensate 
or complete)
+     * @param  parameters   the query parameters of the called URL, without 
the signature
+     * @param  signature    the signature received with the URL, may be 
<tt>null</tt>
+     * @return              true if the signature matches the path and the 
parameters
+     */
+    boolean verify(String callbackPath, Map<String, String> parameters, String 
signature) {
+        if (signature == null) {
+            return false;
+        }
+        byte[] expected = sign(callbackPath, 
parameters).getBytes(StandardCharsets.UTF_8);
+        return MessageDigest.isEqual(expected, 
signature.getBytes(StandardCharsets.UTF_8));
+    }
+
+    private static String canonicalForm(String callbackPath, Map<String, 
String> parameters) {
+        StringJoiner joiner = new StringJoiner("&", callbackPath + "?", "");
+        new TreeMap<>(parameters).forEach((name, value) -> 
joiner.add(encode(name) + "=" + encode(value)));
+        return joiner.toString();

Review Comment:
   💡 `new TreeMap<>(parameters)` will throw `NullPointerException` if 
`parameters` is `null`. Currently unreachable (all callers guard against null 
upstream), but a defensive precondition would make the invariant explicit and 
protect against future callers:
   
   ```suggestion
           new TreeMap<>(Objects.requireNonNull(parameters, "parameters must 
not be null")).forEach((name, value) -> joiner.add(encode(name) + "=" + 
encode(value)));
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to