Skip to content

Document how to use a custom truststore #545

@marschall

Description

@marschall

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions