如何在特定连接上使用不同的证书?

问题

我正在添加到我们的大型Java应用程序的模块必须与另一家公司的SSL安全网站进行交谈。问题是该站点使用自签名证书。我有一份证书副本,以验证我没有遇到中间人攻击,我需要将此证书合并到我们的代码中,以便与服务器的连接成功。

这是基本代码:

void sendRequest(String dataPacket) {
  String urlStr = "https://host.example.com/";
  URL url = new URL(urlStr);
  HttpURLConnection conn = (HttpURLConnection)url.openConnection();
  conn.setMethod("POST");
  conn.setRequestProperty("Content-Length", data.length());
  conn.setDoOutput(true);
  OutputStreamWriter o = new OutputStreamWriter(conn.getOutputStream());
  o.write(data);
  o.flush();
}

在没有为自签名证书进行任何额外处理的情况下,这会在conn.getOutputStream()处死,但有以下异常:

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
....
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
....
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

理想情况下,我的代码需要教Java接受这个自签名证书,对于应用程序中的这一点,以及其他任何地方。

我知道我可以将证书导入JRE的证书颁发机构商店,这将允许Java接受它。如果我能提供帮助,那不是我想采取的方法;对于他们可能不使用的一个模块,我们所有客户的机器上看起来非常具有侵入性;它会影响使用相同JRE的所有其他Java应用程序,我不喜欢它,即使访问此站点的任何其他Java应用程序的几率为零。这也不是一个简单的操作:在UNIX上我必须获得以这种方式修改JRE的访问权限。

我还看到我可以创建一个执行一些自定义检查的TrustManager实例。看起来我甚至可以创建一个TrustManager,在除了这个证书之外的所有实例中委托给真正的TrustManager。但看起来TrustManager是全局安装的,我认为会影响我们应用程序的所有其他连接,这对我来说也不是很合适。

设置Java应用程序以接受自签名证书的首选,标准或最佳方法是什么?我可以完成上面提到的所有目标,还是我必须妥协?是否有一个涉及文件和目录和配置设置的选项,以及几乎没有代码?


#1 热门回答(153 赞)

自己创建一个SSLSocket,并在连接前将其设置在HttpsURLConnection上。

...
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslFactory);
conn.setMethod("POST");
...

你想创建oneSSLSocketFactory并保持它。这是如何初始化它的草图:

/* Load the keyStore that includes self-signed cert as a "trusted" entry. */
KeyStore keyStore = ... 
TrustManagerFactory tmf = 
  TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);
sslFactory = ctx.getSocketFactory();

如果你在创建密钥库时需要帮助,请发表评论。

这是加载密钥库的示例:

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(trustStore, trustStorePassword);
trustStore.close();

要使用PEM格式证书创建密钥存储区,你可以使用CertificateFactory编写自己的代码,或者只从JDK导入keytool(keytoolwon'twork用于"密钥条目",但对于"可信条目"来说很好)。

keytool -import -file selfsigned.pem -alias server -keystore server.jks

#2 热门回答(14 赞)

如果创建aSSLSocketFactory不是一个选项,只需将密钥导入JVM即可

  • 检索公钥:$ openssl s_client -connect dev-server:443,然后创建一个文件dev-server.pem,看起来像----- BEGIN CERTIFICATE -----
    lklkkkllklklklklllkllklkl
    lklkkkllklklklklllkllklkl
    lklkkkllklk ....
    -----结束证书-----
  • 导入密钥:#keytool -import -alias dev-server -keystore $ JAVA_HOME / jre / lib / security / cacerts -file dev-server.pem。密码:改变
  • 重启JVM

来源:How to solve javax.net.ssl.SSLHandshakeException?


#3 热门回答(12 赞)

我们复制JRE的信任库并将我们的自定义证书添加到该信任库,然后告诉应用程序将自定义信任库与系统属性一起使用。这样我们就可以单独保留默认的JRE信任库。

缺点是当你更新JRE时,你的新信任库不会自动与你的自定义信任库合并。

你可以通过安装程序或启动例程来验证truststore / jdk并检查不匹配或自动更新信任库来处理此方案。我不知道如果在应用程序运行时更新信任库会发生什么。

这个解决方案不是100%优雅或万无一失,但它简单,有效,并且不需要代码。