Problem
Often we want to connect to create a secure SSL connection to an HTTPs endpoint which is secured by a self-signed Certificate. If we do so just with a simple call we usually face an nice exception like:
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:387) at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:292) at sun.security.validator.Validator.validate(Validator.java:260) at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:324) at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:229) at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:124) at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1479) ... 47 more Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target at sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:145) at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:131) at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:280) at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:382) ... 53 more
Solution
Often I see that either somebody uses an trust all or just overwrites the default Socket Factory (HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory())
). This is maybe okay for just a test but should be used in production-ready code.
Overall it is simple to fix it. We just need to know how the stuff works. In short, we have usually always:
- SSLSocketFactory — creates the Socket connection in case of an SSL connection
- SSLContext — responsible for the verification of the certificate
Export Certificate
First, we need to get the certificate. Either use Firefox to download it or the command line:
openssl s_client -connect google.de:443 -showcerts
The pem
file is here e.g.:
-----BEGIN CERTIFICATE----- MIIDfTCCAuagAwIBAgIDErvmMA0GCSqGSIb3DQEBBQUAME4xCzAJBgNVBAYTAlVT MRAwDgYDVQQKEwdFcXVpZmF4MS0wKwYDVQQLEyRFcXVpZmF4IFNlY3VyZSBDZXJ0 aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDIwNTIxMDQwMDAwWhcNMTgwODIxMDQwMDAw WjBCMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UE AxMSR2VvVHJ1c3QgR2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB CgKCAQEA2swYYzD99BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9m OSm9BXiLnTjoBbdqfnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIu T8rxh0PBFpVXLVDviS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6c JmTM386DGXHKTubU1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmR Cw7+OC7RHQWa9k0+bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5asz PeE4uwc2hGKceeoWMPRfwCvocWvk+QIDAQABo4HwMIHtMB8GA1UdIwQYMBaAFEjm aPkr0rKV10fYIyAQTzOYkJ/UMB0GA1UdDgQWBBTAephojYn7qwVkDBF9qn1luMrM TjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjA6BgNVHR8EMzAxMC+g LaArhilodHRwOi8vY3JsLmdlb3RydXN0LmNvbS9jcmxzL3NlY3VyZWNhLmNybDBO BgNVHSAERzBFMEMGBFUdIAAwOzA5BggrBgEFBQcCARYtaHR0cHM6Ly93d3cuZ2Vv dHJ1c3QuY29tL3Jlc291cmNlcy9yZXBvc2l0b3J5MA0GCSqGSIb3DQEBBQUAA4GB AHbhEm5OSxYShjAGsoEIz/AIx8dxfmbuwu3UOx//8PDITtZDOLC5MH0Y0FWDomrL NhGc6Ehmo21/uBPUR/6LWlxz/K7ZGzIZOKuXNBSqltLroxwUCEm2u+WR74M26x1W b8ravHNjkOR/ez4iyz0H7V84dJzjA1BOoa+Y7mHyhD8S -----END CERTIFICATE-----
Create SSLContext including the Certificate
You can either import this pem file into your trust store or just create the trust store using the pem on the fly:
public static SSLContext buildSslContext(InputStream... inputStreams) throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException, KeyManagementException { X509Certificate cert; KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); trustStore.load(null); for (InputStream inputStream : inputStreams) { try { CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); cert = (X509Certificate)certificateFactory.generateCertificate(inputStream); } finally { IOUtils.closeQuietly(inputStream); } String alias = cert.getSubjectX500Principal().getName(); trustStore.setCertificateEntry(alias, cert); } TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); tmf.init(trustStore); TrustManager[] trustManagers = tmf.getTrustManagers(); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustManagers, null); return sslContext; }
Put the SSLSocketFactory together
I assume the pem
file is in the source folder here, for maven src/main/resources
.
SSLContext sslContext = CryptoUtil.buildSslContext( this.getClass().getResourceAsStream("/my-cert.pem")); SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext);
Now we have the SSLSocketFactory, lets put it to use.
Use the SSLConnectionSocketFactory
Here I will use the created socket factory to configure the Apache HTTP client and use it again in the Spring RestTemplate:
// this two lines are just repeated from above SSLContext sslContext = CryptoUtil.buildSslContext(this.getClass().getResourceAsStream("/beta-comfylight-cert.pem")); SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext); // here we create the Http Client using our SSL Socket Factory and so trust relation CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(csf).build(); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); requestFactory.setHttpClient(httpClient); // create the spring REST Template using our apache HTTP client RestTemplate restTemplate = new RestTemplate(requestFactory);
Summary
What have we achieved now?
- We have an SSL Socket Factory which just trust our „server“ / „certificate“
- We haven’t overwritten any default socket factory in the application, which could cause problems in other subsystems as we then just trust „us“
- We haven’t used a trust all, which would be fine for a test but would render the SSL encryption useless in production (man in the middle attack…)