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

Provide an SSLSocketFactoryFactoryBean #547

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

Provide an SSLSocketFactoryFactoryBean #547

marschall opened this issue Jan 8, 2020 · 12 comments
Assignees
Milestone

Comments

@marschall
Copy link

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

  • a truststore location
  • a truststore type
  • a truststore passphrase
  • a keystrore location, optional
  • a keystrore type, optional
  • a keystrore passphrase, optional
  • a list of cipher suites, optional
    that then creates an SSLSocketFactory implementation class. This would require dynamically generating an SSLSocketFactory subclass but would offer the most convent interface for clients.

This is a follow up for #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 rwinch self-assigned this Jan 25, 2021
@rwinch rwinch added status: waiting-for-feedback We need additional information before we can continue and removed status: waiting-for-feedback We need additional information before we can continue labels Jan 25, 2021
@rwinch
Copy link
Member

rwinch commented Jan 25, 2021

Any interest in providing a pull request?

@marschall
Copy link
Author

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
Copy link
Member

rwinch commented Jan 28, 2021

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
Copy link
Author

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
Copy link

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

@marschall
Copy link
Author

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

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

@renegrob
Copy link

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
Copy link

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
Copy link

jiananxu commented Aug 5, 2024

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
Copy link
Author

JndiSocketFactory.setSocketFactory(getSslSocketFactory());

That does not look particularly thread safe.

@Hakky54
Copy link

Hakky54 commented Aug 5, 2024

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();
    }

}

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

8 participants