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

Setting up a truststore without a keystore #6493

Closed
robert-gv opened this Issue Jul 28, 2016 · 7 comments

Comments

Projects
None yet
5 participants
@robert-gv

robert-gv commented Jul 28, 2016

I would like to be able to run a spring boot webserver that connects to other servers using the SSL protocol that uses self-signed certificates.
To do this I now have to specify the javax.net.ssl.trustStore and javax.net.ssl.trustStorePassword system properties when starting the application.

I would like to be able to set this up using my application.properties, so that all configuration is in one place, and I can use classpath to locate the trust store.

I can specify the server.ssl.trust-store and server.ssl.trust-store-password
but this is not picked up without also specifying server.ssl.key-store and related properties.

The main problem then becomes that the spring boot application will start with a https connector (and no http connector), while actually I have no interest to run in https mode.
The spring boot server just needs to connect to other servers with https.

My feature request is that you are able to set up a trust store without having to specify properties related to running the server in https mode.

@philwebb

This comment has been minimized.

Show comment
Hide comment
@philwebb

philwebb Aug 12, 2016

Member

The server.ssl.trust-store and server.ssl.trust-store-password are specifically to setup entries for the embedded server. If I understand you correctly you want something similar for client connections that you make yourself.

How are you making these connections? Do you use RestTemplate?

Member

philwebb commented Aug 12, 2016

The server.ssl.trust-store and server.ssl.trust-store-password are specifically to setup entries for the embedded server. If I understand you correctly you want something similar for client connections that you make yourself.

How are you making these connections? Do you use RestTemplate?

@robert-gv

This comment has been minimized.

Show comment
Hide comment
@robert-gv

robert-gv Aug 15, 2016

Yes, indeed. I want something similar for client connections. I'm not making the client connections directly. In this case they are created by a keycloak adapter. I see references to RestTemplate in this library.

robert-gv commented Aug 15, 2016

Yes, indeed. I want something similar for client connections. I'm not making the client connections directly. In this case they are created by a keycloak adapter. I see references to RestTemplate in this library.

@philwebb

This comment has been minimized.

Show comment
Hide comment
@philwebb

philwebb Aug 29, 2016

Member

@robert-gv Do you have any sample code that shows what you're currently doing? That might be a good start in us creating auto-configuration.

Member

philwebb commented Aug 29, 2016

@robert-gv Do you have any sample code that shows what you're currently doing? That might be a good start in us creating auto-configuration.

@spring-issuemaster

This comment has been minimized.

Show comment
Hide comment
@spring-issuemaster

spring-issuemaster Sep 5, 2016

Collaborator

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Collaborator

spring-issuemaster commented Sep 5, 2016

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

@robert-gv

This comment has been minimized.

Show comment
Hide comment
@robert-gv

robert-gv Sep 5, 2016

I don't think the sample code will add much to the information already provided, but here you go.
You can see in the Application.java what I would like to be able to set in the application.properties

truststore-example.zip

robert-gv commented Sep 5, 2016

I don't think the sample code will add much to the information already provided, but here you go.
You can see in the Application.java what I would like to be able to set in the application.properties

truststore-example.zip

@imgx64

This comment has been minimized.

Show comment
Hide comment
@imgx64

imgx64 Sep 6, 2016

Contributor

For what it's worth, this is how we do it. We put the self-signed server certificate in src/main/resources, and add a custom property app.ssl.trusted-certificate-location = classpath:server.cert.pem. We use a certificate instead of a keystore because it's easier to export from the server.

In the code, we parse the certificate and add it to a custom X509TrustManager that trusts both the default truststore and the included certificate (because we use valid certificates for production, and self-signed for staging). Then we call SSLContext.setDefault(sslContext) and HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory()). Only the former should be needed, but it seems to be ignored by IBM WebSphere, so we call the latter as well.

SslConfig.java

@Configuration
public class SslConfig {
    private static final Logger logger = LoggerFactory.getLogger(SslConfig.class);

    @Bean
    public Configured configure(SslProperties sslProperties, ResourceLoader resourceLoader)
            throws GeneralSecurityException, IOException {
        String certLocation = sslProperties.getTrustedCertificateLocation();
        if (!StringUtils.hasText(certLocation)) {
            return new Configured();
        }

        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        Collection<Certificate> certificates = new ArrayList<Certificate>();

        Resource certificateResource = resourceLoader.getResource(certLocation);
        InputStream inputStream = null;
        try {
            inputStream = certificateResource.getInputStream();
            certificates.addAll(certificateFactory.generateCertificates(inputStream));
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException ex) {
                    logger.warn("Could not close resource InputStream for {}", certLocation, ex);
                }
            }
        }

        ExtraCertsTrustManager extraCertsTrustManager = new ExtraCertsTrustManager(certificates);
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, new TrustManager[] { extraCertsTrustManager }, null);

        SSLContext.setDefault(sslContext);
        // Required for WebSphere
        HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());

        return new Configured();
    }

    /**
     * Dummy bean to notify that SSL is configured
     */
    public static class Configured {
        private Configured() {
        }
    }
}

SslProperties.java

@Component
@ConfigurationProperties(prefix = "app.ssl")
public class SslProperties {

    /**
     * Location of an X.509 certificate file. Can use classpath: prefix to use
     * certificate file from resources.
     */
    private String trustedCertificateLocation;

    public String getTrustedCertificateLocation() {
        return trustedCertificateLocation;
    }

    public void setTrustedCertificateLocation(String trustedCertificateLocation) {
        this.trustedCertificateLocation = trustedCertificateLocation;
    }
}

ExtraCertsTrustManager.java

public class ExtraCertsTrustManager implements X509TrustManager {

    private final X509TrustManager defaultX509TrustManager;
    private final X509TrustManager extraX509TrustManager;

    public ExtraCertsTrustManager(Collection<Certificate> certificates) throws GeneralSecurityException {
        defaultX509TrustManager = createX509TrustManager(null);

        KeyStore extraKeyStore = createKeyStore(certificates);
        extraX509TrustManager = createX509TrustManager(extraKeyStore);
    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        CertificateException ex1 = null;
        if (defaultX509TrustManager != null) {
            try {
                defaultX509TrustManager.checkClientTrusted(chain, authType);
                // Success
                return;
            } catch (CertificateException ex) {
                ex1 = ex;
            }
        }

        CertificateException ex2 = null;
        if (extraX509TrustManager != null) {
            try {
                extraX509TrustManager.checkClientTrusted(chain, authType);
                // Success
                return;
            } catch (CertificateException ex) {
                ex2 = ex;
            }
        }

        if (ex1 != null) {
            throw ex1;
        }
        if (ex2 != null) {
            throw ex2;
        }

        throw new CertificateException("No trust managers");
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        CertificateException ex1 = null;
        if (defaultX509TrustManager != null) {
            try {
                defaultX509TrustManager.checkServerTrusted(chain, authType);
                // Success
                return;
            } catch (CertificateException ex) {
                ex1 = ex;
            }
        }

        CertificateException ex2 = null;
        if (extraX509TrustManager != null) {
            try {
                extraX509TrustManager.checkServerTrusted(chain, authType);
                // Success
                return;
            } catch (CertificateException ex) {
                ex2 = ex;
            }
        }

        if (ex1 != null) {
            throw ex1;
        }
        if (ex2 != null) {
            throw ex2;
        }

        throw new CertificateException("No trust managers");
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        Set<X509Certificate> acceptedIssuers = new HashSet<X509Certificate>();

        if (defaultX509TrustManager != null) {
            Collections.addAll(acceptedIssuers, defaultX509TrustManager.getAcceptedIssuers());
        }

        if (extraX509TrustManager != null) {
            Collections.addAll(acceptedIssuers, extraX509TrustManager.getAcceptedIssuers());
        }

        return acceptedIssuers.toArray(new X509Certificate[acceptedIssuers.size()]);
    }

    private KeyStore createKeyStore(Collection<Certificate> certificates) throws KeyStoreException {
        KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());

        try {
            keystore.load(null, null);
        } catch (IOException ex) {
            // Should never happen
            throw new RuntimeException(ex);
        } catch (GeneralSecurityException ex) {
            // Should never happen
            throw new RuntimeException(ex);
        }

        for (Certificate certificate : certificates) {
            String alias = certificate.toString();
            keystore.setCertificateEntry(alias, certificate);
        }

        return keystore;
    }

    private X509TrustManager createX509TrustManager(KeyStore keystore) throws GeneralSecurityException {
        TrustManagerFactory trustManagerFactory = //
                TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keystore);

        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();

        if (trustManagers.length == 0) {
            return null;
        }

        if (trustManagers.length > 1) {
            throw new GeneralSecurityException(String.format( //
                    "Expected 1 TrustManager from TrustManagerFactory(%s), got %s", //
                    trustManagerFactory, trustManagers.length));
        }

        TrustManager trustManager = trustManagers[0];
        if (!(trustManager instanceof X509TrustManager)) {
            throw new GeneralSecurityException(String.format( //
                    "Expected %s from TrustManagerFactory(%s), got %s", //
                    X509TrustManager.class.getCanonicalName(), trustManagerFactory,
                    trustManager.getClass().getCanonicalName()));
        }

        return (X509TrustManager) trustManager;
    }

}
Contributor

imgx64 commented Sep 6, 2016

For what it's worth, this is how we do it. We put the self-signed server certificate in src/main/resources, and add a custom property app.ssl.trusted-certificate-location = classpath:server.cert.pem. We use a certificate instead of a keystore because it's easier to export from the server.

In the code, we parse the certificate and add it to a custom X509TrustManager that trusts both the default truststore and the included certificate (because we use valid certificates for production, and self-signed for staging). Then we call SSLContext.setDefault(sslContext) and HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory()). Only the former should be needed, but it seems to be ignored by IBM WebSphere, so we call the latter as well.

SslConfig.java

@Configuration
public class SslConfig {
    private static final Logger logger = LoggerFactory.getLogger(SslConfig.class);

    @Bean
    public Configured configure(SslProperties sslProperties, ResourceLoader resourceLoader)
            throws GeneralSecurityException, IOException {
        String certLocation = sslProperties.getTrustedCertificateLocation();
        if (!StringUtils.hasText(certLocation)) {
            return new Configured();
        }

        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
        Collection<Certificate> certificates = new ArrayList<Certificate>();

        Resource certificateResource = resourceLoader.getResource(certLocation);
        InputStream inputStream = null;
        try {
            inputStream = certificateResource.getInputStream();
            certificates.addAll(certificateFactory.generateCertificates(inputStream));
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException ex) {
                    logger.warn("Could not close resource InputStream for {}", certLocation, ex);
                }
            }
        }

        ExtraCertsTrustManager extraCertsTrustManager = new ExtraCertsTrustManager(certificates);
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, new TrustManager[] { extraCertsTrustManager }, null);

        SSLContext.setDefault(sslContext);
        // Required for WebSphere
        HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());

        return new Configured();
    }

    /**
     * Dummy bean to notify that SSL is configured
     */
    public static class Configured {
        private Configured() {
        }
    }
}

SslProperties.java

@Component
@ConfigurationProperties(prefix = "app.ssl")
public class SslProperties {

    /**
     * Location of an X.509 certificate file. Can use classpath: prefix to use
     * certificate file from resources.
     */
    private String trustedCertificateLocation;

    public String getTrustedCertificateLocation() {
        return trustedCertificateLocation;
    }

    public void setTrustedCertificateLocation(String trustedCertificateLocation) {
        this.trustedCertificateLocation = trustedCertificateLocation;
    }
}

ExtraCertsTrustManager.java

public class ExtraCertsTrustManager implements X509TrustManager {

    private final X509TrustManager defaultX509TrustManager;
    private final X509TrustManager extraX509TrustManager;

    public ExtraCertsTrustManager(Collection<Certificate> certificates) throws GeneralSecurityException {
        defaultX509TrustManager = createX509TrustManager(null);

        KeyStore extraKeyStore = createKeyStore(certificates);
        extraX509TrustManager = createX509TrustManager(extraKeyStore);
    }

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        CertificateException ex1 = null;
        if (defaultX509TrustManager != null) {
            try {
                defaultX509TrustManager.checkClientTrusted(chain, authType);
                // Success
                return;
            } catch (CertificateException ex) {
                ex1 = ex;
            }
        }

        CertificateException ex2 = null;
        if (extraX509TrustManager != null) {
            try {
                extraX509TrustManager.checkClientTrusted(chain, authType);
                // Success
                return;
            } catch (CertificateException ex) {
                ex2 = ex;
            }
        }

        if (ex1 != null) {
            throw ex1;
        }
        if (ex2 != null) {
            throw ex2;
        }

        throw new CertificateException("No trust managers");
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        CertificateException ex1 = null;
        if (defaultX509TrustManager != null) {
            try {
                defaultX509TrustManager.checkServerTrusted(chain, authType);
                // Success
                return;
            } catch (CertificateException ex) {
                ex1 = ex;
            }
        }

        CertificateException ex2 = null;
        if (extraX509TrustManager != null) {
            try {
                extraX509TrustManager.checkServerTrusted(chain, authType);
                // Success
                return;
            } catch (CertificateException ex) {
                ex2 = ex;
            }
        }

        if (ex1 != null) {
            throw ex1;
        }
        if (ex2 != null) {
            throw ex2;
        }

        throw new CertificateException("No trust managers");
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        Set<X509Certificate> acceptedIssuers = new HashSet<X509Certificate>();

        if (defaultX509TrustManager != null) {
            Collections.addAll(acceptedIssuers, defaultX509TrustManager.getAcceptedIssuers());
        }

        if (extraX509TrustManager != null) {
            Collections.addAll(acceptedIssuers, extraX509TrustManager.getAcceptedIssuers());
        }

        return acceptedIssuers.toArray(new X509Certificate[acceptedIssuers.size()]);
    }

    private KeyStore createKeyStore(Collection<Certificate> certificates) throws KeyStoreException {
        KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());

        try {
            keystore.load(null, null);
        } catch (IOException ex) {
            // Should never happen
            throw new RuntimeException(ex);
        } catch (GeneralSecurityException ex) {
            // Should never happen
            throw new RuntimeException(ex);
        }

        for (Certificate certificate : certificates) {
            String alias = certificate.toString();
            keystore.setCertificateEntry(alias, certificate);
        }

        return keystore;
    }

    private X509TrustManager createX509TrustManager(KeyStore keystore) throws GeneralSecurityException {
        TrustManagerFactory trustManagerFactory = //
                TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keystore);

        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();

        if (trustManagers.length == 0) {
            return null;
        }

        if (trustManagers.length > 1) {
            throw new GeneralSecurityException(String.format( //
                    "Expected 1 TrustManager from TrustManagerFactory(%s), got %s", //
                    trustManagerFactory, trustManagers.length));
        }

        TrustManager trustManager = trustManagers[0];
        if (!(trustManager instanceof X509TrustManager)) {
            throw new GeneralSecurityException(String.format( //
                    "Expected %s from TrustManagerFactory(%s), got %s", //
                    X509TrustManager.class.getCanonicalName(), trustManagerFactory,
                    trustManager.getClass().getCanonicalName()));
        }

        return (X509TrustManager) trustManager;
    }

}
@wilkinsona

This comment has been minimized.

Show comment
Hide comment
@wilkinsona

wilkinsona Feb 7, 2017

Member

IMO, setting the default SSLContext is too broad and isn't something that we should do via auto-configuration.

A quick search in Eclipse shows me that the default context is used by Cassandra's driver, RabbitMQ's client, Tomcat, Jetty, etc. While I'm sure it works very nicely in the context of a specific application, I think we might break things in ways that are difficult to debug if we applied this approach more generally. Furthermore, you may want each different sort of client that's using SSL to have different certificates that it trusts. The concerns described above also largely apply to configuring the javax.net.ssl.trustStore and javax.net.ssl.trustStorePassword system properties as well.

I think we're left with making sure it's easy to configure a truststore on clients that may be using SSL. Rather than trying to tackle all of them on a case-by-case basic, I'd prefer to consider each type of client individually and see what requirements people have so I'm going to close this issue.

Anyone looking for easy truststore configuration for a particular type of client, please open a new issue stating the client that you're using and providing as much detail as possible about what you'd like to configure.

Member

wilkinsona commented Feb 7, 2017

IMO, setting the default SSLContext is too broad and isn't something that we should do via auto-configuration.

A quick search in Eclipse shows me that the default context is used by Cassandra's driver, RabbitMQ's client, Tomcat, Jetty, etc. While I'm sure it works very nicely in the context of a specific application, I think we might break things in ways that are difficult to debug if we applied this approach more generally. Furthermore, you may want each different sort of client that's using SSL to have different certificates that it trusts. The concerns described above also largely apply to configuring the javax.net.ssl.trustStore and javax.net.ssl.trustStorePassword system properties as well.

I think we're left with making sure it's easy to configure a truststore on clients that may be using SSL. Rather than trying to tackle all of them on a case-by-case basic, I'd prefer to consider each type of client individually and see what requirements people have so I'm going to close this issue.

Anyone looking for easy truststore configuration for a particular type of client, please open a new issue stating the client that you're using and providing as much detail as possible about what you'd like to configure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment