This is an automated email from the ASF dual-hosted git repository.
Croway pushed a commit to branch camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.18.x by this push:
new 19f91785f9e9 CAMEL-24429: camel-as2 - confine per-request signing keys
to the request in ResponseMDN
19f91785f9e9 is described below
commit 19f91785f9e975e7734efd514946c2dfdbdbd1d7
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Aug 28 10:19:12 2026 +0200
CAMEL-24429: camel-as2 - confine per-request signing keys to the request in
ResponseMDN
ResponseMDN held the AS2 security material in mutable instance fields and
overwrote them per request when the keys are dynamic. A single ResponseMDN
is registered on the shared HttpProcessor and serves every request; the
assignment block was not covered by the class's lock, and the values stayed
in place after the request that set them. A deployment hosting several
partners on different paths, each with its own keys, could therefore sign
one partner's MDN with another partner's private key, or validate against
the wrong chain. An MDN is the non-repudiation record for the interchange,
so signing it with the wrong key undermines the property it exists to carry.
process() now resolves the five values into locals for the duration of the
call; the locals shadow the fields so the three downstream uses need no
change. The fields and keysAreDynamic are now final, which is the actual
regression guard: a later change cannot reintroduce per-request mutation
without failing to compile. Both constructors assign every field.
Adds ResponseMDNPerRequestKeysTest, which fails against the previous code
with "expected: <null> but was: <SHA256WITHRSA>".
Closes #25641
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
(cherry picked from commit 659176dec58522fbebea11a1f6d5ee5c37f235d9)
---
components/camel-as2/camel-as2-api/pom.xml | 5 ++
.../component/as2/api/protocol/ResponseMDN.java | 41 +++++++++----
.../protocol/ResponseMDNPerRequestKeysTest.java | 71 ++++++++++++++++++++++
3 files changed, 106 insertions(+), 11 deletions(-)
diff --git a/components/camel-as2/camel-as2-api/pom.xml
b/components/camel-as2/camel-as2-api/pom.xml
index 5eddbcdfa65a..65a89b8b14e2 100644
--- a/components/camel-as2/camel-as2-api/pom.xml
+++ b/components/camel-as2/camel-as2-api/pom.xml
@@ -99,6 +99,11 @@
<version>${as2-lib-version}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git
a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/protocol/ResponseMDN.java
b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/protocol/ResponseMDN.java
index 176cfcb0ebf9..3ead99471348 100644
---
a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/protocol/ResponseMDN.java
+++
b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/protocol/ResponseMDN.java
@@ -90,12 +90,15 @@ public class ResponseMDN implements HttpResponseInterceptor
{
private final String serverFQDN;
private final String mdnMessageTemplate;
- private AS2SignatureAlgorithm signingAlgorithm;
- private Certificate[] signingCertificateChain;
- private PrivateKey signingPrivateKey;
- private PrivateKey decryptingPrivateKey;
- private Certificate[] validateSigningCertificateChain;
- private boolean keysAreDynamic = false; // Flag indicating if security
keys/certs must be dynamically fetched from the HttpContext
+ // Configured security material. These are the statically configured
values only: when the keys are
+ // dynamic they are resolved per request into locals in process(), never
stored back here, because a
+ // single ResponseMDN instance serves every request on the shared
HttpProcessor.
+ private final AS2SignatureAlgorithm signingAlgorithm;
+ private final Certificate[] signingCertificateChain;
+ private final PrivateKey signingPrivateKey;
+ private final PrivateKey decryptingPrivateKey;
+ private final Certificate[] validateSigningCertificateChain;
+ private final boolean keysAreDynamic; // whether security keys/certs must
be fetched per request from the HttpContext
private final Lock lock = new ReentrantLock();
private VelocityEngine velocityEngine;
@@ -109,6 +112,11 @@ public class ResponseMDN implements
HttpResponseInterceptor {
this.mdnMessageTemplate = DEFAULT_MDN_MESSAGE_TEMPLATE;
}
this.keysAreDynamic = true;
+ this.signingAlgorithm = null;
+ this.signingCertificateChain = null;
+ this.signingPrivateKey = null;
+ this.decryptingPrivateKey = null;
+ this.validateSigningCertificateChain = null;
}
public ResponseMDN(String as2Version, String serverFQDN,
AS2SignatureAlgorithm signingAlgorithm,
@@ -128,6 +136,7 @@ public class ResponseMDN implements HttpResponseInterceptor
{
this.mdnMessageTemplate = DEFAULT_MDN_MESSAGE_TEMPLATE;
}
this.validateSigningCertificateChain = validateSigningCertificateChain;
+ this.keysAreDynamic = false;
}
@Override
@@ -142,15 +151,25 @@ public class ResponseMDN implements
HttpResponseInterceptor {
return;
}
+ // Resolve the security material for THIS request into locals. A
single ResponseMDN instance is
+ // registered on the shared HttpProcessor and serves every request, so
per-request keys must not
+ // be written back to instance fields: a deployment hosting several
partners on different paths
+ // would otherwise be able to sign one partner's MDN with another
partner's key.
+ AS2SignatureAlgorithm signingAlgorithm = this.signingAlgorithm;
+ Certificate[] signingCertificateChain = this.signingCertificateChain;
+ PrivateKey signingPrivateKey = this.signingPrivateKey;
+ PrivateKey decryptingPrivateKey = this.decryptingPrivateKey;
+ Certificate[] validateSigningCertificateChain =
this.validateSigningCertificateChain;
+
if (this.keysAreDynamic) {
// Dynamically load path-specific security material from the
HttpContext,
// which was populated by AS2ConsumerConfigInterceptor.
- this.signingAlgorithm = (AS2SignatureAlgorithm)
context.getAttribute(AS2ServerConnection.AS2_SIGNING_ALGORITHM);
- this.signingCertificateChain
+ signingAlgorithm = (AS2SignatureAlgorithm)
context.getAttribute(AS2ServerConnection.AS2_SIGNING_ALGORITHM);
+ signingCertificateChain
= (Certificate[])
context.getAttribute(AS2ServerConnection.AS2_SIGNING_CERTIFICATE_CHAIN);
- this.signingPrivateKey = (PrivateKey)
context.getAttribute(AS2ServerConnection.AS2_SIGNING_PRIVATE_KEY);
- this.decryptingPrivateKey = (PrivateKey)
context.getAttribute(AS2ServerConnection.AS2_DECRYPTING_PRIVATE_KEY);
- this.validateSigningCertificateChain
+ signingPrivateKey = (PrivateKey)
context.getAttribute(AS2ServerConnection.AS2_SIGNING_PRIVATE_KEY);
+ decryptingPrivateKey = (PrivateKey)
context.getAttribute(AS2ServerConnection.AS2_DECRYPTING_PRIVATE_KEY);
+ validateSigningCertificateChain
= (Certificate[])
context.getAttribute(AS2ServerConnection.AS2_VALIDATE_SIGNING_CERTIFICATE_CHAIN);
}
diff --git
a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/protocol/ResponseMDNPerRequestKeysTest.java
b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/protocol/ResponseMDNPerRequestKeysTest.java
new file mode 100644
index 000000000000..8a5628c08f26
--- /dev/null
+++
b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/protocol/ResponseMDNPerRequestKeysTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.as2.api.protocol;
+
+import java.lang.reflect.Field;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+
+import org.apache.camel.component.as2.api.AS2ServerConnection;
+import org.apache.camel.component.as2.api.AS2SignatureAlgorithm;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.message.BasicHttpResponse;
+import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * A single {@link ResponseMDN} is registered on the shared {@code
HttpProcessor} and serves every request, so the
+ * security material resolved for one request must never be written back onto
the instance. A deployment hosting more
+ * than one partner on different paths, each with its own keys, would
otherwise be able to sign one partner's MDN with
+ * another partner's private key.
+ * <p/>
+ * The fields are {@code final}, which makes that structurally impossible;
this test states the property directly so the
+ * intent survives a refactor that changes how the material is carried.
+ */
+class ResponseMDNPerRequestKeysTest {
+
+ @Test
+ void perRequestKeysAreNotStoredOnTheSharedInstance() throws Exception {
+ // the three-arg constructor is the dynamic-keys form: material comes
from the context per request
+ ResponseMDN responseMDN = new ResponseMDN("1.1", "camel.apache.org",
null);
+
+ KeyPair keyPair =
KeyPairGenerator.getInstance("RSA").generateKeyPair();
+ HttpCoreContext context = HttpCoreContext.create();
+ context.setAttribute(AS2ServerConnection.AS2_SIGNING_ALGORITHM,
AS2SignatureAlgorithm.SHA256WITHRSA);
+ context.setAttribute(AS2ServerConnection.AS2_SIGNING_PRIVATE_KEY,
keyPair.getPrivate());
+ context.setAttribute(AS2ServerConnection.AS2_DECRYPTING_PRIVATE_KEY,
keyPair.getPrivate());
+
+ // 2xx so processing continues past the status check; no request on
the context so it returns
+ // straight after the material has been resolved, which is the point
under test
+ HttpResponse response = new BasicHttpResponse(200);
+ responseMDN.process(response, null, context);
+
+ assertThat(fieldValue(responseMDN, "signingAlgorithm")).isNull();
+ assertThat(fieldValue(responseMDN, "signingPrivateKey")).isNull();
+ assertThat(fieldValue(responseMDN,
"signingCertificateChain")).isNull();
+ assertThat(fieldValue(responseMDN, "decryptingPrivateKey")).isNull();
+ assertThat(fieldValue(responseMDN,
"validateSigningCertificateChain")).isNull();
+ }
+
+ private static Object fieldValue(ResponseMDN target, String name) throws
Exception {
+ Field field = ResponseMDN.class.getDeclaredField(name);
+ field.setAccessible(true);
+ return field.get(target);
+ }
+}