spring-projects / spring-ldap

Spring LDAP
https://spring.io/spring-ldap
Apache License 2.0
347 stars 482 forks source link

Provide an SSLSocketFactoryFactoryBean #547

Open marschall opened 4 years ago

marschall commented 4 years ago

In order to more easily support custom TLS configuration provide an SSLSocketFactoryFactoryBean that takes

This is a follow up for #494

ChristopherSchultz commented 3 years ago

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 commented 3 years ago

Any interest in providing a pull request?

marschall commented 3 years ago

Any interest in providing a pull request?

The tricky part is to generate the dynamic subclass that implements #getDefault(). This would be much simpler with when using a byte code library. In addition a way to dynamically define the class has to be found. Spring repackages GCLib.

rwinch commented 3 years ago

I think I must be missing something. Can you expand on why making it easier for SSL configuraiton you need to generate a dynamic class that implements getDefault()?

marschall commented 3 years ago

I think I must be missing something. Can you expand on why making it easier for SSL configuraiton you need to generate a dynamic class that implements getDefault()?

Because java.naming.ldap.factory.socket expects a socket expects a javax.net.SocketFactory class, not an instance. The class is instantiated using a static #getDefault() method that has to return a fully configured instance.

public class CustomSocketFactory extends SocketFactory {
    public static SocketFactory getDefault() {

    return new CustomSocketFactory();
    }
    ...
}
jakub-moravec commented 3 years ago

This would be very helpful, I'm facing the same issue.

marschall commented 3 years ago

I started a small project that does this. It requires ByteBuddy and Java 11+

https://github.com/marschall/ssl-socket-factory-factory-bean

renegrob commented 3 years ago

One thing that could be clearer: AbstractTlsDirContextAuthenticationStrategy does LDAP with StartTLS (usually on port 389), the implementation by @marschall does LDAPS (Microsoft proprietary but supported by various LDAP providers) does an initial TLS handshake and usually runs on port 636. It would be great to have the choice between LDAPS and LDAP with Start-TLS, with default or custom truststore, protocol and ciphers. @marschall Thanks, your examples helped me to get the ActiveDirectory authentication work.

renegrob commented 3 years ago

Neither do all LDAP servers support LDAPS nor LDAP with Start-TLS. However LDAP without TLS is like having a webserver running on plain http. It's okay for development but don't do this in production. I had to write a bunch of custom code to make it work. I would expect spring-ldap to have built-in support for the common scenarios.

jiananxu commented 2 months ago

Ldaps Extension DirContextAuthenticationStrategy can work normally in the spring the boot 3.2.

public class LdapTlsDirContextAuthenticationStrategy implements DirContextAuthenticationStrategy {

    private static final String SIMPLE_AUTHENTICATION = "simple";

    /**
     * SSL socket factory to use for startTLS negotiation
     */
    private SSLSocketFactory sslSocketFactory;

    public SSLSocketFactory getSslSocketFactory() {
        return sslSocketFactory;
    }

    /**
     * Sets the optional SSL socket factory used for startTLS negotiation. Defaults to
     * <code>null</code> to indicate that the default socket factory provided by the
     * underlying JSSE provider should be used.
     *
     * @param sslSocketFactory SSL socket factory to use, if any.
     */
    public void setSslSocketFactory(final SSLSocketFactory sslSocketFactory) {
        this.sslSocketFactory = sslSocketFactory;
    }

    /**
     * @see DirContextAuthenticationStrategy#
     * setupEnvironment(java.util.Hashtable, java.lang.String, java.lang.String)
     */
    @Override
    public final void setupEnvironment(Hashtable<String, Object> env, String userDn, String password) {
        // Nothing to do in this implementation - authentication should take
        // place after TLS has been negotiated.
    }

    /**
     * @see DirContextAuthenticationStrategy#
     * processContextAfterCreation(javax.naming.directory.DirContext, java.lang.String,
     * java.lang.String)
     */
    @Override
    public final DirContext processContextAfterCreation(DirContext ctx, String userDn, String password)
            throws NamingException {

        if (ctx instanceof LdapContext) {
            final LdapContext ldapCtx = (LdapContext) ctx;
            //set socket factory
            JndiSocketFactory.setSocketFactory(getSslSocketFactory());
            //set auth
            applyAuthentication(ldapCtx, userDn, password);
            return ctx;
        } else {
            throw new IllegalArgumentException(
                    "Processed Context must be an LDAPv3 context, i.e. an LdapContext implementation");
        }

    }

    /**
     * Apply the actual authentication to the specified <code>LdapContext</code> .
     * Typically, this will involve adding stuff to the environment.
     *
     * @param ctx      the <code>LdapContext</code> instance.
     * @param userDn   the user dn of the user to authenticate.
     * @param password the password of the user to authenticate.
     * @throws NamingException if any error occurs.
     */

    protected void applyAuthentication(LdapContext ctx, String userDn, String password) throws NamingException {
        ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, SIMPLE_AUTHENTICATION);
        ctx.addToEnvironment(Context.SECURITY_PROTOCOL, "ssl");

        ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, userDn);
        ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
        // Set the socket factory to use for SSL connections
        ctx.addToEnvironment("java.naming.ldap.factory.socket",
                JndiSocketFactory.class.getName());
        // Force a server call as we have updated the environment (gh-430, gh-502)
        ctx.lookup("");
    }
}

You can customize JndiSocketFactory.

public class JndiSocketFactory extends SSLSocketFactory {
...
}

In this mode, the certificate storage path is not specified.

marschall commented 2 months ago

JndiSocketFactory.setSocketFactory(getSslSocketFactory());

That does not look particularly thread safe.

Hakky54 commented 2 months ago

I was also hoping for some easy ssl configuration, after waiting for some time hoping for this feature to implemented I came up with a way to programatically configure the ssl configuration of spring. It required some customization, but it is not that much. I documented it here:

My usecase was hot reloading ssl and not Ldap, but I think that would also be easily possible.

So I have the feeling that it is already quite easy to customize the ssl configuration. It basically looks like this:

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ServerConfig {

    @Bean
    public ServletWebServerFactory servletContainer(SSLConnectorCustomizer sslConnectorCustomizer) {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
        tomcat.addConnectorCustomizers(sslConnectorCustomizer);
        return tomcat;
    }

}
import nl.altindag.ssl.SSLFactory;
import org.apache.catalina.connector.Connector;
import org.apache.coyote.http11.AbstractHttp11Protocol;
import org.apache.tomcat.util.net.SSLHostConfig;
import org.apache.tomcat.util.net.SSLHostConfigCertificate;
import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SSLConnectorCustomizer implements TomcatConnectorCustomizer {

    private final SSLFactory sslFactory;
    private final int port;

    public SSLConnectorCustomizer(SSLFactory sslFactory, @Value("${server.port}") int port) {
        this.sslFactory = sslFactory;
        this.port = port;
    }

    @Override
    public void customize(Connector connector) {
        connector.setScheme("https");
        connector.setSecure(true);
        connector.setPort(port);

        AbstractHttp11Protocol<?> protocol = (AbstractHttp11Protocol<?>) connector.getProtocolHandler();
        protocol.setSSLEnabled(true);

        SSLHostConfig sslHostConfig = new SSLHostConfig();
        SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, Type.UNDEFINED);
        certificate.setSslContext(new TomcatSSLContext(sslFactory));
        sslHostConfig.addCertificate(certificate);
        protocol.addSslHostConfig(sslHostConfig);
    }

}
import nl.altindag.ssl.SSLFactory;
import org.apache.tomcat.util.net.SSLContext;

import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSessionContext;
import javax.net.ssl.TrustManager;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;

public final class TomcatSSLContext implements SSLContext {

    private final SSLFactory sslFactory;

    public TomcatSSLContext(SSLFactory sslFactory) {
        this.sslFactory = sslFactory;
    }

    @Override
    public void init(KeyManager[] kms, TrustManager[] tms, SecureRandom sr) {
        // not needed to initialize as it is already initialized
    }

    @Override
    public void destroy() {

    }

    @Override
    public SSLSessionContext getServerSessionContext() {
        return sslFactory.getSslContext().getServerSessionContext();
    }

    @Override
    public SSLEngine createSSLEngine() {
        return sslFactory.getSSLEngine();
    }

    @Override
    public SSLServerSocketFactory getServerSocketFactory() {
        return sslFactory.getSslServerSocketFactory();
    }

    @Override
    public SSLParameters getSupportedSSLParameters() {
        return sslFactory.getSslParameters();
    }

    @Override
    public X509Certificate[] getCertificateChain(String alias) {
        return sslFactory.getKeyManager()
                .map(keyManager -> keyManager.getCertificateChain(alias))
                .orElseThrow();
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return sslFactory.getTrustedCertificates().toArray(new X509Certificate[0]);
    }

}
import nl.altindag.ssl.SSLFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SSLConfig {

    @Bean
    public SSLFactory sslFactory(@Value("${ssl.keystore-path}") String keyStorePath,
                                 @Value("${ssl.keystore-password}") char[] keyStorePassword,
                                 @Value("${ssl.truststore-path}") String trustStorePath,
                                 @Value("${ssl.truststore-password}") char[] trustStorePassword,
                                 @Value("${ssl.client-auth}") boolean isClientAuthenticationRequired) {

        return SSLFactory.builder()
                .withIdentityMaterial(keyStorePath, keyStorePassword)
                .withTrustMaterial(trustStorePath, trustStorePassword)
                .withNeedClientAuthentication(isClientAuthenticationRequired)
                .withDefaultTrustMaterial() // JDK truststore
                .withSystemTrustMaterial()  // OS truststore
                .build();
    }

}