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

138

我正在开发一个Java应用程序,通过HTTP查询远程服务器上的REST API。出于安全考虑,这种通信应该切换到HTTPS。

现在Let's Encrypt开始了公共测试版,我想知道Java目前是否默认使用他们的证书(或确认将来能够使用)。

Let's Encrypt通过IdenTrust进行了交叉签名,这应该是个好消息。然而,我在这个命令的输出中找不到其中任何一个:

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

我知道可信CA可以在每台机器上手动添加,但由于我的应用程序应该可以自由下载和执行而无需进一步配置,因此我正在寻找开箱即用的解决方案。 你有好消息吗?


2
您也可以在此处检查 Let's Encrypt 的兼容性 https://letsencrypt.org/docs/certificate-compatibility/ - potame
@potame "使用Java 8u131,您仍需要将证书添加到您的信任存储库",因此如果您获得了来自Let's Encrypt的证书,则需要将您获得的证书添加到信任存储库中吗?他们的CA不包含在内不就足够了吗? - mxro
1
@mxro您好——感谢您引起我的注意。我的上述评论根本不正确(事实上,问题比那更复杂,并且与我们的基础设施有关),我将将其删除,因为它们确实只会引起困惑。因此,如果您拥有jdk> Java 8u101,则Let's Encrypt证书应该可以正常工作并被正确识别和信任。 - potame
@potame 非常好,感谢您的澄清! - mxro
4个回答

153

[更新于2016年06月08日根据此链接,IdenTrust CA将包含在Oracle Java 8u101中。]

[更新于2016年08月05日:Java 8u101已发布并确实包含了IdenTrust CA: 发行说明]


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

是的。Let's Encrypt证书只是一个普通的公钥证书。Java支持它(根据Let's Encrypt证书兼容性,适用于Java 7 >= 7u111和Java 8 >= 8u101)。

Java是否默认信任Let's Encrypt证书?

否 / 这取决于JVM。 Oracle JDK/JRE的信任存储器直到8u66都不包含特定的Let's Encrypt CA或跨签名它的IdenTrust CA。例如,new URL("https://letsencrypt.org/").openConnection().connect(); 的结果是javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException

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

此链接也讨论了这个话题。


以下是一些示例代码,演示如何在运行时将证书添加到默认信任存储器。您只需添加证书(从Firefox中导出为.der并放置在类路径中)

基于如何在Java中获取受信任的根证书列表?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);
        }
    }

}

@Hexaholic 取决于您想要信任什么以及您使用的网站如何配置证书。例如,转到 https://helloworld.letsencrypt.org 并在浏览器中检查证书链 (单击绿色图标)。对于此示例,您需要站点特定证书、X1 中间证书 (由 IdenTrust 交叉签名) 或 DSTRootCAX3 证书之一。ISRG Root X1 对于 helloworld 网站不起作用,因为它不在链中,即备用链。我会使用 DSTRoot,通过浏览器导出,因为我没有在任何地方看到它可以下载。 - zapl
1
谢谢你的代码。别忘了在keyStore.load(Files.newInputStream(ksPath))中关闭InputStream。 - Michael Wyraz
这不是Java中一个巨大的安全漏洞吗?有什么方法可以阻止任何Java应用程序始终提供自己的CA,始终使用自签名证书并绕过整个“受信任的第三方”机制吗? - adapt-dev
3
@adapt-dev 不,为什么软件要信任一个未知系统来提供好的证书?软件需要能够信任它实际信任的证书。如果这意味着我的Java程序可以为其他人的程序安装证书,那将是一个安全漏洞。但这里并不会发生这种情况,代码只在自己的运行时添加证书。 - zapl
3
赞赏展示不仅通过设置javax.net.ssl.trustStore系统属性的代码,但批评设置了JVM默认的SSLContext。最好创建一个新的SSLSocketFactory并将其用于各种连接(例如HttpUrlConnection)而不是替换整个VM的SSL配置。(我意识到这种更改只在运行的JVM中生效,不会持续存在或影响其他程序。我只是认为显式说明您的配置适用于何处是更好的编程实践。) - Christopher Schultz
显示剩余4条评论

71
我知道原帖要求无需进行本地配置更改的解决方案,但如果您想永久将信任链添加到密钥库中,请按如下操作:
$ 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/JRE默认列表中的信任所覆盖。结论是,如果您使用的是较新版本的JDK/JRE,则不需要额外安装交叉根证书,因为它们已经包含在默认信任列表中。但是,如果您使用的是旧版的JDK/JRE,则需要手动安装交叉根证书才能确保正确的信任链。


6
是的,虽然提问者没有要求这样做,但对我来说这是最简单的解决方案! - Auspex
我将这个证书 https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txt 导入到我的老Java 8版本中,使用Lets Encrypt证书的Web服务无法访问!这个解决方案快速而简单。 - ezwrighter

63

对于那些愿意进行本地配置更改并包括备份配置文件的人,以下是详细答案:

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

您还可以查看这个更轻量级的仓库:https://github.com/commandercool/ssl-check。使用方法非常简单:`wget https://github.com/commandercool/ssl-check/raw/master/SSLCheck.class && java -cp . SSLCheck google.com 443`。 - Aleksandr Erokhin

9
对于不支持Let's Encrypt证书的JDK,您可以按照以下过程将它们添加到JDK的中(感谢此处链接)。
下载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

这会改善情况,但是接着我会收到连接错误:javax.net.ssl.SSLException: java.lang.RuntimeException: Could not generate DH keypair。 - nafg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接