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