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

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

这是基本的代码:

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接受它。 如果我可以帮忙的话,那不是我想要采取的方法; 对于我们所有客户的机器来说,对于一个不能使用的模块来说,这似乎是非常有创意的。 它会影响所有其他Java应用程序使用相同的JRE,我不喜欢,即使任何其他Java应用程序访问此网站的可能性为零。 这也不是一个简单的操作:在UNIX上,我必须获得访问权限才能以这种方式修改JRE。

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

设置Java应用程序接受自签名证书的首选,标准或最佳方式是什么? 我能完成上面所有我想到的目标吗?还是我将不得不妥协? 是否有涉及文件和目录以及configuration设置的选项,以及很less的代码?

自己创build一个SSLSocket工厂,并在连接之前将其设置在HttpsURLConnection

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

你会想创build一个SSLSocketFactory并保持它。 以下是如何初始化它的草图:

 /* 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(); 

如果您需要帮助创build密钥存储,请发表评论。


以下是加载密钥存储区的示例:

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

要创build带有PEM格式证书的密钥存储区,您可以使用CertificateFactory编写自己的代码,或者仅使用来自JDK的keytool导入它(keytool 不适用于“密钥条目”,但对于“可信”条目”)。

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

如果创buildSSLSocketFactory不是一个选项,只需将该键导入到JVM中

  1. 检索公钥: $openssl s_client -connect dev-server:443 ,然后创build一个文件dev-server.pem ,看起来像

     -----BEGIN CERTIFICATE----- lklkkkllklklklklllkllklkl lklkkkllklklklklllkllklkl lklkkkllklk.... -----END CERTIFICATE----- 
  2. 导入密钥: #keytool -import -alias dev-server -keystore $JAVA_HOME/jre/lib/security/cacerts -file dev-server.pem 。 密码: changeit

  3. 重新启动JVM

来源: 如何解决javax.net.ssl.SSLHandshakeException?

我们复制JRE的信任库并将我们的自定义证书添加到该信任库,然后告诉应用程序使用具有系统属性的自定义信任库。 这样我们就离开了默认的JRE信任库。

不足之处在于,当您更新JRE时,您不会将其新的信任库自动合并到您的自定义信任库中。

您可以通过安装程序或启动例程validationtruststore / jdk并检查不匹配或自动更新信任库来处理这种情况。 如果在应用程序运行时更新信任库,我不知道会发生什么情况。

这个解决scheme不是100%优雅或者简单,但是它很简单,工作,并且不需要代码。

当使用commons-httpclient访问带有自签名证书的内部https服务器时,我不得不这样做。 是的,我们的解决scheme是创build一个自定义的TrustManager,只需传递一切(loggingdebugging消息)。

这归结为有我们自己的SSLSocketFactory,从我们的本地SSLContext创buildSSL套接字,它被设置为只有我们的本地TrustManager关联它。 你根本不需要靠近keystore / certstore。

所以这是在我们的LocalSSLSocketFactory中:

 static { try { SSL_CONTEXT = SSLContext.getInstance("SSL"); SSL_CONTEXT.init(null, new TrustManager[] { new LocalSSLTrustManager() }, null); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("Unable to initialise SSL context", e); } catch (KeyManagementException e) { throw new RuntimeException("Unable to initialise SSL context", e); } } public Socket createSocket(String host, int port) throws IOException, UnknownHostException { LOG.trace("createSocket(host => {}, port => {})", new Object[] { host, new Integer(port) }); return SSL_CONTEXT.getSocketFactory().createSocket(host, port); } 

与实现SecureProtocolSocketFactory的其他方法一起。 LocalSSLTrustManager是上述虚拟信任pipe理器的实现。

我经历了很多地方在networking和networking上解决这个问题。 这是我工作的代码:

  ByteArrayInputStream derInputStream = new ByteArrayInputStream(app.certificateString.getBytes()); CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); X509Certificate cert = (X509Certificate) certificateFactory.generateCertificate(derInputStream); String alias = "alias";//cert.getSubjectX500Principal().getName(); KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); trustStore.load(null); trustStore.setCertificateEntry(alias, cert); KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); kmf.init(trustStore, null); KeyManager[] keyManagers = kmf.getKeyManagers(); TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); tmf.init(trustStore); TrustManager[] trustManagers = tmf.getTrustManagers(); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyManagers, trustManagers, null); URL url = new URL(someURL); conn = (HttpsURLConnection) url.openConnection(); conn.setSSLSocketFactory(sslContext.getSocketFactory()); 

app.certificateString是一个包含证书的string,例如:

  static public String certificateString= "-----BEGIN CERTIFICATE-----\n" + "MIIGQTCCBSmgAwIBAgIHBcg1dAivUzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE" + "BhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKzApBgNVBAsTIlNlY3VyZSBE" + ... a bunch of characters... "5126sfeEJMRV4Fl2E5W1gDHoOd6V==\n" + "-----END CERTIFICATE-----"; 

我已经testing过,如果你是自签名的,你可以把任何字符放在证书string中,只要你保持上面的确切结构。 我用我的笔记本电脑的terminal命令行获得证书string。