即使将我的自签名证书添加到 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:
Add your hosts to the hostsToTrust String array.
Call CustomTrustManager.initSsl().
Make SSL connections to your hosts.
话虽如此,我不建议更新 cacerts,因为它位于公共(public) JVM 位置,因此每个 Java 应用程序都会使用它。您应该为您正在使用的特定应用程序创建一个自定义 trustStore,并且只允许该应用程序信任自签名证书。结账this answer了解如何做到这一点。
关于java - 无法让 Java 信任我的自签名证书,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22327320/