Java 是否支持 Let's Encrypt 证书?

Does Java support Let's Encrypt certificates?

我正在开发一个 Java 应用程序,它通过 HTTP 查询远程服务器上的 REST API。出于安全原因,此通信应切换为 HTTPS。

既然 Let's Encrypt 开始了他们的 public 测试版,我想知道 Java 目前是否可以使用他们的证书(或确认将来可以使用)默认。

Let's Encrypt 获得了中间件 cross-signed by IdenTrust,这应该是个好消息。但是,我在此命令的输出中找不到这两个中的任何一个:

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

我知道可以在每台机器上手动添加受信任的 CA,但由于我的应用程序无需任何进一步配置即可免费下载和执行,因此我正在寻找可行的解决方案 "out of the box"。你有好消息告诉我吗?

[更新 2016-06-08:根据 https://bugs.openjdk.java.net/browse/JDK-8154757 IdenTrust CA 将包含在 Oracle Java 8u101 中。]

[更新 2016-08-05:Java 8u101 已发布并且确实包含 IdenTrust CA:release notes]


Does Java support Let's Encrypt certificates?

是的。 Let's Encrypt 证书只是一个普通的 public 密钥证书。 Java 支持它(根据 Let's Encrypt Certificate Compatibility,对于 Java 7 >= 7u111 和 Java 8 >= 8u101)。

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

否/这取决于 JVM。高达 8u66 的 Oracle JDK/JRE 的信任库既不包含专门的 Let's Encrypt CA 也不包含对其进行交叉签名的 IdenTrust CA。 new URL("https://letsencrypt.org/").openConnection().connect(); 例如导致 javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException.

但是,您可以提供自己的验证程序/定义包含所需根 CA 的自定义密钥库,或将证书导入 JVM 信任库。

https://community.letsencrypt.org/t/will-the-cross-root-cover-trust-by-the-default-list-in-the-jdk-jre/134/10 也讨论了该主题。


下面是一些示例代码,展示了如何在运行时将证书添加到默认信任库。您只需要添加证书(从 firefox 导出为 .der 并放入类路径)

基于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);
        }
    }

}

我知道 OP 要求不更改本地配置的解决方案,但如果您想将信任链永久添加到密钥库:

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

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

对于尚不支持 Let's Encrypt 证书的 JDK,您可以按照此过程将它们添加到 JDK cacerts(感谢 this)。

下载https://letsencrypt.org/certificates/上的所有证书(选择der格式)并用这种命令一个一个添加(例如letsencryptauthorityx1.der ):

keytool -import -keystore PATH_TO_JDK\jre\lib\security\cacerts -storepass changeit -noprompt -trustcacerts -alias letsencryptauthorityx1 -file PATH_TO_DOWNLOADS\letsencryptauthorityx1.der

我们这些愿意进行本地配置更改(包括备份配置文件)的人的详细答案:

1。在更改之前测试它是否正常工作

如果您还没有测试程序,可以使用我的 java SSLPing ping 程序来测试 TLS 握手(适用于任何 SSL/TLS 端口,而不仅仅是 HTTPS)。我将使用预构建的 SSLPing.jar,但阅读代码并自己构建它是一项快速而简单的任务:

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

由于我的 Java 版本早于 1.8.0_101(在撰写本文时尚未发布),Let's Encrypt 证书默认不会验证。在应用修复之前,让我们看看失败是什么样的:

$ 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。导入证书

我在 Mac OS X 上设置了 JAVA_HOME 环境变量。稍后的命令将假定为您正在修改的 java 安装设置了此变量:

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

备份我们将要修改的 cacerts 文件,这样您就可以撤销任何更改而无需重新安装 JDK:

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

下载我们需要导入的签名证书:

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

执行导入:

$ 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。验证它在更改后是否正常工作

验证 Java 现在可以正常连接到 SSL 端口:

$ java -jar SSLPing/dist/SSLPing.jar helloworld.letsencrypt.org 443
About to connect to 'helloworld.letsencrypt.org' on port 443
Successfully connected