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

174

我正在向我们的大型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应用程序以接受自签名证书的首选、标准或最佳方法是什么?我能否实现上述所有目标,还是我必须妥协?是否存在涉及文件、目录和配置设置的选项,以及很少的代码?

小修补方案:http://www.rgagnon.com/javadetails/java-fix-certificate-problem-in-HTTPS.html - user896832
23
@Hasenpriester:请不要建议使用这个页面。它会禁用所有的信任验证。你不仅会接受你想要的自签名证书,还会接受中间人攻击者呈现给你的任何证书。 - Bruno
这个回答解决了你的问题吗?在Java中同时使用自定义信任库和默认信任库 - undefined
5个回答

173

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

...
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslFactory);
conn.setMethod("POST");
...
你需要创建一个 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();

如果您需要帮助创建密钥库,请留言。


以下是加载密钥库的示例:

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

要创建带有PEM格式证书的密钥库,您可以使用CertificateFactory编写自己的代码,或者只需使用JDK中的keytool导入它(keytool无法用于"key entry",但对于"trusted entry"来说完全没有问题)。

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

4
非常感谢!其他人帮我指明了方向,但最终我采用了你的方法。我需要将PEM证书文件转换为JKS Java密钥库文件,我在这里找到了帮助:https://dev59.com/1EfRa4cB1Zd3GeqP6iFO - skiphoppy
2
很高兴它能够成功。很抱歉你在密钥库方面遇到了困难,我应该在我的回答中直接包含它。使用CertificateFactory并不太困难。实际上,我想我会为后来的人更新一下。 - erickson
5
这是一段时间之前的内容,所以我记不太清了,但我猜测其理由是在“大型Java应用程序”中,很可能会建立其他HTTP连接。设置全局属性可能会干扰正在运行的连接,或允许某个不受信任的方伪造服务器。这是按情况使用内置信任管理器的示例。 - erickson
5
正如原帖所述,“我的代码需要教Java接受这一个自签名证书,只针对应用程序中的这一处位置,而不涉及其他地方。” - erickson
1
@skiphoppy 嗯,我找到的唯一方法是创建一个临时的KeyStore(在内存中),并将所有“真实”密钥库的根证书添加到其中,或者(几乎等效地)从您信任的密钥库的所有条目中创建一个Set<TrustAnchor>,并使用它来初始化PKIXParameters,然后再用它来初始化TrustManagerFactory - erickson
显示剩余11条评论

19

我在许多网站上阅读了大量内容以解决这个问题。这是我编写的代码,使其正常工作:

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是一个包含证书的字符串,例如:

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

我已经测试过,只要遵循上述确切结构,如果证书为自签名,则可以在证书字符串中放入任何字符。我是使用笔记本电脑的终端命令行获取证书字符串的。


2
感谢分享,@Josh。我创建了一个小的Github项目,演示了您的代码的使用:https://github.com/aasaru/ConnectToTrustedServerExample - Master Drools

14
如果无法创建SSLSocketFactory,只需将密钥导入JVM即可。
  1. Retrieve the public key: $openssl s_client -connect dev-server:443, then create a file dev-server.pem that looks like

    -----BEGIN CERTIFICATE----- 
    lklkkkllklklklklllkllklkl
    lklkkkllklklklklllkllklkl
    lklkkkllklk....
    -----END CERTIFICATE-----
    
  2. Import the key: #keytool -import -alias dev-server -keystore $JAVA_HOME/jre/lib/security/cacerts -file dev-server.pem. Password: changeit

  3. Restart JVM

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


1
我不认为这解决了原来的问题,但它解决了我的问题,所以谢谢! - Ed Norris
1
这样做也不是个好主意:现在你有了这个随机额外的系统级自签名证书,默认情况下,所有 Java 进程都会信任它。 - user268396

12

当我使用commons-httpclient访问具有自签名证书的内部https服务器时,我不得不做类似的事情。是的,我们的解决方案是创建一个自定义TrustManager,只是简单地传递所有内容(同时记录调试消息)。

这归结于拥有我们自己的SSLSocketFactory,它从我们的本地SSLContext创建SSL套接字,该SSLContext设置为仅与我们的本地TrustManager关联。您根本不需要接近密钥库/证书存储。

因此,在我们的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);
}

LocalSSLTrustManager是实现了SecureProtocolSocketFactory的其他方法之一。如前所述,它是虚拟的信任管理器实现。


9
如果禁用所有的信任验证,使用 SSL/TLS 就失去了意义。在本地测试时可以接受,但连接到外部时则不行。 - Bruno
当我在Java 7上运行它时,出现了以下异常。javax.net.ssl.SSLHandshakeException: no cipher suites in common 你能提供帮助吗? - Uri Lukach

12
我们复制JRE的信任存储并将自定义证书添加到该信任存储中,然后使用系统属性告诉应用程序使用自定义信任存储。这样,我们就保留了默认的JRE信任存储。 缺点是当您更新JRE时,您不会自动将其新的信任存储与自定义信任存储合并。 您可以通过安装程序或启动例程来处理此场景,以验证信任存储/JDK并检查不匹配或自动更新信任存储。我不知道在应用程序运行时更新信任存储会发生什么。 这种解决方案不是百分之百优雅或万无一失,但它简单易行,并且不需要编写代码。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,