spring-projects / spring-projects/spring-ldap

Document how to use a custom truststore

Open
#545 7 comments 3 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Java
Stars
375
Forks
501
Avg merge
6h 4m
Merged PRs (30d)
63

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reviewing the Spring LDAP documentation around SimpleDirContextAuthenticationStrategy, setupEnvironment, DefaultSpringSecurityContextSource, and getAuthenticatedEnv. Document the custom SSLSocketFactory and truststore configuration shown in the issue, including how the socket factory is connected to LDAP authentication; the documentation is done when users can follow the example to configure a custom truststore.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring
Domain
authentication, documentation, security
Issue type
Documentation
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.