spring-ldap icon indicating copy to clipboard operation
spring-ldap copied to clipboard

Document how to use a custom truststore

Open marschall opened this issue 5 years ago • 7 comments

If you want to use a custom truststore, eg. with just the root CA certificate of the server, you have to do:

  • implement a custom SSLSocketFactory
  • implement a custom SimpleDirContextAuthenticationStrategy that sets the java.naming.ldap.factory.socket property in the #setupEnvironment(Hashtable, String, String) method.
  • implement a custom DefaultSpringSecurityContextSource that in sets the java.naming.ldap.factory.socket property in the #getAuthenticatedEnv(String, String) method

It would be good if this was documented somewhere.

Socket factory base class

package com.acme.spring.ldap;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * {@link SSLSocketFactory} that allows to specify a custom truststore.
 */
public abstract class TruststoreSSLSocketFactory extends SSLSocketFactory {

  private static final Logger LOGGER = LoggerFactory.getLogger(TruststoreSSLSocketFactory.class);

  private static final String[] CIPHER_SUITES = new String[] {
      // ...
  };

  private final SSLSocketFactory delegate;

  public TruststoreSSLSocketFactory() {
    this.delegate = loadWithTrustStore(this.getTrustStoreLocation(), getTruststorePassword());
  }

  private static SSLSocketFactory loadWithTrustStore(String truststorePath, char[] truststorePassword) {

    SSLContext sslContext;
    try {
      sslContext = SSLContext.getInstance("TLSv1.2");
    } catch (NoSuchAlgorithmException e) {
      LOGGER.warn("TLS 1.2 not available", e);
      throw new RuntimeException("TLS 1.2 not available", e);
    }

    KeyStore keyStore;
    try {
      keyStore = KeyStore.getInstance("PKCS12");
    } catch (KeyStoreException e) {
      LOGGER.warn("PKCS12 not supported", e);
      throw new RuntimeException("PKCS12 not supported", e);
    }

    try (FileInputStream fileInputStream = new FileInputStream(truststorePath)) {
      keyStore.load(fileInputStream, truststorePassword);
    } catch (GeneralSecurityException | IOException e) {
      LOGGER.warn("Could not load from: " + truststorePath, e);
      throw new RuntimeException("Could not load from: " + truststorePath, e);
    }

    String defaultTrustManagerAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
    TrustManagerFactory trustManagerFactory;
    try {
      trustManagerFactory = TrustManagerFactory.getInstance(defaultTrustManagerAlgorithm);
    } catch (NoSuchAlgorithmException e) {
      LOGGER.warn("Default algorithm not supported: " + defaultTrustManagerAlgorithm, e);
      throw new RuntimeException("Default algorithm not supported: " + defaultTrustManagerAlgorithm, e);
    }

    try {
      trustManagerFactory.init(keyStore);
    } catch (KeyStoreException e) {
      LOGGER.warn("Could not initialize trust manager factory", e);
      throw new RuntimeException("Could not initialize trust manager factory", e);
    }

    try {
      sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
    } catch (KeyManagementException e) {
      LOGGER.warn("Could not initialize ssl context", e);
      throw new RuntimeException("Could not initialize ssl context", e);
    }

    return sslContext.getSocketFactory();
  }

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

  @Override
  public Socket createSocket(InetAddress host, int port) throws IOException {
    return delegate.createSocket(host, port);
  }

  @Override
  public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
    return delegate.createSocket(s, host, port, autoClose);
  }

  @Override
  public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
    return delegate.createSocket(host, port, localHost, localPort);
  }

  @Override
  public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
    return delegate.createSocket(host, port);
  }

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

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

  @Override
  public Socket createSocket() throws IOException {
    return delegate.createSocket();
  }

  @Override
  public Socket createSocket(Socket s, InputStream consumed, boolean autoClose) throws IOException {
    return delegate.createSocket(s, consumed, autoClose);
  }

  protected abstract String getTrustStoreLocation();

  protected abstract char[] getTruststorePassword();

}

concrete socket factory

package com.acme.spring.ldap;

import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;


/**
 * {@link SSLSocketFactory} that uses a custom truststore for acme.domain.
 */
public class AcmeDomainSSLSocketFactory extends TruststoreSSLSocketFactory {

  static final String PASSPHRASE = "...";

  /**
   * Returns the default SSL socket factory.
   *
   * @return the default SocketFactory
   */
  public static SocketFactory getDefault() {
    return new AcmeDomainSSLSocketFactory();
  }

  @Override
  protected char[] getTruststorePassword() {
    return PASSPHRASE.toCharArray();
  }

  @Override
  protected String getTrustStoreLocation() {
    return "/opt/acme/truststore.p12";
  }

}

custom authentication strategy

package com.acme.spring.ldap;

import java.util.Hashtable;

import javax.net.ssl.SSLSocketFactory;

import org.springframework.ldap.core.support.DirContextAuthenticationStrategy;
import org.springframework.ldap.core.support.SimpleDirContextAuthenticationStrategy;

/**
 * A custom {@link DirContextAuthenticationStrategy} that allows setting a custom {@link SSLSocketFactory}.
 */
final class SslSockeFactorySimpleDirContextAuthenticationStrategy extends SimpleDirContextAuthenticationStrategy {

  private final Class<? extends SSLSocketFactory> sslSocketFactoryClass;

  SslSockeFactorySimpleDirContextAuthenticationStrategy(Class<? extends SSLSocketFactory> sslSocketFactoryClass) {
    this.sslSocketFactoryClass = sslSocketFactoryClass;
  }

  Class<? extends SSLSocketFactory> getSslSocketFactoryClass() {
    return sslSocketFactoryClass;
  }

  @Override
  public void setupEnvironment(Hashtable<String, Object> env, String userDn, String password) {
    super.setupEnvironment(env, userDn, password);
    // https://docs.oracle.com/javase/jndi/tutorial/ldap/security/ssl.html
    env.put("java.naming.ldap.factory.socket", this.sslSocketFactoryClass.getName());
  }

}

package com.acme.spring.ldapr;

import java.util.Hashtable;

import javax.net.ssl.SSLSocketFactory;

import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.support.DirContextAuthenticationStrategy;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;

/**
 * A {@link ContextSource} that allows setting a custom {@link SSLSocketFactory}.
 */
final class SslSocketFactoryContextSource extends DefaultSpringSecurityContextSource {

  private DirContextAuthenticationStrategy authenticationStrategy;

  SslSocketFactoryContextSource(String providerUrl) {
    super(providerUrl);
  }

  @Override
  public void setAuthenticationStrategy(DirContextAuthenticationStrategy authenticationStrategy) {
    this.authenticationStrategy = authenticationStrategy;
    super.setAuthenticationStrategy(authenticationStrategy);
  }

  @Override
  protected Hashtable<String, Object> getAuthenticatedEnv(String principal, String credentials) {
    Hashtable<String, Object> env = super.getAuthenticatedEnv(principal, credentials);
    if (this.authenticationStrategy instanceof SslSockeFactorySimpleDirContextAuthenticationStrategy) {
      // https://docs.oracle.com/javase/jndi/tutorial/ldap/security/ssl.html
      Class<? extends SSLSocketFactory> sslSocketFactoryClass = ((SslSockeFactorySimpleDirContextAuthenticationStrategy) this.authenticationStrategy).getSslSocketFactoryClass();
      env.put("java.naming.ldap.factory.socket", sslSocketFactoryClass.getName());
    }
    return env;
  }

}

usage

SslSockeFactorySimpleDirContextAuthenticationStrategy authenticationStrategy = new SslSockeFactorySimpleDirContextAuthenticationStrategy(AcmeDomainSSLSocketFactory.class);

DefaultSpringSecurityContextSource contextSource = new SslSocketFactoryContextSource(SERVER_URL);

This is a follow up to #494

marschall avatar Jan 08 '20 14:01 marschall

Yes, please! Difficult TLS configuration is something that is holding the whole industry back from becoming more secure. People resort to crazy things like modifying the JVM's cacerts file or setting JVM-wide trust stores instead of configuring each connection with separate trust. It's even worse when trying to provide client TLS certificates.

ChristopherSchultz avatar Jan 25 '21 16:01 ChristopherSchultz

Any interest in providing a PR?

rwinch avatar Jan 25 '21 21:01 rwinch

I'm not an actual direct user of Spring, only a user of a product which uses it. I'm afraid I'm not steeped enough in Spring-isms to do a very good job with this.

The custom SSLSocketFactory is the easy part (for me). The wiring into Spring is not.

ChristopherSchultz avatar Jan 26 '21 15:01 ChristopherSchultz

Any interest in providing a PR?

I can give it a try. Where would that go? docs/asciidoc/index.adoc? If so which chapter?

marschall avatar Jan 26 '21 18:01 marschall

Thanks for volunteering @marschall! A section in https://github.com/spring-projects/spring-ldap/blob/master/src/docs/asciidoc/index.adoc#configuration would probably be where I'd put it.

rwinch avatar Jan 28 '21 19:01 rwinch

@marschall What you've done is awesome and quite involved. Combined with the CompositeX509TrustManager available elsewhere, this gives us a very powerful toolkit for managing trust per individual connection.

JanHron avatar Oct 07 '21 10:10 JanHron

I wanted to share here that it is easier to configure the ssl of spring boot as shown here: https://github.com/spring-projects/spring-ldap/issues/547#issuecomment-2269935433

Hakky54 avatar Aug 05 '24 21:08 Hakky54