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