6

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?

ROMANIA_engineer
  • 54,432
  • 29
  • 203
  • 199
Steffen Heil
  • 4,286
  • 3
  • 32
  • 35

3 Answers3

7

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

Steffen Heil
  • 4,286
  • 3
  • 32
  • 35
  • So basically, you've produced your own answer that is right in the continuation of my answer, which you've said you've downvoted... – Bruno Mar 27 '12 at 09:57
  • I never said the things you wrote are wrong, just that information was already mentioned in my question and did not at all help to solve the question in any way. That's why I downvoted. If that is not a valid reasion for downvoting, I would remove that downvote, however then EJPs upvote (which was only ment to clear my downvote) would remain, voting up an useless answer, so I won't. – Steffen Heil Mar 28 '12 at 10:29
  • 1
    As I said, fair enough before edit, but once you get an answer that shows you the reason in the OpenJDK code why you can't do what you want unless you use static members (which you have done) or something like JDNI, that's spot on an answer to your question, I think. Then, you've accepted your own answer, which was quite clearly along the lines of what I suggested. I don't care that much about the downvote and I don't mind the fact you've accepted your own answer (since you're more specific with a `ThreadLocal`). I just find downvoting + accepting own answer derived from downvoted answer odd. – Bruno Mar 28 '12 at 11:17
4

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.

Bruno
  • 119,590
  • 31
  • 270
  • 376
  • I have this line of code in my listing above. However, as I can only specify a classname and not a certain instance, I cannot pass arguments to that class - which is what I need. Did you read the question? If so, tell me where I should be more detailed. – Steffen Heil Feb 22 '12 at 15:57
  • @SteffenHeil, sorry, I answered too quickly, I've added more details. – Bruno Feb 22 '12 at 16:33
  • I have to connect to server S1 and only accept Certificate C1 and to connect to server S2 and only accept Certiicate C2. How should this work using a a CustomSocketFactory? – Steffen Heil Mar 14 '12 at 10:15
  • 1
    Every possibility noted in this answer was aready in my question. So I downvoted this answer... – Steffen Heil Mar 14 '12 at 10:25
  • @SteffenHeil, fair enough before edit, but when the answer tells you you can't do something after quoting the code that implements what you're after (and isn't coded as you'd want), it doesn't deserve a downvote (more an upvote). Anyway, I'm not going to worry about one vote... – Bruno Mar 14 '12 at 11:06
1

I've built my own solution that works for me but it's far from perfect. I'm actually afraid there's no perfect solution due to the unfortunate implementation in javax.naming.

My SelectiveLdapSslSocketFactory contains a static Map mapping hosts to different SSLSocketFactories. When any of the createSocket(...) methods is called, the call is delegated to the corresponding SSLSocketFactory. It also contains a defaultSslSocketFactory used for hosts without any mapping and also in the getDefaultCipherSuites() and getSupportedCipherSuites() methods. I'm not sure, if it's correct but it works fine in my case so test it if you like:

public class SelectiveLdapSslSocketFactory extends SSLSocketFactory {

    private static SSLSocketFactory defaultSslSocketFactory;
    private static final Map<String, SSLSocketFactory> hostToSslSocketFactoryMap = new HashMap<>();
    
    {
        try {
            defaultSslSocketFactory = <yourOwnDefaultSslSocketFactory>;
        } catch (Exception ex) {
            Logger.warn(ex, "Couldn't initialize a defaultSslSocketFactory for LDAP connections!");
        }
    }

    public static SSLSocketFactory getRegisteredSslSocketFactory(String host) {
        return hostToSslSocketFactoryMap.get(host);
    }

    public static void registerSslSocketFactory(String host, SSLSocketFactory sslSocketFactory) {
        hostToSslSocketFactoryMap.put(host, sslSocketFactory);
    }

    public static void deregisterSslSocketFactory(String host) {
        hostToSslSocketFactoryMap.remove(host);
    }

    public SelectiveLdapSslSocketFactory() {
    }

    public static SocketFactory getDefault() {
        return new SelectiveLdapSslSocketFactory();
    }

    @Override
    public String[] getDefaultCipherSuites() {
        return defaultSslSocketFactory.getDefaultCipherSuites();
    }

    @Override
    public String[] getSupportedCipherSuites() {
        return defaultSslSocketFactory.getSupportedCipherSuites();
    }

    @Override
    public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
        SSLSocketFactory sslSocketFactory = Objects.requireNonNullElse(hostToSslSocketFactoryMap.get(host), defaultSslSocketFactory);
        return sslSocketFactory.createSocket(s, host, port, autoClose);
    }

    @Override
    public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
        SSLSocketFactory sslSocketFactory = Objects.requireNonNullElse(hostToSslSocketFactoryMap.get(host), defaultSslSocketFactory);
        return sslSocketFactory.createSocket(host, port);
    }

    @Override
    public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
        SSLSocketFactory sslSocketFactory = Objects.requireNonNullElse(hostToSslSocketFactoryMap.get(host), defaultSslSocketFactory);
        return sslSocketFactory.createSocket(host, port, localHost, localPort);
    }

    @Override
    public Socket createSocket(InetAddress host, int port) throws IOException {
        SSLSocketFactory sslSocketFactory = getSslSocketFactory(host);
        return sslSocketFactory.createSocket(host, port);
    }

    @Override
    public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
        SSLSocketFactory sslSocketFactory = getSslSocketFactory(address);
        return sslSocketFactory.createSocket(address, port, localAddress, localPort);
    }

    private SSLSocketFactory getSslSocketFactory(InetAddress inetAddress) {
        SSLSocketFactory sslSocketFactory = hostToSslSocketFactoryMap.get(Objects.requireNonNullElse(inetAddress.getCanonicalHostName(), ""));
        if (sslSocketFactory == null) {
            sslSocketFactory = hostToSslSocketFactoryMap.get(Objects.requireNonNullElse(inetAddress.getHostName(), ""));
            if (sslSocketFactory == null) {
                sslSocketFactory = hostToSslSocketFactoryMap.get(Objects.requireNonNullElse(inetAddress.getHostAddress(), ""));
                if (sslSocketFactory == null) {
                    sslSocketFactory = defaultSslSocketFactory;
                }
            }
        }
        return sslSocketFactory;
    }
}

You can then use it like:

...
SelectiveLdapSslSocketFactory.registerSslSocketFactory(host01, sslSocketFactory01);
SelectiveLdapSslSocketFactory.registerSslSocketFactory(host02, sslSocketFactory02);
SelectiveLdapSslSocketFactory.registerSslSocketFactory(host03, sslSocketFactory03);

props.put("java.naming.ldap.factory.socket", SelectiveLdapSslSocketFactory.class.getName());
tomorrow
  • 1,260
  • 1
  • 14
  • 26