javasslldapjndijsse

jndi LDAPS custom HostnameVerifier and TrustManager


We are writing an application that shall connect to different LDAP servers. For each server we may only accept a certain certificate. The hostname in that certificate shall not matter. This is easy, when we use LDAP and STARTTLS, because we can use StartTlsResponse.setHostnameVerifier(..-) and use StartTlsResponse.negotiate(...) with a matching SSLSocketFactory. However we also need to support LDAPS connections. Java supports this natively, but only if the server certificate is trusted by the default java keystore. While we could replace that, we still cannot use different keystores for different servers.

The existing connection code is as follows:

Hashtable<String,String> env = new Hashtable<String,String>();
env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory" );
env.put( Context.PROVIDER_URL, ( encryption == SSL ? "ldaps://" : "ldap://" ) + host + ":" + port );
if ( encryption == SSL ) {
    // env.put( "java.naming.ldap.factory.socket", "CustomSocketFactory" );
}
ctx = new InitialLdapContext( env, null );
if ( encryption != START_TLS )
    tls = null;
else {
    tls = (StartTlsResponse) ctx.extendedOperation( new StartTlsRequest() );
    tls.setHostnameVerifier( hostnameVerifier );
    tls.negotiate( sslContext.getSocketFactory() );
}

We could add out own CustomSocketFactory, but how to pass information to that?


Solution

  • For others have the same problem: I found a very ugly solution for my case:

    import javax.net.SocketFactory;
    
    public abstract class ThreadLocalSocketFactory
      extends SocketFactory
    {
    
      static ThreadLocal<SocketFactory> local = new ThreadLocal<SocketFactory>();
    
      public static SocketFactory getDefault()
      {
        SocketFactory result = local.get();
        if ( result == null )
          throw new IllegalStateException();
        return result;
      }
    
      public static void set( SocketFactory factory )
      {
        local.set( factory );
      }
    
      public static void remove()
      {
        local.remove();
      }
    
    }
    

    Using it like this:

    env.put( "java.naming.ldap.factory.socket", ThreadLocalSocketFactory.class.getName() );
    ThreadLocalSocketFactory.set( sslContext.getSocketFactory() );
    try {
      ctx = new InitialLdapContext( env, null );
    } finally {
      ThreadLocalSocketFactory.remove();
    }
    

    Not nice, but it works. JNDI should be more flexible here...