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

Croway pushed a commit to branch camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/camel-4.22.x by this push:
     new ab5eacd3fa48 CAMEL-24429: camel-as2 - confine per-request signing keys 
to the request in ResponseMDN
ab5eacd3fa48 is described below

commit ab5eacd3fa485eb990cf021dc56e48465c060d39
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 3e1bd3e1db93..456968658fb7 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 59ad21bf618e..81d96b8f3a0f 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
@@ -92,12 +92,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;
@@ -111,6 +114,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,
@@ -130,6 +138,7 @@ public class ResponseMDN implements HttpResponseInterceptor 
{
             this.mdnMessageTemplate = DEFAULT_MDN_MESSAGE_TEMPLATE;
         }
         this.validateSigningCertificateChain = validateSigningCertificateChain;
+        this.keysAreDynamic = false;
     }
 
     @Override
@@ -144,15 +153,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);
+    }
+}

Reply via email to