spring-ldap
spring-ldap copied to clipboard
Document how to use a custom truststore
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 thejava.naming.ldap.factory.socket
property in the#setupEnvironment(Hashtable, String, String)
method. - implement a custom
DefaultSpringSecurityContextSource
that in sets thejava.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
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.
Any interest in providing a PR?
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.
Any interest in providing a PR?
I can give it a try. Where would that go? docs/asciidoc/index.adoc
? If so which chapter?
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.
@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.
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