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

robbie pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/activemq-artemis.git


The following commit(s) were added to refs/heads/main by this push:
     new 8e9337b741 ARTEMIS-5738 Improve LDAPLoginModule ssl support
8e9337b741 is described below

commit 8e9337b74120ca1d683115569b12f88ec252072f
Author: Domenico Francesco Bruscino <[email protected]>
AuthorDate: Mon Nov 3 12:01:18 2025 +0100

    ARTEMIS-5738 Improve LDAPLoginModule ssl support
---
 .../spi/core/security/jaas/LDAPLoginModule.java    |  21 ++
 .../security/jaas/LDAPLoginSSLSocketFactory.java   | 185 ++++++++++++++++
 .../core/security/jaas/LDAPLoginModuleTest.java    | 232 +++++++++++++++++++-
 .../jaas/LDAPLoginSSLSocketFactoryTest.java        | 242 +++++++++++++++++++++
 docs/user-manual/security.adoc                     | 101 +++++++++
 tests/security-resources/build.sh                  |   1 +
 .../server-keystore-without-ca.p12                 | Bin 0 -> 3992 bytes
 7 files changed, 777 insertions(+), 5 deletions(-)

diff --git 
a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java
 
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java
index b7e2e10989..6118cca888 100644
--- 
a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java
+++ 
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java
@@ -71,6 +71,20 @@ public class LDAPLoginModule implements AuditLoginModule {
 
    private static final Logger logger = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
+   private static final ThreadLocal<Map<String, String>> 
environmentThreadLocal = new ThreadLocal<>();
+
+   public static Map<String, String> getEnvironment() {
+      return environmentThreadLocal.get();
+   }
+
+   protected static void setEnvironment(Map<String, String> environment) {
+      environmentThreadLocal.set(environment);
+   }
+
+   protected static void removeEnvironment() {
+      environmentThreadLocal.remove();
+   }
+
    enum ConfigKey {
 
       DEBUG("debug"),
@@ -708,6 +722,7 @@ public class LDAPLoginModule implements AuditLoginModule {
 
             extendInitialEnvironment(config, env);
 
+            setEnvironment(env);
             try {
                context = SecurityManagerShim.callAs(brokerGssapiIdentity, 
(Callable<DirContext>) () -> new InitialDirContext(env));
             } catch (CompletionException ce) {
@@ -717,6 +732,8 @@ public class LDAPLoginModule implements AuditLoginModule {
                }
 
                throw ce;
+            } finally {
+               removeEnvironment();
             }
          } catch (NamingException e) {
             closeContext();
@@ -736,6 +753,10 @@ public class LDAPLoginModule implements AuditLoginModule {
             initialContextEnv.put(propName, prop.getPropertyValue());
          }
       }
+
+      if (codecClass != null) {
+         initialContextEnv.put(ConfigKey.PASSWORD_CODEC.getName(), codecClass);
+      }
    }
 
    private String getLDAPPropertyValue(ConfigKey key) {
diff --git 
a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginSSLSocketFactory.java
 
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginSSLSocketFactory.java
new file mode 100644
index 0000000000..ed3014553b
--- /dev/null
+++ 
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginSSLSocketFactory.java
@@ -0,0 +1,185 @@
+/*
+ * 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.activemq.artemis.spi.core.security.jaas;
+
+import 
org.apache.activemq.artemis.core.remoting.impl.ssl.CachingSSLContextFactory;
+import org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextConfig;
+import org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextFactory;
+import org.apache.activemq.artemis.utils.PasswordMaskingUtil;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+
+public class LDAPLoginSSLSocketFactory extends SocketFactory {
+
+   private static final String KEYSTORE_PROVIDER = "keystoreProvider";
+   private static final String KEYSTORE_TYPE = "keystoreType";
+   private static final String KEYSTORE_PATH = "keystorePath";
+   private static final String KEYSTORE_PASSWORD = "keystorePassword";
+   private static final String KEYSTORE_ALIAS = "keystoreAlias";
+   private static final String TRUSTSTORE_PROVIDER = "truststoreProvider";
+   private static final String TRUSTSTORE_TYPE = "truststoreType";
+   private static final String TRUSTSTORE_PATH = "truststorePath";
+   private static final String TRUSTSTORE_PASSWORD = "truststorePassword";
+   private static final String CRL_PATH = "crlPath";
+   private static final String TRUST_ALL = "trustAll";
+   private static final String TRUST_MANAGER_FACTORY_PLUGIN = 
"trustManagerFactoryPlugin";
+
+   private static final SSLContextFactory sslContextFactory = new 
CachingSSLContextFactory();
+
+   protected static SSLContextFactory getSSLContextFactory() {
+      return sslContextFactory;
+   }
+
+   public static LDAPLoginSSLSocketFactory getDefault() {
+      return new LDAPLoginSSLSocketFactory();
+   }
+
+   private final String codecClass;
+   private final Map<String, String> environment;
+
+   private SSLSocketFactory sslSocketFactory;
+
+   public LDAPLoginSSLSocketFactory() {
+      this(LDAPLoginModule.getEnvironment());
+   }
+
+   public LDAPLoginSSLSocketFactory(Map<String, String> environment) {
+      Objects.requireNonNull(environment, "LDAPLoginModule environment is 
null");
+      this.environment = environment;
+
+      codecClass = 
environment.get(LDAPLoginModule.ConfigKey.PASSWORD_CODEC.getName());
+   }
+
+   protected void loadSSLSocketFactory() {
+      final SSLContext sslContext = getSSLContext();
+
+      sslSocketFactory = sslContext.getSocketFactory();
+   }
+
+   protected SSLContext getSSLContext() {
+      final SSLContextConfig sslContextConfig = getSSLContextConfig();
+
+      try {
+         return sslContextFactory.getSSLContext(sslContextConfig,
+            Collections.unmodifiableMap(environment));
+      } catch (Exception e) {
+         throw new IllegalStateException("Error getting the ssl context", e);
+      }
+   }
+
+   protected SSLContextConfig getSSLContextConfig() {
+      SSLContextConfig.Builder sslContextConfigBuilder = 
SSLContextConfig.builder();
+
+      if (environment.containsKey(KEYSTORE_PROVIDER)) {
+         
sslContextConfigBuilder.keystoreProvider(environment.get(KEYSTORE_PROVIDER));
+      }
+      if (environment.containsKey(KEYSTORE_TYPE)) {
+         sslContextConfigBuilder.keystoreType(environment.get(KEYSTORE_TYPE));
+      }
+      if (environment.containsKey(KEYSTORE_PATH)) {
+         sslContextConfigBuilder.keystorePath(environment.get(KEYSTORE_PATH));
+      }
+      if (environment.containsKey(KEYSTORE_PASSWORD)) {
+         
sslContextConfigBuilder.keystorePassword(getSensitiveText(KEYSTORE_PASSWORD));
+      }
+      if (environment.containsKey(KEYSTORE_ALIAS)) {
+         
sslContextConfigBuilder.keystoreAlias(environment.get(KEYSTORE_ALIAS));
+      }
+      if (environment.containsKey(TRUSTSTORE_PROVIDER)) {
+         
sslContextConfigBuilder.truststoreProvider(environment.get(TRUSTSTORE_PROVIDER));
+      }
+      if (environment.containsKey(TRUSTSTORE_TYPE)) {
+         
sslContextConfigBuilder.truststoreType(environment.get(TRUSTSTORE_TYPE));
+      }
+      if (environment.containsKey(TRUSTSTORE_PATH)) {
+         
sslContextConfigBuilder.truststorePath(environment.get(TRUSTSTORE_PATH));
+      }
+      if (environment.containsKey(TRUSTSTORE_PASSWORD)) {
+         
sslContextConfigBuilder.truststorePassword(getSensitiveText(TRUSTSTORE_PASSWORD));
+      }
+      if (environment.containsKey(CRL_PATH)) {
+         sslContextConfigBuilder.crlPath(environment.get(CRL_PATH));
+      }
+      if (environment.containsKey(TRUST_ALL)) {
+         
sslContextConfigBuilder.trustAll(Boolean.parseBoolean(environment.get(TRUST_ALL)));
+      }
+      if (environment.containsKey(TRUST_MANAGER_FACTORY_PLUGIN)) {
+         
sslContextConfigBuilder.trustManagerFactoryPlugin(environment.get(TRUST_MANAGER_FACTORY_PLUGIN));
+      }
+
+      return sslContextConfigBuilder.build();
+   }
+
+   protected String getSensitiveText(String key) {
+      try {
+         String text = environment.get(key);
+         return PasswordMaskingUtil.resolveMask(text, codecClass);
+      } catch (Exception e) {
+         throw new IllegalArgumentException("Failed to parse sensitive text " 
+ key, e);
+      }
+   }
+
+   private void checkSSLSocketFactory() {
+      if (sslSocketFactory == null) {
+         loadSSLSocketFactory();
+      }
+   }
+
+   @Override
+   public Socket createSocket() throws IOException {
+      checkSSLSocketFactory();
+
+      return sslSocketFactory.createSocket();
+   }
+
+   @Override
+   public Socket createSocket(String host, int port) throws IOException, 
UnknownHostException {
+      checkSSLSocketFactory();
+
+      return sslSocketFactory.createSocket(host, port);
+   }
+
+   @Override
+   public Socket createSocket(String host, int port, InetAddress localHost, 
int localPort) throws IOException, UnknownHostException {
+      checkSSLSocketFactory();
+
+      return sslSocketFactory.createSocket(host, port, localHost, localPort);
+   }
+
+   @Override
+   public Socket createSocket(InetAddress host, int port) throws IOException {
+      checkSSLSocketFactory();
+
+      return sslSocketFactory.createSocket(host, port);
+   }
+
+   @Override
+   public Socket createSocket(InetAddress address, int port, InetAddress 
localAddress, int localPort) throws IOException {
+      checkSSLSocketFactory();
+
+      return sslSocketFactory.createSocket(address, port, localAddress, 
localPort);
+   }
+}
diff --git 
a/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModuleTest.java
 
b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModuleTest.java
index e6a45c0aed..681cccb48b 100644
--- 
a/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModuleTest.java
+++ 
b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModuleTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.activemq.artemis.spi.core.security.jaas;
 
+import javax.naming.CommunicationException;
 import javax.naming.Context;
 import javax.naming.NameClassPair;
 import javax.naming.NamingEnumeration;
@@ -32,19 +33,25 @@ import javax.security.auth.spi.LoginModule;
 import java.lang.invoke.MethodHandles;
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Hashtable;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
-import org.apache.directory.server.annotations.CreateLdapServer;
-import org.apache.directory.server.annotations.CreateTransport;
+import org.apache.activemq.artemis.utils.PasswordMaskingUtil;
+import org.apache.activemq.artemis.utils.SensitiveDataCodec;
 import org.apache.directory.server.core.annotations.ApplyLdifFiles;
+import org.apache.directory.server.core.api.DirectoryService;
 import org.apache.directory.server.core.integ.AbstractLdapTestUnit;
 import org.apache.directory.server.core.integ.FrameworkRunner;
+import org.apache.directory.server.ldap.LdapServer;
+import org.apache.directory.server.protocol.shared.transport.TcpTransport;
 import org.junit.After;
+import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -58,7 +65,6 @@ import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 @RunWith(FrameworkRunner.class)
-@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP", port = 
1024)}, allowAnonymousAccess = true)
 @ApplyLdifFiles("test.ldif")
 public class LDAPLoginModuleTest extends AbstractLdapTestUnit {
 
@@ -67,13 +73,37 @@ public class LDAPLoginModuleTest extends 
AbstractLdapTestUnit {
    private static final String PRINCIPAL = "uid=admin,ou=system";
    private static final String CREDENTIALS = "secret";
 
+   private static LdapServer ldapServer;
+
    private final String loginConfigSysPropName = 
"java.security.auth.login.config";
    private String oldLoginConfig;
 
    @Before
-   public void setLoginConfigSysProperty() {
+   public void setUp() throws Exception {
       oldLoginConfig = System.getProperty(loginConfigSysPropName, null);
       System.setProperty(loginConfigSysPropName, 
"src/test/resources/login.config");
+
+      if (ldapServer == null) {
+         ldapServer = new LdapServer();
+
+         TcpTransport ldapTcpTransport = new TcpTransport(1024);
+         ldapServer.addTransports(ldapTcpTransport);
+
+         TcpTransport ldapsTcpTransport = new TcpTransport(1025);
+         ldapsTcpTransport.setEnableSSL(true);
+         ldapServer.addTransports(ldapsTcpTransport);
+
+         DirectoryService directoryService = getService();
+         directoryService.setAllowAnonymousAccess(true);
+         ldapServer.setDirectoryService(directoryService);
+
+         String keystore = 
Objects.requireNonNull(this.getClass().getClassLoader().
+            getResource("server-keystore-without-ca.p12")).getFile();
+         ldapServer.setKeystoreFile(keystore);
+         ldapServer.setCertificatePassword("securepass");
+
+         ldapServer.start();
+      }
    }
 
    @After
@@ -83,6 +113,13 @@ public class LDAPLoginModuleTest extends 
AbstractLdapTestUnit {
       }
    }
 
+   @AfterClass
+   public static void stopLDAPServer() {
+      if (ldapServer != null) {
+         ldapServer.stop();
+      }
+   }
+
    @Test
    public void testRunning() throws Exception {
 
@@ -111,6 +148,50 @@ public class LDAPLoginModuleTest extends 
AbstractLdapTestUnit {
 
    }
 
+   @Test
+   public void testRunningSSL() throws Exception {
+      Hashtable<String, Object> env = new Hashtable<>();
+      env.put(Context.PROVIDER_URL, "ldaps://localhost:1025");
+      env.put(Context.INITIAL_CONTEXT_FACTORY, 
"com.sun.jndi.ldap.LdapCtxFactory");
+      env.put(Context.SECURITY_AUTHENTICATION, "simple");
+      env.put(Context.SECURITY_PRINCIPAL, PRINCIPAL);
+      env.put(Context.SECURITY_CREDENTIALS, CREDENTIALS);
+
+      // Uncomment to enable SSL debugging
+      // System.setProperty("-Djavax.net.debug", "all");
+      System.setProperty("javax.net.ssl.trustStore", 
Objects.requireNonNull(this.getClass().
+         getClassLoader().getResource("server-ca-truststore.p12")).getFile());
+      System.setProperty("javax.net.ssl.trustStorePassword", "securepass");
+
+      DirContext ctx = null;
+
+      try {
+         ctx = new InitialDirContext(env);
+
+         Set<String> set = new HashSet<>();
+
+         NamingEnumeration<NameClassPair> list = ctx.list("ou=system");
+
+         while (list.hasMore()) {
+            NameClassPair ncp = list.next();
+            set.add(ncp.getName());
+         }
+
+         assertTrue(set.contains("uid=admin"));
+         assertTrue(set.contains("ou=users"));
+         assertTrue(set.contains("ou=groups"));
+         assertTrue(set.contains("ou=configuration"));
+         assertTrue(set.contains("prefNodeName=sysPrefRoot"));
+      } finally {
+         System.clearProperty("javax.net.ssl.trustStore");
+         System.clearProperty("javax.net.ssl.trustStorePassword");
+
+         if (ctx != null) {
+            ctx.close();
+         }
+      }
+   }
+
    @Test
    public void testLogin() throws Exception {
       logger.info("num session: {}", 
ldapServer.getLdapSessionManager().getSessions().length);
@@ -317,6 +398,8 @@ public class LDAPLoginModuleTest extends 
AbstractLdapTestUnit {
             options.put(configKey.getName(), "s");
          } else if (configKey.getName().equals("debug")) {
             options.put(configKey.getName(), "true");
+         } else if (configKey.getName().equals("passwordCodec")) {
+            options.put(configKey.getName(), "my.password.Codec");
          } else {
             options.put(configKey.getName(), configKey.getName() + 
"_value_set");
          }
@@ -342,7 +425,9 @@ public class LDAPLoginModuleTest extends 
AbstractLdapTestUnit {
 
       // module config keys should not be passed to environment
       for (LDAPLoginModule.ConfigKey configKey: 
LDAPLoginModule.ConfigKey.values()) {
-         assertNull("value should not be set for key: " + configKey.getName(), 
environment.get(configKey.getName()));
+         if (!LDAPLoginModule.ConfigKey.PASSWORD_CODEC.equals(configKey)) {
+            assertNull("value should not be set for key: " + 
configKey.getName(), environment.get(configKey.getName()));
+         }
       }
 
       // extra, non-module configs should be passed to environment
@@ -363,6 +448,9 @@ public class LDAPLoginModuleTest extends 
AbstractLdapTestUnit {
       assertEquals("value should be set for key: " + 
Context.SECURITY_PRINCIPAL, PRINCIPAL, 
environment.get(Context.SECURITY_PRINCIPAL));
       assertEquals("value should be set for key: " + 
Context.SECURITY_CREDENTIALS, CREDENTIALS, 
environment.get(Context.SECURITY_CREDENTIALS));
       assertEquals("value should be set for key: " + 
Context.SECURITY_PROTOCOL, "s", environment.get(Context.SECURITY_PROTOCOL));
+
+      // passwordCodec should be set
+      assertEquals("value should be set for key: " + "passwordCodec", 
"my.password.Codec", environment.get("passwordCodec"));
    }
 
    private boolean presentIn(Set<LDAPLoginProperty> ldapProps, String 
propertyName) {
@@ -373,4 +461,138 @@ public class LDAPLoginModuleTest extends 
AbstractLdapTestUnit {
       return false;
    }
 
+   @Test
+   public void testLDAPLoginSSLSocketFactoryWithTruststore() throws Exception {
+      Map<String, Object> extraOptions = new HashMap<>();
+      extraOptions.put("truststorePath", 
Objects.requireNonNull(this.getClass().
+         getClassLoader().getResource("server-ca-truststore.jks")).getFile());
+      extraOptions.put("truststorePassword", "securepass");
+
+      testLDAPSConnectionWithLDAPLoginSSLSocketFactory(extraOptions, true);
+   }
+
+   @Test
+   public void 
testLDAPLoginSSLSocketFactoryWithDefaultPasswordCodecAndTruststore() throws 
Exception {
+      Map<String, Object> extraOptions = new HashMap<>();
+      extraOptions.put("truststorePath", 
Objects.requireNonNull(this.getClass().
+         getClassLoader().getResource("server-ca-truststore.jks")).getFile());
+      extraOptions.put("truststorePassword", PasswordMaskingUtil.wrap(
+         PasswordMaskingUtil.getDefaultCodec().encode("securepass")));
+
+      testLDAPSConnectionWithLDAPLoginSSLSocketFactory(extraOptions, true);
+   }
+
+   @Test
+   public void 
testLDAPLoginSSLSocketFactoryWithCustomPasswordCodecAndTruststore() throws 
Exception {
+      Map<String, Object> extraOptions = new HashMap<>();
+      extraOptions.put("passwordCodec", 
TestSensitiveDataCodec.class.getName());
+      extraOptions.put("truststorePath", 
Objects.requireNonNull(this.getClass().
+         getClassLoader().getResource("server-ca-truststore.jks")).getFile());
+      extraOptions.put("truststorePassword", "ENC(ssaperuces)");
+
+      testLDAPSConnectionWithLDAPLoginSSLSocketFactory(extraOptions, true);
+   }
+
+   @Test
+   public void testLDAPLoginSSLSocketFactoryWithInvalidTruststore() throws 
Exception {
+      Map<String, Object> extraOptions = new HashMap<>();
+      extraOptions.put("truststorePath", "invalid-ca-truststore.jks");
+      extraOptions.put("truststorePassword", "securepass");
+
+      try {
+         testLDAPSConnectionWithLDAPLoginSSLSocketFactory(extraOptions, true);
+         fail("Should have thrown CommunicationException");
+      } catch (Exception e) {
+         assertEquals(CommunicationException.class, e.getClass());
+      }
+   }
+
+   @Test
+   public void testLDAPLoginSSLSocketFactoryWithWrongTruststore() throws 
Exception {
+      Map<String, Object> extraOptions = new HashMap<>();
+      extraOptions.put("truststorePath", 
Objects.requireNonNull(this.getClass().
+         
getClassLoader().getResource("other-server-truststore.jks")).getFile());
+      extraOptions.put("truststorePassword", "securepass");
+
+      try {
+         testLDAPSConnectionWithLDAPLoginSSLSocketFactory(extraOptions, true);
+         fail("Should have thrown CommunicationException");
+      } catch (Exception e) {
+         assertEquals(CommunicationException.class, e.getClass());
+      }
+   }
+
+   @Test
+   public void testLDAPLoginSSLSocketFactoryWithTrustAll() throws Exception {
+      Map<String, Object> extraOptions = new HashMap<>();
+      extraOptions.put("trustAll", "true");
+
+      testLDAPSConnectionWithLDAPLoginSSLSocketFactory(extraOptions, true);
+   }
+
+   @Test
+   public void testLDAPLoginSSLSocketFactoryWithMultipleLDAPLoginModules() 
throws Exception {
+      Map<String, Object> extraOptions = new HashMap<>();
+      extraOptions.put("truststorePath", 
Objects.requireNonNull(this.getClass().
+         getClassLoader().getResource("server-ca-truststore.jks")).getFile());
+      extraOptions.put("truststorePassword", "securepass");
+
+      try {
+         testLDAPSConnectionWithLDAPLoginSSLSocketFactory(extraOptions, false);
+
+         try {
+            
testLDAPSConnectionWithLDAPLoginSSLSocketFactory(Collections.emptyMap(), false);
+            fail("Should have thrown CommunicationException");
+         } catch (Exception e) {
+            assertEquals(CommunicationException.class, e.getClass());
+         }
+
+         
testLDAPSConnectionWithLDAPLoginSSLSocketFactory(Collections.singletonMap("trustAll",
 "true"), false);
+      } finally {
+         LDAPLoginSSLSocketFactory.getSSLContextFactory().clearSSLContexts();
+      }
+   }
+
+   private void testLDAPSConnectionWithLDAPLoginSSLSocketFactory(Map<String, 
Object> extraOptions, boolean clearSSLContexts) throws Exception {
+      Map<String, Object> options = new HashMap<>();
+
+      // Set basic LDAP connection options for LDAPS
+      options.put("initialContextFactory", "com.sun.jndi.ldap.LdapCtxFactory");
+      options.put("connectionURL", "ldaps://localhost:1025");
+      options.put("connectionUsername", PRINCIPAL);
+      options.put("connectionPassword", CREDENTIALS);
+      options.put("authentication", "simple");
+      options.put("connectionProtocol", "ssl");
+
+      // Set SSL configuration options
+      options.put("java.naming.ldap.factory.socket", 
LDAPLoginSSLSocketFactory.class.getName());
+      options.putAll(extraOptions);
+
+      LDAPLoginModule loginModule = new LDAPLoginModule();
+      loginModule.initialize(new Subject(), null, null, options);
+
+      try {
+         assertNull("The environment should be cleared before opening 
context", LDAPLoginModule.getEnvironment());
+         loginModule.openContext();
+         assertNull("The environment should be cleared after opening context", 
LDAPLoginModule.getEnvironment());
+      } finally {
+         if (clearSSLContexts) {
+            
LDAPLoginSSLSocketFactory.getSSLContextFactory().clearSSLContexts();
+         }
+         loginModule.closeContext();
+      }
+   }
+
+
+   public static class TestSensitiveDataCodec implements 
SensitiveDataCodec<String> {
+      @Override
+      public String decode(Object encodedValue) throws Exception {
+         return new StringBuilder((String) encodedValue).reverse().toString();
+      }
+
+      @Override
+      public String encode(Object value) throws Exception {
+         return new StringBuilder((String) value).reverse().toString();
+      }
+   }
 }
diff --git 
a/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginSSLSocketFactoryTest.java
 
b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginSSLSocketFactoryTest.java
new file mode 100644
index 0000000000..0772a70092
--- /dev/null
+++ 
b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginSSLSocketFactoryTest.java
@@ -0,0 +1,242 @@
+/*
+ * 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.activemq.artemis.spi.core.security.jaas;
+
+import org.apache.activemq.artemis.spi.core.remoting.ssl.SSLContextConfig;
+import org.apache.activemq.artemis.utils.PasswordMaskingUtil;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class LDAPLoginSSLSocketFactoryTest {
+
+   @Test
+   public void testConstructorsWithNullEnvironment() {
+      try {
+         LDAPLoginModule.removeEnvironment();
+         new LDAPLoginSSLSocketFactory();
+         fail("Should have thrown NullPointerException");
+      } catch (Exception e) {
+         assertEquals(NullPointerException.class, e.getClass());
+      }
+
+      try {
+         new LDAPLoginSSLSocketFactory(null);
+         fail("Should have thrown NullPointerException");
+      } catch (Exception e) {
+         assertEquals(NullPointerException.class, e.getClass());
+      }
+   }
+
+   @Test
+   public void testConstructorsWithEmptyEnvironment() {
+      LDAPLoginModule.setEnvironment(Collections.emptyMap());
+      try {
+         new LDAPLoginSSLSocketFactory();
+      } finally {
+         LDAPLoginModule.removeEnvironment();
+      }
+
+      new LDAPLoginSSLSocketFactory(Collections.emptyMap());
+   }
+
+   @Test
+   public void testGetDefaultFactoryWithNullEnvironment() {
+      try {
+         LDAPLoginModule.removeEnvironment();
+         LDAPLoginSSLSocketFactory.getDefault();
+         fail("Should have thrown NullPointerException");
+      } catch (Exception e) {
+         assertEquals(NullPointerException.class, e.getClass());
+      }
+   }
+
+   @Test
+   public void testGetDefaultFactoryWithEmptyEnvironment() {
+      LDAPLoginModule.setEnvironment(Collections.emptyMap());
+      try {
+         LDAPLoginSSLSocketFactory.getDefault();
+      } finally {
+         LDAPLoginModule.removeEnvironment();
+      }
+   }
+
+   @Test
+   public void testGetSSLContextConfigWithAllOptions() {
+      Map<String, String> environment = new HashMap<>();
+      environment.put("keystoreProvider", "SunJSSE");
+      environment.put("keystoreType", "PKCS12");
+      environment.put("keystorePath", "server-keystore.p12");
+      environment.put("keystorePassword", "securepass");
+      environment.put("keystoreAlias", "server");
+      environment.put("truststoreProvider", "SUN");
+      environment.put("truststoreType", "JKS");
+      environment.put("truststorePath", "server-ca-truststore.jks");
+      environment.put("truststorePassword", "securepass");
+      environment.put("crlPath", "/path/to/crl");
+      environment.put("trustAll", "false");
+      environment.put("trustManagerFactoryPlugin", 
"trust.manager.factory.Plugin");
+
+      LDAPLoginSSLSocketFactory factory = new 
LDAPLoginSSLSocketFactory(environment);
+      SSLContextConfig config = factory.getSSLContextConfig();
+
+      assertNotNull(config);
+      assertEquals("SunJSSE", config.getKeystoreProvider());
+      assertEquals("PKCS12", config.getKeystoreType());
+      assertEquals("server-keystore.p12", config.getKeystorePath());
+      assertEquals("securepass", config.getKeystorePassword());
+      assertEquals("server", config.getKeystoreAlias());
+      assertEquals("SUN", config.getTruststoreProvider());
+      assertEquals("JKS", config.getTruststoreType());
+      assertEquals("server-ca-truststore.jks", config.getTruststorePath());
+      assertEquals("securepass", config.getTruststorePassword());
+      assertEquals("/path/to/crl", config.getCrlPath());
+      assertFalse(config.isTrustAll());
+      assertEquals("trust.manager.factory.Plugin", 
config.getTrustManagerFactoryPlugin());
+   }
+
+   @Test
+   public void testPlainPasswordsWithDefaultPasswordCodec() throws Exception {
+      Map<String, String> environment = new HashMap<>();
+      environment.put("keystorePassword", "abc");
+      environment.put("truststorePassword", "xyz");
+
+      testPasswords(environment);
+   }
+
+   @Test
+   public void testEncodedPasswordsWithDefaultPasswordCodec() throws Exception 
{
+      Map<String, String> environment = new HashMap<>();
+      environment.put("keystorePassword", PasswordMaskingUtil.wrap(
+         PasswordMaskingUtil.getDefaultCodec().encode("abc")));
+      environment.put("truststorePassword", PasswordMaskingUtil.wrap(
+         PasswordMaskingUtil.getDefaultCodec().encode("xyz")));
+
+      testPasswords(environment);
+   }
+
+   @Test
+   public void testPlainPasswordsWithCustomPasswordCodec() {
+      Map<String, String> environment = new HashMap<>();
+      environment.put("passwordCodec", 
LDAPLoginModuleTest.TestSensitiveDataCodec.class.getName());
+      environment.put("keystorePassword", "abc");
+      environment.put("truststorePassword", "xyz");
+
+      testPasswords(environment);
+   }
+
+   @Test
+   public void testEncodedPasswordsWithCustomPasswordCodec() {
+      Map<String, String> environment = new HashMap<>();
+      environment.put("passwordCodec", 
LDAPLoginModuleTest.TestSensitiveDataCodec.class.getName());
+      environment.put("keystorePassword", "ENC(cba)");
+      environment.put("truststorePassword", "ENC(zyx)");
+
+      testPasswords(environment);
+   }
+
+   public void testPasswords(Map<String, String> environment) {
+      LDAPLoginSSLSocketFactory factory = new 
LDAPLoginSSLSocketFactory(environment);
+      SSLContextConfig config = factory.getSSLContextConfig();
+      assertEquals("abc", config.getKeystorePassword());
+      assertEquals("xyz", config.getTruststorePassword());
+   }
+
+   @Test
+   public void testEncodedPasswordsWithInvalidCodec() {
+      Map<String, String> environment = new HashMap<>();
+      environment.put("passwordCodec", "com.example.NonExistentCodec");
+      environment.put("keystorePassword", "ENC(cba)");
+      environment.put("truststorePassword", "ENC(zyx)");
+
+      LDAPLoginSSLSocketFactory factory = new 
LDAPLoginSSLSocketFactory(environment);
+      try {
+         factory.getSSLContextConfig();
+         fail("Should have thrown IllegalArgumentException");
+      } catch (IllegalArgumentException e) {
+         assertTrue(e.getMessage().contains("Failed to parse sensitive text"));
+      }
+   }
+
+
+   @Test
+   public void testCreateSocketUnconnected() throws Exception {
+      final LDAPLoginSSLSocketFactory factory = new 
LDAPLoginSSLSocketFactory(Collections.emptyMap());
+
+      try (Socket socket = factory.createSocket()) {
+         assertFalse(socket.isConnected());
+         assertFalse(socket.isClosed());
+      }
+   }
+
+   @Test
+   public void testCreateSocketWithHostAndPort() throws IOException {
+      final LDAPLoginSSLSocketFactory factory = new 
LDAPLoginSSLSocketFactory(Collections.emptyMap());
+
+      testSocket((TestSocketFactory) port -> factory.createSocket("localhost", 
port));
+   }
+
+   @Test
+   public void testCreateSocketWithAllParameters() throws IOException {
+      final LDAPLoginSSLSocketFactory factory = new 
LDAPLoginSSLSocketFactory(Collections.emptyMap());
+
+      testSocket((TestSocketFactory) port -> factory.createSocket("localhost", 
port, InetAddress.getLoopbackAddress(), 0));
+   }
+
+   @Test
+   public void testCreateSocketWithInetAddress() throws IOException {
+      final LDAPLoginSSLSocketFactory factory = new 
LDAPLoginSSLSocketFactory(Collections.emptyMap());
+
+      testSocket((TestSocketFactory) port -> 
factory.createSocket(InetAddress.getLoopbackAddress(), port));
+   }
+
+   @Test
+   public void testCreateSocketWithInetAddressAndLocalAddress() throws 
IOException {
+      final LDAPLoginSSLSocketFactory factory = new 
LDAPLoginSSLSocketFactory(Collections.emptyMap());
+
+      testSocket((TestSocketFactory) port -> 
factory.createSocket(InetAddress.getLoopbackAddress(), port, 
InetAddress.getLoopbackAddress(), 0));
+   }
+
+   @FunctionalInterface
+   private interface TestSocketFactory {
+      Socket createSocket(int port) throws IOException;
+   }
+
+   private void testSocket(TestSocketFactory socketFactory) throws IOException 
{
+      try (ServerSocket serverSocket = new ServerSocket(0, 0, 
InetAddress.getLoopbackAddress())) {
+         try (Socket socket = 
socketFactory.createSocket(serverSocket.getLocalPort())) {
+            assertTrue(socket.getRemoteSocketAddress() instanceof 
InetSocketAddress);
+            assertEquals(InetAddress.getLoopbackAddress(), 
((InetSocketAddress)socket.getRemoteSocketAddress()).getAddress());
+            assertEquals(serverSocket.getLocalPort(), 
((InetSocketAddress)socket.getRemoteSocketAddress()).getPort());
+         }
+      }
+   }
+}
+
diff --git a/docs/user-manual/security.adoc b/docs/user-manual/security.adoc
index 2a49b4126d..4b345aa340 100644
--- a/docs/user-manual/security.adoc
+++ b/docs/user-manual/security.adoc
@@ -806,6 +806,107 @@ Define a `member` attribute for each member of the 
role/group, setting the `memb
 If you want to add roles to user entries, you would need to customize the 
directory schema, by adding a suitable attribute type to the user entry's 
object class.
 The chosen attribute type must be capable of handling multiple values.
 
+===== LDAPLoginSSLSocketFactory
+
+To secure the connection to your LDAP server using SSL/TLS, you can configure 
`LDAPLoginModule` to use `LDAPLoginSSLSocketFactory`.
+This socket factory provides comprehensive SSL configuration options including 
keystore, truststore, and certificate validation settings.
+
+To use `LDAPLoginSSLSocketFactory`, you need to:
+
+1. Enable SSL/TLS by setting the property `java.naming.security.protocol` to 
'ssl' or by using `ldaps://` protocol in your `connectionURL`.
+2. Set `java.naming.ldap.factory.socket` to 
`org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginSSLSocketFactory`.
+3. Configure the SSL-related options as described below.
+
+Here's an example configuration in `login.config`:
+
+----
+LDAPLogin {
+   org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required
+       debug=false
+       initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory
+       connectionURL="ldaps://ldapserver:636"
+       
java.naming.ldap.factory.socket=org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginSSLSocketFactory
+       connectionUsername="uid=admin,ou=system"
+       connectionPassword="secret"
+       userBase="ou=User,ou=ActiveMQ,ou=system"
+       userSearchMatching="(uid={0})"
+       userSearchSubtree=false
+       roleBase="ou=Group,ou=ActiveMQ,ou=system"
+       roleName=cn
+       roleSearchMatching="(member=uid={1})"
+       roleSearchSubtree=true
+       truststorePath="/path/to/truststore.jks"
+       truststorePassword="trustpass"
+       keystorePath="/path/to/keystore.jks"
+       keystorePassword="keypass";
+};
+----
+
+The following SSL configuration options are supported:
+
+keyStoreProvider::
+The provider used for the keystore.
+For example, `SUN`, `SunJCE`, etc.
+Default is `null`.
+
+keystoreType::
+The type of keystore being used.
+For example, `PKCS12`, `JKS`, `JCEKS`, `PEM` etc.
+Default is `JKS`.
+
+keystorePath::
+The path to the keystore file containing the client certificate and private 
key.
+This is only required if client certificate authentication is needed.
+Default is `null`.
+
+keystorePassword::
+The password for the keystore file.
+Supports xref:masking-passwords.adoc#masking-passwords[password masking].
+Default is `null`.
+
+keystoreAlias::
+The alias of the certificate to use from the keystore if multiple certificates 
exist.
+Default is `null`.
+
+truststoreProvider::
+The provider used for the truststore.
+For example, `SUN`, `SunJCE`, etc.
+Default is `null`.
+
+truststoreType::
+The type of truststore being used.
+For example, `PKCS12`, `JKS`, `JCEKS`, `PEM` etc.
+Default is `JKS`.
+
+truststorePath::
+The path to the truststore file containing the trusted CA certificates.
+This is required to validate the LDAP server's certificate.
+Default is `null`.
+
+truststorePassword::
+The password for the truststore file.
+Supports xref:masking-passwords.adoc#masking-passwords[password masking].
+Default is `null`.
+
+crlPath::
+The path to the Certificate Revocation List (CRL) file for additional 
certificate validation.
+Default is `null`.
+
+trustAll::
+Boolean flag to disable certificate validation and trust all certificates.
+Default is `false`.
++
+WARNING: Setting this to `true` is insecure and should only be used for 
testing purposes.
+
+trustManagerFactoryPlugin::
+The fully qualified class name of a custom trust manager factory plugin for 
advanced certificate validation scenarios.
+Default is `null`.
+
+passwordCodec::
+The fully qualified class name of a custom password codec for decoding masked 
passwords.
+See xref:masking-passwords.adoc#masking-passwords[password masking] for more 
details.
+Default is `org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec`.
+
 ==== CertificateLoginModule
 
 The JAAS certificate authentication login module must be used in combination 
with SSL and the clients must be configured with their own certificate.
diff --git a/tests/security-resources/build.sh 
b/tests/security-resources/build.sh
index 22001d10a5..c4028648ae 100755
--- a/tests/security-resources/build.sh
+++ b/tests/security-resources/build.sh
@@ -52,6 +52,7 @@ keytool -storetype pkcs12 -keystore server-ca-keystore.p12 
-storepass $STORE_PAS
 
 keytool -storetype pkcs12 -keystore server-keystore.p12 -storepass $STORE_PASS 
-keypass $KEY_PASS -importcert -alias server-ca -file server-ca.crt -noprompt
 keytool -storetype pkcs12 -keystore server-keystore.p12 -storepass $STORE_PASS 
-keypass $KEY_PASS -importcert -alias server -file server.crt
+cp server-keystore.p12 server-keystore-without-ca.p12 && keytool -delete 
-noprompt -alias server-ca -storetype pkcs12 -keystore 
server-keystore-without-ca.p12 -storepass $STORE_PASS
 
 keytool -importkeystore -srckeystore server-keystore.p12 -destkeystore 
server-keystore.jceks -srcstoretype pkcs12 -deststoretype jceks -srcstorepass 
securepass -deststorepass securepass
 keytool -importkeystore -srckeystore server-keystore.p12 -destkeystore 
server-keystore.jks -srcstoretype pkcs12 -deststoretype jks -srcstorepass 
securepass -deststorepass securepass
diff --git a/tests/security-resources/server-keystore-without-ca.p12 
b/tests/security-resources/server-keystore-without-ca.p12
new file mode 100644
index 0000000000..db1c6459d5
Binary files /dev/null and 
b/tests/security-resources/server-keystore-without-ca.p12 differ


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]
For further information, visit: https://activemq.apache.org/contact



Reply via email to