137

I am developing a Java application that queries a REST API on a remote server over HTTP. For security reasons this communication should be switched to HTTPS.

Now that Let's Encrypt started their public beta, I'd like to know if Java currently works (or is confirmed to be working in the future) with their certificates by default.

Let's Encrypt got their intermediate cross-signed by IdenTrust, which should be good news. However, I cannot find any of these two in the output of this command:

keytool -keystore "..\lib\security\cacerts" -storepass changeit -list

I know that trusted CAs can be added manually on each machine, but since my application should be free to download and executable without any further configuration, I am looking for solutions that work "out of the box". Do you have good news for me?

Hexaholic
  • 3,299
  • 7
  • 30
  • 39
  • 2
    One can also check Let's Encrypt compatibility here https://letsencrypt.org/docs/certificate-compatibility/ – potame Jun 13 '17 at 08:54
  • @potame "with Java 8u131 you still have to add your the certificate to your truststore" so if you get a cert from Let's Encrypt, you will need to add the cert you got to the truststore? Shouldn't it be enough that their CA is included? – mxro Aug 25 '17 at 03:27
  • 1
    @mxro Hi -- thanks for drawing my attention onto this. My comments above don't hold true at all (in fact the problem was more complicated than that and related to our infrastructure) and I'm going to remove them because they're indeed only leading to confusion. So if you have a jdk > Java 8u101, Let's Encrypt certificate should work and be properly recognized and trusted. – potame Aug 25 '17 at 07:37
  • @potame That's excellent. Thank you for the clarification! – mxro Aug 25 '17 at 07:56

4 Answers4

153

[Update 2016-06-08: According to https://bugs.openjdk.java.net/browse/JDK-8154757 the IdenTrust CA will be included in Oracle Java 8u101.]

[Update 2016-08-05: Java 8u101 has been released and does indeed include the IdenTrust CA: release notes]


Does Java support Let's Encrypt certificates?

Yes. The Let's Encrypt certificate is just a regular public key certificate. Java supports it (according to Let's Encrypt Certificate Compatibility, for Java 7 >= 7u111 and Java 8 >= 8u101).

Does Java trust Let's Encrypt certificates out of the box?

No / it depends on the JVM. The truststore of Oracle JDK/JRE up to 8u66 contains neither the Let's Encrypt CA specifically nor the IdenTrust CA that cross signed it. new URL("https://letsencrypt.org/").openConnection().connect(); for example results in javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException.

You can however provide your own validator / define a custom keystore that contains the required root CA or import the certificate into the JVM truststore.

https://community.letsencrypt.org/t/will-the-cross-root-cover-trust-by-the-default-list-in-the-jdk-jre/134/10 discusses the topic as well.


Here is some example code that shows how to add a certificate to the default truststore at runtime. You'll just need to add the certificate (exported from firefox as .der and put in classpath)

Based on How can I get a list of trusted root certificates in Java? and http://developer.android.com/training/articles/security-ssl.html#UnknownCa

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.TrustManagerFactory;

public class SSLExample {
    // BEGIN ------- ADDME
    static {
        try {
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            Path ksPath = Paths.get(System.getProperty("java.home"),
                    "lib", "security", "cacerts");
            keyStore.load(Files.newInputStream(ksPath),
                    "changeit".toCharArray());

            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            try (InputStream caInput = new BufferedInputStream(
                    // this files is shipped with the application
                    SSLExample.class.getResourceAsStream("DSTRootCAX3.der"))) {
                Certificate crt = cf.generateCertificate(caInput);
                System.out.println("Added Cert for " + ((X509Certificate) crt)
                        .getSubjectDN());

                keyStore.setCertificateEntry("DSTRootCAX3", crt);
            }

            if (false) { // enable to see
                System.out.println("Truststore now trusting: ");
                PKIXParameters params = new PKIXParameters(keyStore);
                params.getTrustAnchors().stream()
                        .map(TrustAnchor::getTrustedCert)
                        .map(X509Certificate::getSubjectDN)
                        .forEach(System.out::println);
                System.out.println();
            }

            TrustManagerFactory tmf = TrustManagerFactory
                    .getInstance(TrustManagerFactory.getDefaultAlgorithm());
            tmf.init(keyStore);
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, tmf.getTrustManagers(), null);
            SSLContext.setDefault(sslContext);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    // END ---------- ADDME

    public static void main(String[] args) throws IOException {
        // signed by default trusted CAs.
        testUrl(new URL("https://google.com"));
        testUrl(new URL("https://www.thawte.com"));

        // signed by letsencrypt
        testUrl(new URL("https://helloworld.letsencrypt.org"));
        // signed by LE's cross-sign CA
        testUrl(new URL("https://letsencrypt.org"));
        // expired
        testUrl(new URL("https://tv.eurosport.com/"));
        // self-signed
        testUrl(new URL("https://www.pcwebshop.co.uk/"));

    }

    static void testUrl(URL url) throws IOException {
        URLConnection connection = url.openConnection();
        try {
            connection.connect();
            System.out.println("Headers of " + url + " => "
                    + connection.getHeaderFields());
        } catch (SSLHandshakeException e) {
            System.out.println("Untrusted: " + url);
        }
    }

}
potame
  • 7,597
  • 4
  • 26
  • 33
zapl
  • 63,179
  • 10
  • 123
  • 154
  • One more thing: Which file do I need to use? Let's Encrypt offers [one root and four intermediate certificates](https://letsencrypt.org/certificates/) for download. I tried `isrgrootx1.der` and `lets-encrypt-x1-cross-signed.der`, but neither of them seems to be the right one. – Hexaholic Dec 06 '15 at 16:41
  • @Hexaholic Depends on what you want to trust and how the site you use has certs configure. Go to `https://helloworld.letsencrypt.org` for example and inspect the certificate chain in the browser (clicking the green icon). For this one you need either the site specific certificate, the X1 intermediate one (cross signed by IdenTrust) or the DSTRootCAX3 one. The `ISRG Root X1` does not work for the helloworld site because it's not in the chain, that is the alternative chain. I would use the DSTRoot one, simply exported via browser because I've not seen it for download anywhere. – zapl Dec 06 '15 at 17:36
  • 1
    Thank you for the code. Don't forget to close the InputStream in keyStore.load(Files.newInputStream(ksPath)). – Michael Wyraz Feb 22 '16 at 20:01
  • Isn't this a huge security hole in Java? What is to stop any java application from always supplying its own CA, always using self-signed certs and bypassing the whole "trusted 3rd party" thing? – adapt-dev Apr 01 '16 at 02:30
  • 3
    @adapt-dev No, why should software trust an unknown system to supply good certificates? Software needs to be able to trust certificates it actually trusts. It would be a security hole if this would mean that my java program could install certificates for someone elses program. But that does not happen here, the code only adds the cert during it's own runtime – zapl Apr 01 '16 at 08:01
  • @zapl: you are such a hero. Thank you. For anyone who's a bit weary after hours of fruitless crunching, skips paragraphs and gets an exception when loading the cert from a custom path: this example expects the cert file in the resources next to `SSLExample`. Replace `SSLExample.class.getResourceAsStream("DSTRootCAX3.der")` with `FileInputStream fis = new FileInputStream(filePath)` – Rolf W. Jun 26 '16 at 20:37
  • Thank you, works perfectly! Small addendum if using Apache HttpClient: The default is ignored (at least for me), you actively need to set the newly enhanced context when building the client: `HttpClients.custom().setSSLContext( sslContext ).build();` – Sebastian R Jul 14 '16 at 11:25
  • 3
    +1 for showing code that doesn't just cheat by setting the `javax.net.ssl.trustStore` system property, but -1 for then setting the JVM's default `SSLContext`. It would be better to create a new `SSLSocketFactory` and then use that for various connections (e.g. `HttpUrlConnection`) rather than replacing the VM-wide SSL configuration. (I realize this change is only effective for the running JVM and does not persist or affect other programs. I just think it's a better programming practice to be explicit about where your configuration applies.) – Christopher Schultz Sep 14 '16 at 21:29
  • Java 7 update 111 doesn't seem to exist: http://java.com/en/download/faq/release_dates.xml. Last version is "update 80", and I quickly checked with "keytool -list" on its "lib/security/cacerts", and the IdenTrust CA certificate is not there... – xav Mar 11 '18 at 03:41
71

I know the OP asked for a solution without local configuration changes, but in case you want to add the trust chain to the keystore permanently:

$ keytool -trustcacerts \
    -keystore $JAVA_HOME/jre/lib/security/cacerts \
    -storepass changeit \
    -noprompt \
    -importcert \
    -file /etc/letsencrypt/live/hostname.com/chain.pem

source: https://community.letsencrypt.org/t/will-the-cross-root-cover-trust-by-the-default-list-in-the-jdk-jre/134/13

Jan Berkel
  • 3,373
  • 1
  • 30
  • 23
  • 6
    Yes, the OP didn't ask for it, but this was by far the simplest solution for me! – Auspex Nov 10 '16 at 16:35
  • I imported this cert https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txt into my older java 8 build and the web service that is using the lets encrypt certificate is not reachable! This solution was fast and easy. – ezwrighter Jan 16 '18 at 18:58
63

Detailed answer for those of us willing to make local config changes that includes backing up the config file:

1. Test if it is working before the changes

If you don't have a test program already, you can use my java SSLPing ping program which tests the TLS handshake (will work with any SSL/TLS port, not just HTTPS). I'll use the prebuilt SSLPing.jar, but reading the code and building it yourself is a quick and easy task:

$ git clone https://github.com/dimalinux/SSLPing.git
Cloning into 'SSLPing'...
[... output snipped ...]

Since my Java version is earlier than 1.8.0_101 (not released at the time of this writing), a Let's Encrypt certificate will not verify by default. Let's see what failure looks like before applying the fix:

$ java -jar SSLPing/dist/SSLPing.jar helloworld.letsencrypt.org 443
About to connect to 'helloworld.letsencrypt.org' on port 443
javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
[... output snipped ...]

2. Import the certificate

I'm on Mac OS X with the JAVA_HOME environment variable set. Later commands will assume this variable is set for the java installation you are modifying:

$ echo $JAVA_HOME 
/Library/Java/JavaVirtualMachines/jdk1.8.0_92.jdk/Contents/Home/

Make a backup of the cacerts file we will be modifying so you can back out any change without reinstalling the JDK:

$ sudo cp -a $JAVA_HOME/jre/lib/security/cacerts $JAVA_HOME/jre/lib/security/cacerts.orig

Download the signing certificate we need to import:

$ wget https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.der

Perform the import:

$ sudo keytool -trustcacerts -keystore $JAVA_HOME/jre/lib/security/cacerts -storepass changeit -noprompt -importcert -alias lets-encrypt-x3-cross-signed -file lets-encrypt-x3-cross-signed.der 
Certificate was added to keystore

3. Verify that it is working after the changes

Verify that Java is now happy connecting to the SSL port:

$ java -jar SSLPing/dist/SSLPing.jar helloworld.letsencrypt.org 443
About to connect to 'helloworld.letsencrypt.org' on port 443
Successfully connected
JRL
  • 3,363
  • 24
  • 36
dimalinux
  • 839
  • 8
  • 8
  • You can also check this more lightweight repo: https://github.com/commandercool/ssl-check . Usage is as simple as: `wget https://github.com/commandercool/ssl-check/raw/master/SSLCheck.class && java -cp . SSLCheck google.com 443`. – Aleksandr Erokhin Apr 12 '21 at 09:28
9

For JDK which do not support Let's Encrypt certificates yet, you can add those to the JDK cacerts following this process (thanks to this).

Download all the certificates on https://letsencrypt.org/certificates/ (choose the der format) and add them one by one with this kind of command (example for letsencryptauthorityx1.der):

keytool -import -keystore PATH_TO_JDK\jre\lib\security\cacerts -storepass changeit -noprompt -trustcacerts -alias letsencryptauthorityx1 -file PATH_TO_DOWNLOADS\letsencryptauthorityx1.der
Community
  • 1
  • 1
Anthony O.
  • 22,041
  • 18
  • 107
  • 163
  • This improves the situation but then I get Connection error: javax.net.ssl.SSLException: java.lang.RuntimeException: Could not generate DH keypair – nafg Dec 08 '16 at 19:35