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 ba02ad20742f CAMEL-24474: camel-as2 - support TLS delivery of 
asynchronous MDNs via SSLContextParameters
ba02ad20742f is described below

commit ba02ad20742f7a3ebd06d44a89d0aadb494b4f82
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Sep 7 17:22:16 2026 +0200

    CAMEL-24474: camel-as2 - support TLS delivery of asynchronous MDNs via 
SSLContextParameters
    
    AS2AsynchronousMDNManager delivered asynchronous MDNs over a plain Socket
    with no TLS, so CAMEL-24417 refused https delivery addresses outright.
    This threads the endpoint's SSLContextParameters (already resolved into
    the SSLContext used by AS2ServerConnection for the inbound listener) to
    the manager: an https delivery address is now served via an SSLSocket
    with hostname verification (HTTPS endpoint identification) and an explicit
    handshake before the MDN and credentials are written. https remains
    fail-closed when no SSLContextParameters is configured, preserving the
    CAMEL-24417 behaviour for deployments that have not opted in.
    
    Closes #26164
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
 .../as2/api/AS2AsynchronousMDNManager.java         | 60 +++++++++++++++++++---
 .../component/as2/api/AS2ServerConnection.java     |  5 +-
 ...2AsynchronousMDNManagerDeliveryAddressTest.java | 49 ++++++++++++++----
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    | 11 ++--
 4 files changed, 101 insertions(+), 24 deletions(-)

diff --git 
a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2AsynchronousMDNManager.java
 
b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2AsynchronousMDNManager.java
index 8cf1c9a30dc6..22c28c16286c 100644
--- 
a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2AsynchronousMDNManager.java
+++ 
b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2AsynchronousMDNManager.java
@@ -23,6 +23,10 @@ import java.security.PrivateKey;
 import java.security.cert.Certificate;
 import java.util.Locale;
 
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSocket;
+
 import org.apache.camel.component.as2.api.entity.MultipartMimeEntity;
 import org.apache.camel.component.as2.api.protocol.RequestAsynchronousMDN;
 import org.apache.camel.component.as2.api.util.AS2HeaderUtils;
@@ -104,6 +108,7 @@ public class AS2AsynchronousMDNManager {
     private String password;
     private String accessToken;
     private String allowedHosts;
+    private SSLContext sslContext;
 
     /**
      * @deprecated use
@@ -133,7 +138,22 @@ public class AS2AsynchronousMDNManager {
                                      String password,
                                      String accessToken,
                                      String allowedHosts) {
+        this(as2Version, userAgent, senderFQDN, signingCertificateChain, 
signingPrivateKey, userName, password,
+             accessToken, allowedHosts, null);
+    }
+
+    public AS2AsynchronousMDNManager(String as2Version,
+                                     String userAgent,
+                                     String senderFQDN,
+                                     Certificate[] signingCertificateChain,
+                                     PrivateKey signingPrivateKey,
+                                     String userName,
+                                     String password,
+                                     String accessToken,
+                                     String allowedHosts,
+                                     SSLContext sslContext) {
         this.allowedHosts = allowedHosts;
+        this.sslContext = sslContext;
         this.signingCertificateChain = signingCertificateChain;
         this.signingPrivateKey = signingPrivateKey;
         this.userName = userName;
@@ -161,19 +181,24 @@ public class AS2AsynchronousMDNManager {
         // header), so it is untrusted input that selects an outbound 
destination.
         URI uri = URI.create(recipientDeliveryAddress);
         String scheme = uri.getScheme() == null ? null : 
uri.getScheme().toLowerCase(Locale.US);
-        // Only http. This class delivers over a plain Socket and has no TLS 
of any kind, so accepting https
-        // would mean writing the request - including the Authorization header 
- in cleartext to the TLS port.
-        // https delivery has never worked here for that reason, so refusing 
it removes nothing that functioned.
-        if (!"http".equals(scheme)) {
+        // The delivery scheme is sender-chosen (untrusted input). Plain http 
is always allowed; https is allowed
+        // only when the AS2 endpoint has SSLContextParameters configured, 
otherwise it is refused (fail-closed)
+        // rather than written in cleartext to the TLS port. Any other scheme 
is rejected.
+        boolean https = "https".equals(scheme);
+        if (!"http".equals(scheme) && !https) {
+            throw new HttpException(
+                    "Refusing to deliver the asynchronous MDN: the delivery 
address must use http or https");
+        }
+        if (https && sslContext == null) {
             throw new HttpException(
-                    "Refusing to deliver the asynchronous MDN: the delivery 
address must use http."
-                                    + " TLS delivery of asynchronous MDNs is 
not supported");
+                    "Refusing to deliver the asynchronous MDN over https: no 
SSLContextParameters are configured"
+                                    + " on the AS2 endpoint");
         }
         String host = normalizeHost(uri.getHost());
         if (host == null) {
             throw new HttpException("Refusing to deliver the asynchronous MDN: 
the delivery address has no host");
         }
-        int port = uri.getPort() != -1 ? uri.getPort() : 80;
+        int port = uri.getPort() != -1 ? uri.getPort() : (https ? 443 : 80);
 
         boolean hostIsAllowed = isAllowedHost(host);
         if (allowedHosts != null && !allowedHosts.isBlank() && !hostIsAllowed) 
{
@@ -187,7 +212,7 @@ public class AS2AsynchronousMDNManager {
         HttpConnectionFactory<ManagedHttpClientConnection> connFactory
                 = 
ManagedHttpClientConnectionFactory.builder().http1Config(h1Config).build();
 
-        try (HttpClientConnection httpConnection = 
connFactory.createConnection(new Socket(host, port))) {
+        try (HttpClientConnection httpConnection = 
connFactory.createConnection(createSocket(https, host, port))) {
 
             // Add Context attributes
             HttpCoreContext httpContext = HttpCoreContext.create();
@@ -223,6 +248,25 @@ public class AS2AsynchronousMDNManager {
         }
     }
 
+    private Socket createSocket(boolean https, String host, int port) throws 
IOException {
+        if (!https) {
+            return new Socket(host, port);
+        }
+        SSLSocket sslSocket = (SSLSocket) 
sslContext.getSocketFactory().createSocket(host, port);
+        try {
+            // verify the delivery host against the certificate the peer 
presents during the handshake
+            SSLParameters sslParameters = sslSocket.getSSLParameters();
+            sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
+            sslSocket.setSSLParameters(sslParameters);
+            // handshake now so a certificate or hostname mismatch fails 
before the MDN and any credentials are written
+            sslSocket.startHandshake();
+        } catch (IOException e) {
+            sslSocket.close();
+            throw e;
+        }
+        return sslSocket;
+    }
+
     /**
      * {@link URI#getHost()} returns an IPv6 literal in its bracketed form 
({@code [::1]}), which would never match an
      * allow-list entry written the way an operator writes it. Compare the 
address itself.
diff --git 
a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ServerConnection.java
 
b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ServerConnection.java
index e3c5349a6e97..e11d877b58d4 100644
--- 
a/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ServerConnection.java
+++ 
b/components/camel-as2/camel-as2-api/src/main/java/org/apache/camel/component/as2/api/AS2ServerConnection.java
@@ -93,6 +93,7 @@ public class AS2ServerConnection {
     private final String password;
     private final String accessToken;
     private final String asyncMdnAllowedHosts;
+    private final SSLContext sslContext;
 
     /**
      * Stores the configuration for each consumer endpoint path (e.g., 
"/consumerA"). Uses LinkedHashMap to preserve
@@ -532,7 +533,8 @@ public class AS2ServerConnection {
                                 AS2ServerConnection.this.userName,
                                 AS2ServerConnection.this.password,
                                 AS2ServerConnection.this.accessToken,
-                                AS2ServerConnection.this.asyncMdnAllowedHosts);
+                                AS2ServerConnection.this.asyncMdnAllowedHosts,
+                                AS2ServerConnection.this.sslContext);
 
                         HttpRequest request = coreContext.getRequest();
                         AS2SignedDataGenerator gen = 
ResponseMDN.createSigningGenerator(
@@ -613,6 +615,7 @@ public class AS2ServerConnection {
                                String asyncMdnAllowedHosts)
                                                             throws IOException 
{
         this.asyncMdnAllowedHosts = asyncMdnAllowedHosts;
+        this.sslContext = sslContext;
         this.as2Version = ObjectHelper.notNull(as2Version, "as2Version");
         this.originServer = ObjectHelper.notNull(originServer, "userAgent");
         this.serverFqdn = ObjectHelper.notNull(serverFqdn, "serverFqdn");
diff --git 
a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2AsynchronousMDNManagerDeliveryAddressTest.java
 
b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2AsynchronousMDNManagerDeliveryAddressTest.java
index 09ea7b6cca42..ccf5c08513d7 100644
--- 
a/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2AsynchronousMDNManagerDeliveryAddressTest.java
+++ 
b/components/camel-as2/camel-as2-api/src/test/java/org/apache/camel/component/as2/api/AS2AsynchronousMDNManagerDeliveryAddressTest.java
@@ -16,6 +16,8 @@
  */
 package org.apache.camel.component.as2.api;
 
+import javax.net.ssl.SSLContext;
+
 import org.apache.camel.component.as2.api.entity.MultipartMimeEntity;
 import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.HttpException;
@@ -33,24 +35,47 @@ class AS2AsynchronousMDNManagerDeliveryAddressTest {
 
     private static final String ALLOWED = "partner.example";
 
-    /**
-     * https in particular: this class delivers over a plain {@link 
java.net.Socket} and has no TLS, so accepting an
-     * https address would write the request - including the Authorization 
header - in cleartext to the TLS port. Before
-     * the delivery address was validated at all, an https address without an 
explicit port failed closed on
-     * {@code new Socket(host, -1)}; resolving it to 443 instead would have 
been a regression.
-     */
     @Test
-    void aSchemeOtherThanHttpIsRefused() {
+    void aSchemeOtherThanHttpOrHttpsIsRefused() {
         for (String address : new String[] {
-                "https://partner.example/receipts";, 
"https://partner.example:443/receipts";,
                 "file:///etc/passwd", "ftp://partner.example/x";, 
"gopher://partner.example:70/x";, "//partner.example/x" }) {
             HttpException e = assertThrows(HttpException.class, () -> 
deliver(address, ALLOWED),
                     "expected " + address + " to be refused");
-            assertTrue(e.getMessage().contains("must use http") || 
e.getMessage().contains("no host"),
+            assertTrue(e.getMessage().contains("must use http or https") || 
e.getMessage().contains("no host"),
+                    "unexpected message for " + address + ": " + 
e.getMessage());
+        }
+    }
+
+    /**
+     * https is fail-closed: without SSLContextParameters the manager has no 
way to deliver over TLS, so it must refuse
+     * rather than write the MDN and the Authorization header in cleartext to 
the TLS port. This preserves the behaviour
+     * introduced by CAMEL-24417 for a deployment that has not opted in.
+     */
+    @Test
+    void httpsWithoutAnSslContextIsRefused() {
+        for (String address : new String[] {
+                "https://partner.example/receipts";, 
"https://partner.example:443/receipts"; }) {
+            HttpException e = assertThrows(HttpException.class, () -> 
deliver(address, ALLOWED),
+                    "expected " + address + " to be refused without an 
SSLContext");
+            assertTrue(e.getMessage().contains("https") && 
e.getMessage().contains("SSLContextParameters"),
                     "unexpected message for " + address + ": " + 
e.getMessage());
         }
     }
 
+    /**
+     * With an SSLContext configured, an https address is accepted: it gets 
past the scheme checks and fails on the
+     * connection instead, which is what tells us the address itself was 
accepted.
+     */
+    @Test
+    void httpsWithAnSslContextIsAccepted() throws Exception {
+        SSLContext sslContext = SSLContext.getInstance("TLS");
+        sslContext.init(null, null, null);
+        Exception e = assertThrows(Exception.class,
+                () -> deliver("https://localhost:1/receipts";, ALLOWED, 
sslContext));
+        assertTrue(!(e instanceof HttpException) || 
!e.getMessage().contains("must use http"),
+                "an https address must be accepted when an SSLContext is 
configured: " + e.getMessage());
+    }
+
     @Test
     void aHostOutsideTheAllowListIsRefused() {
         HttpException e = assertThrows(HttpException.class,
@@ -79,8 +104,12 @@ class AS2AsynchronousMDNManagerDeliveryAddressTest {
     }
 
     private static void deliver(String deliveryAddress, String allowedHosts) 
throws Exception {
+        deliver(deliveryAddress, allowedHosts, null);
+    }
+
+    private static void deliver(String deliveryAddress, String allowedHosts, 
SSLContext sslContext) throws Exception {
         AS2AsynchronousMDNManager manager = new AS2AsynchronousMDNManager(
-                "1.1", "Camel", "sender.example.com", null, null, "user", 
"password", null, allowedHosts);
+                "1.1", "Camel", "sender.example.com", null, null, "user", 
"password", null, allowedHosts, sslContext);
         manager.send(new TestEntity(), AS2MimeType.MULTIPART_REPORT, 
deliveryAddress);
     }
 
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index fde4a2e87878..09de0d03b17f 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -764,12 +764,13 @@ Deployments that rely on authenticating to a partner's 
asynchronous MDN endpoint
 partner's host to `asyncMdnAllowedHosts`.
 
 Two further checks are applied to the delivery address regardless of the 
option: the scheme must be
-`http`, and an address with no explicit port now uses 80 rather than being 
passed to the socket as `-1`.
+`http` or `https`, and an address with no explicit port now uses 80 for `http` 
(443 for `https`) rather
+than being passed to the socket as `-1`.
 
-`https` is refused. `AS2AsynchronousMDNManager` delivers over a plain socket 
and has no TLS support, so an
-`https` address was never actually delivered over TLS — the request was 
written in cleartext to the TLS
-port and the peer reset the connection. Such an address is now refused 
outright rather than attempted, and
-TLS delivery of asynchronous MDNs remains unsupported.
+`https` delivery is supported when the AS2 endpoint has `sslContextParameters` 
configured: the manager
+builds an `SSLSocket` from it, defaults the port to 443, and verifies the 
delivery host against the
+certificate the peer presents. When no `sslContextParameters` is configured an 
`https` address is refused
+(fail-closed) rather than written in cleartext to the TLS port.
 
 === camel-ibm-cos
 

Reply via email to