java - 无法让 Java 信任我的自签名证书

标签 java security ssl https

即使将我的自签名证书添加到 cacerts 后,我仍然无法让 Java 信任它

我在本地计算机上运行 nginx,作为具有自签名证书的 SSL 反向代理。我已经生成了这样的证书:

openssl req -new -nodes -keyout server.key -out server.csr
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

当提示输入通用名称时,我填写了 my-org.local。在我的主机文件中,my-org.local 是 localhost 的别名。

在浏览器中测试此设置时,我收到一条警告,指出该证书未由已知机构签署,这正是我所期望的。然后我告诉浏览器信任该证书,这成功了。

接下来,我编写了这个小型 Java 程序来验证是否可以让 Java 信任该证书:

import java.net.*;
import java.io.*;

public class Main {

    public static void main(String[] args) throws Exception {
        String url = "https://my-org.local/";
        URL obj = new URL(url);
        HttpURLConnection con = (HttpURLConnection) obj.openConnection();
        int responseCode = con.getResponseCode();
        System.out.println("\nSending 'GET' request to URL : " + url);
        System.out.println("Response Code : " + responseCode);
    }
}

如果我对此进行测试,例如https://google.com ,一切都按预期进行。针对我的本地计算机,我得到以下堆栈跟踪:

Exception in thread "main" 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
    at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
    at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1884)
    at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:276)
    at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:270)
    at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1341)
    at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:153)
    at sun.security.ssl.Handshaker.processLoop(Handshaker.java:868)
    at sun.security.ssl.Handshaker.process_record(Handshaker.java:804)
    at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1016)
    at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1312)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1339)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1323)
    at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:563)
    at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
    at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1300)
    at java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:468)
    at sun.net.www.protocol.https.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:338)
    at Main.main(Main.java:10)
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:385)
    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:326)
    at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:231)
    at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:126)
    at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1323)
    ... 13 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:196)
    at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:268)
    at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:380)
    ... 19 more

...这或多或少是我所期望的。

我的理解是,为了让 Java 信任我的自签名证书,我必须将该证书添加到 Java 的 cacerts 中。在我的机器(运行 Mavericks 的 Mac)上,可以在此处找到此文件:

/System/Library/Frameworks/JavaVM.framework/Home/lib/security/cacerts

这是我尝试添加证书的方法:

sudo keytool -import -keystore /System/Library/Frameworks/JavaVM.framework/Home/lib/security/cacerts -storepass changeit -noprompt -alias myorg -file server.crt

但这没有任何效果;我的小型 Java 程序仍然因相同的堆栈跟踪而终止。我做错了什么?

最佳答案

您收到 SSLHandshakeException,这显然意味着证书未成功导入到您的 cacerts 文件中。

所以,我们首先检查您的 cacerts 是否具有该证书:

echo 'changeit' | keytool -list -v -keystore $(find $JAVA_HOME -name cacerts) | grep 'Owner:'

changeit 是默认密码。这只会列出所有证书的所有者。您也可以通过 grep 自己的证书的关键字来检查它是否存在。

如果没有,this code在创建 cacerts 文件方面做得非常出色。我添加代码以供引用:

import javax.net.ssl.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

public class CustomTrustManager implements X509TrustManager
{
    private static final String JAVA_CA_CERT_FILE_NAME = "cacerts";
    private static final String CLASSIC_JAVA_CA_CERT_FILE_NAME = "jssecacerts";
    private static final int DEFAULT_HTTPS_PORT = 443;

    private String[] hostsToTrust = {"server1.company.com", "server2.company.com"};
    private char[] defaultCAKeystorePassphrase = "changeit".toCharArray();
    private KeyStore certificateTrustStore;
    private X509TrustManager defaultTrustManager;

    public static void initSsl()
    {
        try
        {
            SSLContext context = SSLContext.getInstance("TLS");
            context.init(null, new TrustManager[] { new CustomTrustManager() }, new SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());
        }
        catch (Exception e)
        {
            throw new RuntimeException(e);
        }
    }

    public CustomTrustManager()
    {
        try
        {
            initTrustStore();
            addTrustedHosts();
            initDefaultTrustManager();
        }
        catch (Exception e)
        {
            throw new RuntimeException(e);
        }
    }

    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException
    {
        defaultTrustManager.checkClientTrusted(chain, authType);
    }

    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException
    {
        defaultTrustManager.checkServerTrusted(chain, authType);
    }

    public X509Certificate[] getAcceptedIssuers()
    {
        return defaultTrustManager.getAcceptedIssuers();
    }

    private void initTrustStore() throws Exception
    {
        File javaTrustStoreFile = findJavaTrustStoreFile();
        InputStream inputStream = new FileInputStream(javaTrustStoreFile);
        certificateTrustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        certificateTrustStore.load(inputStream, defaultCAKeystorePassphrase);
        inputStream.close();
    }

    private void addTrustedHosts() throws Exception
    {
        SSLContext tempConnectContext = SSLContext.getInstance("TLS");
        ExtractX509CertTrustManager getX509CertTrustManager = new ExtractX509CertTrustManager();
        tempConnectContext.init(null, new TrustManager[] { getX509CertTrustManager }, null);
        SSLSocketFactory socketFactory = tempConnectContext.getSocketFactory();
        for (String host : hostsToTrust)
        {
            SSLSocket socket = (SSLSocket) socketFactory.createSocket(host, DEFAULT_HTTPS_PORT);
            // connect the socket to set the cert chain in getX509CertTrustManager
            socket.startHandshake();
            for (X509Certificate cert : getX509CertTrustManager.getCurrentChain())
            {
                if (!certificateTrustStore.isCertificateEntry(host))
                {
                    certificateTrustStore.setCertificateEntry(host, cert);
                }
            }
        }
    }

    private void initDefaultTrustManager() throws Exception
    {
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(certificateTrustStore);
        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
        for (TrustManager trustManager : trustManagers)
        {
            if (trustManager instanceof X509TrustManager)
            {
                defaultTrustManager = (X509TrustManager) trustManager;
                break;
            }
        }
    }

    /**
     * Trust Manager for the sole purpose of retrieving the X509 cert when a connection is made to a host we want
     * to start trusting.
     */
    private static class ExtractX509CertTrustManager implements X509TrustManager
    {
        private X509Certificate[] currentChain;
        public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { }
        public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException
        {
            currentChain = x509Certificates;
        }
        public X509Certificate[] getAcceptedIssuers() { return null; }
        public X509Certificate[] getCurrentChain()
        {
            return currentChain;
        }
    }

    private File findJavaTrustStoreFile()
    {
        File javaHome = new File(System.getProperty("java.home") + File.separatorChar + "lib" + File.separatorChar + "security");
        File caCertsFile = new File(javaHome, JAVA_CA_CERT_FILE_NAME);
        if (!caCertsFile.exists() || !caCertsFile.isFile())
        {
            caCertsFile = new File(javaHome, CLASSIC_JAVA_CA_CERT_FILE_NAME);
        }
        return caCertsFile;
    }
} 

To get started quickly:

  1. Add your hosts to the hostsToTrust String array.

  2. Call CustomTrustManager.initSsl().

  3. Make SSL connections to your hosts.

话虽如此,我不建议更新 cacerts,因为它位于公共(public) JVM 位置,因此每个 Java 应用程序都会使用它。您应该为您正在使用的特定应用程序创建一个自定义 trustStore,并且只允许该应用程序信任自签名证书。结账this answer了解如何做到这一点。

关于java - 无法让 Java 信任我的自签名证书,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22327320/

相关文章:

java - 在所有内部异常中查找特定类型的自定义异常

java - Spring Autowiring 自动检测

security - Tomcat:针对来自某些接口(interface)的请求阻止某些路径的请求

java - Spring安全循环bean依赖错误

Android 应用程序使用 Socket.io 连接到 Node.js 服务器

java - hibernate搜索将一个实体索引到两个不同的索引

php - 网站安全 - 帮我少吸点

tomcat - 如何在 Tomcat 6 上配置 SSL 证书?

django - 在 Apache 上设置 Django 应用程序后 CSRF 失败

java - 多个 .replace 给出 NoSuchElementException