Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document how to use a custom truststore #545

Open
marschall opened this issue Jan 8, 2020 · 7 comments
Open

Document how to use a custom truststore #545

marschall opened this issue Jan 8, 2020 · 7 comments
Milestone

Comments

@marschall
Copy link

marschall commented Jan 8, 2020

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

@ChristopherSchultz
Copy link

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.

@rwinch
Copy link
Member

rwinch commented Jan 25, 2021

Any interest in providing a PR?

@ChristopherSchultz
Copy link

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.

@marschall
Copy link
Author

Any interest in providing a PR?

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

@rwinch
Copy link
Member

rwinch commented Jan 28, 2021

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.

@JanHron
Copy link

JanHron commented Oct 7, 2021

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

@Hakky54
Copy link

Hakky54 commented Aug 5, 2024

I wanted to share here that it is easier to configure the ssl of spring boot as shown here: #547 (comment)

@jzheaux jzheaux added this to the 2.4.x milestone Sep 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants