jndi LDAPS custom HostnameVerifier and TrustManage

2019-01-26 19:22发布

问题:

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?

回答1:

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...



回答2:

You should pass the name of own SSLSocketFactory subclass and pass its fully qualified named into the "java.naming.ldap.factory.socket" env property, as described in the "Using Custom Sockets" section of the Java LDAP/SSL guide:

env.put("java.naming.ldap.factory.socket", "example.CustomSocketFactory");

You can't pass any specific argument to this class, see instantiation in com.sun.jndi.ldap.Connection.createSocket(...):

Class socketFactoryClass = Obj.helper.loadClass(socketFactory);
Method getDefault =
    socketFactoryClass.getMethod("getDefault", new Class[]{});
Object factory = getDefault.invoke(null, new Object[]{});

If you want additional parameters, you may have to use static members or JNDI perhaps (usually not ideal).

As far as I can tell, there doesn't seem to be any hostname verification when using ldaps:// in this implementation unfortunately. If you only trust one explicit certificate in your trust manager, this should compensate for the lack of hostname verification anyway.