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

Setting up a truststore without a keystore #6493

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

Setting up a truststore without a keystore #6493

robert-gv opened this issue Jul 28, 2016 · 7 comments
Labels
status: declined A suggestion or change that we don't feel we should currently apply

Comments

@robert-gv
Copy link

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.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jul 28, 2016
@philwebb philwebb added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels Aug 12, 2016
@philwebb
Copy link
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?

@philwebb philwebb added the status: waiting-for-feedback We need additional information before we can continue label Aug 12, 2016
@robert-gv
Copy link
Author

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.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Aug 15, 2016
@philwebb
Copy link
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.

@philwebb philwebb added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels Aug 29, 2016
@spring-projects-issues
Copy link
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.

@spring-projects-issues spring-projects-issues added the status: feedback-reminder We've sent a reminder that we need additional information before we can continue label Sep 5, 2016
@robert-gv
Copy link
Author

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

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue status: feedback-reminder We've sent a reminder that we need additional information before we can continue labels Sep 5, 2016
@imgx64
Copy link
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
Copy link
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.

@wilkinsona wilkinsona added status: declined A suggestion or change that we don't feel we should currently apply and removed status: feedback-provided Feedback has been provided type: enhancement A general enhancement labels Feb 7, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: declined A suggestion or change that we don't feel we should currently apply
Projects
None yet
Development

No branches or pull requests

5 participants