There is a way of setting a trust-store for just an explicit LDAP call and not as a global setting for all SSL activity. Setting up a truststore for just LDAP connections involves creating a custom SocketFactory. You can use this custom SocketFactory to read from a separate truststore or certificate file, independent of the global truststore referenced by javax.net.ssl.keyStore*
properties. This JNDI tutorial explains how to do such a thing: https://docs.oracle.com/javase/jndi/tutorial/ldap/security/ssl.html
Here's what you need to do:
- env.put(Context.PROVIDER_URL, "ldaps://some-ad.foo.bar:636");
- env.put(Context.SECURITY_PROTOCOL, "SSL");
- env.put("java.naming.ldap.factory.socket", "fully.qualified.name.of.your.custom.socket.factory.class");
Your custom socket factory class will then read from your custom truststore or certificate. To pass the location and password of your truststore to your custom socket factory class, you may make use of custom user-defined system properties.
Here's an example using your ldap query method and a sample custom socket factory. I've added in additional code that will be needed to accomplish using a custom truststore:
private List<Map<String, Object>> queryImp(String filter) throws NamingException
{
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactoryClassName);
env.put(Context.PROVIDER_URL, url); //Must start with "ldaps" and have port "636"
env.put(Context.SECURITY_PRINCIPAL, user);
env.put(Context.SECURITY_CREDENTIALS, password);
//New settings that need to be added for LDAP SSL
env.put(Context.SECURITY_PROTOCOL, "SSL");
env.put("java.naming.ldap.factory.socket", CustomLdapSslSocketFactory.class.getName()); //See: https://docs.oracle.com/javase/jndi/tutorial/ldap/security/ssl.html
/*These are custom user-defined properties. Name them whatever you like.
These will be referenced later in the custom socket factory class. */
System.setProperty("custom.ldap.truststore.type", "pkcs12");
System.setProperty("custom.ldap.truststore.loc", "C:/certs/MyCustomLdapTruststore.p12");
System.setProperty("custom.ldap.truststore.password", "My custom ldap truststore password");
System.setProperty("custom.ldap.ssl.protocol", "TLSv1.2");
DirContext context = new InitialDirContext(env);
SearchControls sc = new SearchControls();
sc.setReturningAttributes(attributeFilter);
sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration results = context.search(base, filter, sc);
List<Map<String, Object>> result = toMaps(results);
context.close();
return result;
}
Here's the corresponding sample custom socket factory class. I've made the class return a singleton each time the public static SocketFactory getDefault()
method is invoked. That's why it's a bit more complex than would be needed if a new instance were to be returned each time.
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
public class CustomLdapSslSocketFactory extends SSLSocketFactory
{
private SSLSocketFactory sslSocketFactory;
private static volatile CustomLdapSslSocketFactory singletonCustLdapSslSockFact;
private CustomLdapSslSocketFactory() throws KeyManagementException, KeyStoreException, FileNotFoundException, NoSuchAlgorithmException, CertificateException, IOException
{
sslSocketFactory = loadTrustStoreProgrammatically();
}
private static CustomLdapSslSocketFactory getSingletonInstance() throws KeyManagementException, KeyStoreException, FileNotFoundException, NoSuchAlgorithmException, CertificateException, IOException
{
if(CustomLdapSslSocketFactory.singletonCustLdapSslSockFact == null)
{
synchronized(CustomLdapSslSocketFactory.class)
{
if(CustomLdapSslSocketFactory.singletonCustLdapSslSockFact == null)
{
CustomLdapSslSocketFactory.singletonCustLdapSslSockFact = new CustomLdapSslSocketFactory();
}
}
}
return CustomLdapSslSocketFactory.singletonCustLdapSslSockFact;
}
public static SocketFactory getDefault() //this method is called by Ldap implementations to create the custom SSL socket factory. See: https://docs.oracle.com/javase/jndi/tutorial/ldap/security/ssl.html
{
/*
There are times when you need to have more control over the SSL sockets, or sockets in general, used by the LDAP service provider.
To set the socket factory implementation used by the LDAP service provider, set the "java.naming.ldap.factory.socket" property to the
fully qualified class name of the socket factory.
This class must extend the javax.net.SocketFactory abstract class and provide an implementation of the getDefault() method that
returns an instance of the custom socket factory.
See:
https://docs.oracle.com/javase/jndi/tutorial/ldap/security/ssl.html
*/
CustomLdapSslSocketFactory custLdapSslSockFact = null;
try
{
//custLdapSslSockFact = new CustomLdapSslSocketFactory(); //returns a new instance each time
custLdapSslSockFact = CustomLdapSslSocketFactory.getSingletonInstance(); //returns the same instance each time (singleton pattern)
}
catch(Exception e)
{
throw new RuntimeException("Failed create CustomSslSocketFactory. Exception: " + e.getClass().getSimpleName() + ". Reason: " + e.getMessage(), e);
}
return custLdapSslSockFact;
}
private SSLSocketFactory loadTrustStoreProgrammatically() throws KeyStoreException, FileNotFoundException, IOException, NoSuchAlgorithmException, KeyManagementException, CertificateException
{
//Now, reference the custom user-defined system properties defined in your ldap query method above.
String trustStoreType = System.getProperty("custom.ldap.truststore.type");
String trustStoreLoc = System.getProperty("custom.ldap.truststore.loc");
char[] trustStorePasswordCharArr = System.getProperty("custom.ldap.truststore.password").toCharArray();
String sslContextProtocol = System.getProperty("custom.ldap.ssl.protocol");
KeyStore trustStore = KeyStore.getInstance(trustStoreType);
try(BufferedInputStream bisTrustStore = new BufferedInputStream(new FileInputStream(trustStoreLoc)))
{
trustStore.load(bisTrustStore, trustStorePasswordCharArr); // if your does not have a password specify null
}
// initialize a trust manager factory with the trusted store
TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustFactory.init(trustStore);
// get the trust managers from the factory
TrustManager[] trustManagers = trustFactory.getTrustManagers();
// initialize an ssl context to use these managers
SSLContext sslContext = SSLContext.getInstance(sslContextProtocol); //.getInstance("SSL"); or TLS, etc.
sslContext.init(null, trustManagers, null);
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
return sslSocketFactory;
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException
{
return sslSocketFactory.createSocket(s, host, port, autoClose);
}
@Override
public String[] getDefaultCipherSuites()
{
return sslSocketFactory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites()
{
return sslSocketFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket(String host, int port) throws IOException, UnknownHostException
{
return sslSocketFactory.createSocket(host, port);
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException
{
return sslSocketFactory.createSocket(host, port);
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException
{
return sslSocketFactory.createSocket(localHost, port, localHost, localPort);
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException
{
return sslSocketFactory.createSocket(address, port, localAddress, localPort);
}
}
See the related stackoverflow post: How to send a params to the SocketFactory in Ldap for an explanation on why I chose to use custom user-defined system properties instead of hard-coded values in the CustomLdapSslSocketFactory
class above.
That's it. With just these changes, you'll be able make LDAP calls over SSL.