Does Java support Let's Encrypt certificates?

JavaSslKeystoreLets EncryptCa

Java Problem Overview


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?

Java Solutions


Solution 1 - Java

[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 https://stackoverflow.com/questions/3508050/how-can-i-get-a-list-of-trusted-root-certificates-in-java/3508175#3508175 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);
        }
    }

}

Solution 2 - Java

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

Solution 3 - Java

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

Solution 4 - Java

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionHexaholicView Question on Stackoverflow
Solution 1 - JavazaplView Answer on Stackoverflow
Solution 2 - JavaJan BerkelView Answer on Stackoverflow
Solution 3 - JavadimalinuxView Answer on Stackoverflow
Solution 4 - JavaAnthony O.View Answer on Stackoverflow