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

时间:2014-03-11 13:36:32

标签: 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

我在提示输入Common Name时填写my-org.local。在我的hosts文件中,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程序仍然死于相同的堆栈跟踪。我做错了什么?

1 个答案:

答案 0 :(得分:0)

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

因此,我们首先检查你的cacerts是否有证书:

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

changeit是默认密码。这将只列出所有证书的所有者。您还可以使用自己的证书的关键字来检查它是否存在。

如果没有,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;
    }
} 
  

快速入门:

     
      
  1. 将您的主机添加到hostsToTrust字符串数组。

  2.   
  3. 调用CustomTrustManager.initSsl()。

  4.   
  5. 与您的主机建立SSL连接。

  6.   

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