oscerd commented on code in PR #27719:
URL: https://github.com/apache/camel/pull/27719#discussion_r4248497409
##########
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:
Agreed that a colliding option name should not end in a confusing 403.
Filtering the reserved names out of `callbackParameters()` would not avoid it,
though: `LRAUrlBuilder` still writes the option into the callback URL next to
the parameter with the same name, so the duplicate is still in the URL the
coordinator calls back, and the signed map would no longer match that URL. Such
a name already broke the callback before this change (the duplicate also fails
`parseQuery()` on the unsigned path).
So I went with failing fast: `LRASagaService.registerStep()` now rejects a
saga option named like `Camel-Saga-Compensate`, `Camel-Saga-Complete` or
`Camel-Saga-Signature`, in any case (the signed values become headers, whose
names are case-insensitive), with an `IllegalArgumentException` when the route
is created. Test:
`LRASagaServiceAllowlistTest#testRegisterStepRejectsReservedOptionNames`.
_Claude Code on behalf of oscerd_
##########
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:
Done slightly differently: `verify()` now returns `false` when the
parameters are `null`, so a callback whose query cannot be read is rejected by
the signer itself instead of relying on the callers' guards. `sign()` is only
called with the map built in `LRAClient`. Test:
`LRACallbackSignerTest#missingParametersDoNotMatch`.
_Claude Code on behalf of oscerd_
--
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]