user2303818
user2303818

Reputation: 13

How to make a call, via Java, with ssl and certificate to an ldap (AD)?

1, I want to make a call, via ssl, to an ldap database. It is a search to be performed, usually for an employee.

2, A certificate must be attached to the call in order for the call to be accepted.

3, The certificate could be added to jvm's default key storage - the one found on JAVA_HOME / jre / lib / security / cacerts. Have tested this and it works - but I do not want this solution. Do not want the application to be dependent on having its jvm configured in that way - the operation of and the environment for the application is, in part, out of my control.

4, Similar solution as in point three could also be made by referring to a separate key collection via System.setProperty ("javax.net.ssl.keyStore", "serverKeys"). Do not want to do this for the same reason as in paragraph 3 and because I dont want the cert to be in every call from the environment.

5, Would prefer to have it all in vanilla Java - not a brand new Spring solution.

I would prefer to be able to send with the certificate just for this call. To do it only through the program itself.

Below is my code for the call, as it looks now:

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);
    env.put(Context.SECURITY_PRINCIPAL, user);
    env.put(Context.SECURITY_CREDENTIALS, password);
    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;
}

The adress for the ldap is something like ldaps://some-ad.foo

In some ideal fantasy-land I would be able to do something like env.put("cert-key-name", "raw-cert-base64-encoded-text") :*)

Upvotes: 0

Views: 4179

Answers (1)

Mody
Mody

Reputation: 156

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:

  1. env.put(Context.PROVIDER_URL, "ldaps://some-ad.foo.bar:636");
  2. env.put(Context.SECURITY_PROTOCOL, "SSL");
  3. 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.

Upvotes: 2

Related Questions