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

chibenwa pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git


The following commit(s) were added to refs/heads/master by this push:
     new 0a26ed2f8d [PERF] Allow using tcnative with Cassandra driver (#3181)
0a26ed2f8d is described below

commit 0a26ed2f8d169ec0d9628035c1f05d8632d385f9
Author: Benoit TELLIER <[email protected]>
AuthorDate: Mon Sep 14 16:22:00 2026 +0200

    [PERF] Allow using tcnative with Cassandra driver (#3181)
---
 .../backends/cassandra/init/ClusterFactory.java    |  22 +++
 .../cassandra/init/TcNativeSslEngineFactory.java   | 161 +++++++++++++++++++++
 .../pages/distributed/configure/cassandra.adoc     |   9 ++
 3 files changed, 192 insertions(+)

diff --git 
a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/ClusterFactory.java
 
b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/ClusterFactory.java
index a9b5a0223f..d3d25c1b04 100644
--- 
a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/ClusterFactory.java
+++ 
b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/ClusterFactory.java
@@ -23,12 +23,18 @@ import java.net.InetSocketAddress;
 
 import 
org.apache.james.backends.cassandra.init.configuration.ClusterConfiguration;
 import 
org.apache.james.backends.cassandra.init.configuration.KeyspaceConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.datastax.oss.driver.api.core.CqlSession;
 import com.datastax.oss.driver.api.core.CqlSessionBuilder;
+import com.datastax.oss.driver.api.core.config.DriverConfigLoader;
+import com.datastax.oss.driver.api.core.config.DriverExecutionProfile;
 import com.google.common.base.Preconditions;
 
 public class ClusterFactory {
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(ClusterFactory.class);
+    private static final boolean TCNATIVE_ENABLED = 
Boolean.parseBoolean(System.getProperty("james.cassandra.tcnative.enabled", 
"false"));
 
     public static CqlSession create(ClusterConfiguration configuration, 
KeyspaceConfiguration keyspaceConfiguration) {
         Preconditions.checkState(configuration.getUsername().isPresent() == 
configuration.getPassword().isPresent(), "If you specify username, you must 
specify password");
@@ -44,6 +50,7 @@ public class ClusterFactory {
                 sessionBuilder.withAuthCredentials(username, password)));
 
         
sessionBuilder.withLocalDatacenter(configuration.getLocalDC().orElse("datacenter1"));
+        configureTcNative(sessionBuilder);
 
         createKeyspace(keyspaceConfiguration, sessionBuilder);
 
@@ -73,6 +80,7 @@ public class ClusterFactory {
                 sessionBuilder.withAuthCredentials(username, password)));
 
         
sessionBuilder.withLocalDatacenter(configuration.getLocalDC().orElse("datacenter1"));
+        configureTcNative(sessionBuilder);
 
         CqlSession session = sessionBuilder.build();
 
@@ -85,6 +93,20 @@ public class ClusterFactory {
         }
     }
 
+    private static void configureTcNative(CqlSessionBuilder sessionBuilder) {
+        if (!TCNATIVE_ENABLED) {
+            return;
+        }
+        try (DriverConfigLoader configLoader = 
DriverConfigLoader.fromDefaults(ClusterFactory.class.getClassLoader())) {
+            DriverExecutionProfile profile = 
configLoader.getInitialConfig().getDefaultProfile();
+            if (TcNativeSslEngineFactory.supports(profile)) {
+                sessionBuilder.withSslEngineFactory(new 
TcNativeSslEngineFactory(profile));
+            } else {
+                LOGGER.warn("james.cassandra.tcnative.enabled ignored: 
requires advanced.ssl-engine-factory.class = DefaultSslEngineFactory without 
keystore-reload-interval");
+            }
+        }
+    }
+
     private static void createKeyspace(KeyspaceConfiguration 
keyspaceConfiguration, CqlSessionBuilder sessionBuilder) {
         CqlSession cqlSession = sessionBuilder.build();
         KeyspaceFactory.createKeyspace(keyspaceConfiguration, 
cqlSession).block();
diff --git 
a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/TcNativeSslEngineFactory.java
 
b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/TcNativeSslEngineFactory.java
new file mode 100644
index 0000000000..e78734cf37
--- /dev/null
+++ 
b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/TcNativeSslEngineFactory.java
@@ -0,0 +1,161 @@
+/****************************************************************
+ * 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.james.backends.cassandra.init;
+
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.KeyStore;
+import java.util.Optional;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.TrustManagerFactory;
+
+import com.datastax.oss.driver.api.core.config.DefaultDriverOption;
+import com.datastax.oss.driver.api.core.config.DriverExecutionProfile;
+import com.datastax.oss.driver.api.core.context.DriverContext;
+import com.datastax.oss.driver.api.core.metadata.EndPoint;
+import com.datastax.oss.driver.api.core.ssl.SslEngineFactory;
+import com.datastax.oss.driver.internal.core.ssl.DefaultSslEngineFactory;
+
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.handler.ssl.IdentityCipherSuiteFilter;
+import io.netty.handler.ssl.OpenSsl;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.SslProvider;
+
+/**
+ * BoringSSL (Netty tcnative) counterpart of the driver's {@link 
DefaultSslEngineFactory}, reading the same
+ * {@code advanced.ssl-engine-factory} options.
+ *
+ * <p>Keystore reloading ({@code keystore-reload-interval}) is not supported.
+ *
+ * <p>Requires {@code netty-tcnative-boringssl-static} on the runtime 
classpath.
+ */
+public class TcNativeSslEngineFactory implements SslEngineFactory {
+    public static boolean supports(DriverExecutionProfile profile) {
+        return profile.isDefined(DefaultDriverOption.SSL_ENGINE_FACTORY_CLASS)
+            && 
isDefaultSslEngineFactory(profile.getString(DefaultDriverOption.SSL_ENGINE_FACTORY_CLASS))
+            && 
!profile.isDefined(DefaultDriverOption.SSL_KEYSTORE_RELOAD_INTERVAL);
+    }
+
+    private static boolean isDefaultSslEngineFactory(String className) {
+        return className.equals(DefaultSslEngineFactory.class.getSimpleName())
+            || className.equals(DefaultSslEngineFactory.class.getName());
+    }
+
+    private final SslContext sslContext;
+    private final boolean requireHostnameValidation;
+    private final boolean allowDnsReverseLookupSan;
+
+    public TcNativeSslEngineFactory(DriverContext driverContext) {
+        this(driverContext.getConfig().getDefaultProfile());
+    }
+
+    public TcNativeSslEngineFactory(DriverExecutionProfile profile) {
+        OpenSsl.ensureAvailability();
+        try {
+            this.sslContext = buildContext(profile);
+        } catch (Exception e) {
+            throw new IllegalStateException("Cannot initialize SSL Context", 
e);
+        }
+        this.requireHostnameValidation = 
profile.getBoolean(DefaultDriverOption.SSL_HOSTNAME_VALIDATION, true);
+        this.allowDnsReverseLookupSan = 
profile.getBoolean(DefaultDriverOption.SSL_ALLOW_DNS_REVERSE_LOOKUP_SAN, true);
+    }
+
+    private static SslContext buildContext(DriverExecutionProfile profile) 
throws Exception {
+        SslContextBuilder builder = SslContextBuilder.forClient()
+            .sslProvider(SslProvider.OPENSSL);
+
+        Optional<KeyStore> trustStore = loadStore(profile, 
DefaultDriverOption.SSL_TRUSTSTORE_PATH, 
DefaultDriverOption.SSL_TRUSTSTORE_PASSWORD);
+        if (trustStore.isPresent()) {
+            TrustManagerFactory trustManagerFactory = 
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+            trustManagerFactory.init(trustStore.get());
+            builder.trustManager(trustManagerFactory);
+        }
+
+        Optional<KeyStore> keyStore = loadStore(profile, 
DefaultDriverOption.SSL_KEYSTORE_PATH, 
DefaultDriverOption.SSL_KEYSTORE_PASSWORD);
+        if (keyStore.isPresent()) {
+            KeyManagerFactory keyManagerFactory = 
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+            keyManagerFactory.init(keyStore.get(), password(profile, 
DefaultDriverOption.SSL_KEYSTORE_PASSWORD));
+            builder.keyManager(keyManagerFactory);
+        }
+
+        if (profile.isDefined(DefaultDriverOption.SSL_CIPHER_SUITES)) {
+            // IdentityCipherSuiteFilter: see TCNativeEncryptionFactory, the 
default filter strips TLS 1.3 suites
+            
builder.ciphers(profile.getStringList(DefaultDriverOption.SSL_CIPHER_SUITES), 
IdentityCipherSuiteFilter.INSTANCE);
+        }
+        return builder.build();
+    }
+
+    private static Optional<KeyStore> loadStore(DriverExecutionProfile 
profile, DefaultDriverOption pathOption, DefaultDriverOption passwordOption) 
throws Exception {
+        if (!profile.isDefined(pathOption)) {
+            return Optional.empty();
+        }
+        try (InputStream inputStream = 
Files.newInputStream(Paths.get(profile.getString(pathOption)))) {
+            KeyStore keyStore = KeyStore.getInstance("JKS");
+            keyStore.load(inputStream, password(profile, passwordOption));
+            return Optional.of(keyStore);
+        }
+    }
+
+    private static char[] password(DriverExecutionProfile profile, 
DefaultDriverOption passwordOption) {
+        if (profile.isDefined(passwordOption)) {
+            return profile.getString(passwordOption).toCharArray();
+        }
+        return null;
+    }
+
+    @Override
+    public SSLEngine newSslEngine(EndPoint remoteEndpoint) {
+        SSLEngine engine = createEngine(remoteEndpoint.resolve());
+        if (requireHostnameValidation) {
+            SSLParameters parameters = engine.getSSLParameters();
+            parameters.setEndpointIdentificationAlgorithm("HTTPS");
+            engine.setSSLParameters(parameters);
+        }
+        return engine;
+    }
+
+    private SSLEngine createEngine(SocketAddress remoteAddress) {
+        if (remoteAddress instanceof InetSocketAddress inetAddress) {
+            return sslContext.newEngine(ByteBufAllocator.DEFAULT, 
hostname(inetAddress), inetAddress.getPort());
+        }
+        return sslContext.newEngine(ByteBufAllocator.DEFAULT);
+    }
+
+    private String hostname(InetSocketAddress address) {
+        if (allowDnsReverseLookupSan) {
+            return address.getHostName();
+        }
+        return address.getHostString();
+    }
+
+    @Override
+    public void close() {
+        // SslProvider.OPENSSL contexts and engines are released by the GC: 
nothing to close, and the
+        // factory stays usable when ClusterFactory reuses its session builder
+    }
+}
diff --git a/docs/modules/servers/pages/distributed/configure/cassandra.adoc 
b/docs/modules/servers/pages/distributed/configure/cassandra.adoc
index 9821b4d0cc..c7ce210d63 100644
--- a/docs/modules/servers/pages/distributed/configure/cassandra.adoc
+++ b/docs/modules/servers/pages/distributed/configure/cassandra.adoc
@@ -54,6 +54,15 @@ Falls back to default read consistency level if the blob is 
missing. Defaults to
 
 |===
 
+== Native TLS (tcnative)
+
+Set `james.cassandra.tcnative.enabled=true` in *jvm.properties* to encrypt 
Cassandra client connections with BoringSSL
+(Netty tcnative) instead of the JDK. This requires 
`netty-tcnative-boringssl-static` on the classpath.
+
+It only applies when *cassandra-driver.conf* uses 
`advanced.ssl-engine-factory.class = DefaultSslEngineFactory` without
+`keystore-reload-interval`. The truststore, keystore, hostname validation and 
cipher suite options stay the same.
+Otherwise, a warning is logged and the JDK implementation is kept.
+
 == Cassandra Mailbox Configuration
 
 *cassandra.properties* file furthermore expose some options to tune the 
Cassandra Mailbox behaviour.


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to