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


164

我要添加到大型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应用程序以接受自签名证书的首选,标准或最佳方法是什么?我是否可以实现上述所有目标,还是必须做出让步?是否有涉及文件和目录以及配置设置的选项,以及几乎没有代码的选项?


小,工作修复:rgagnon.com/javadetails/...

20
@Hasenpriester:请不要建议此页面。它禁用所有信任验证。您不仅要接受所需的自签名证书,还要接受MITM攻击者向您提供的任何证书。
布鲁诺

Answers:


168

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,也可以keytool仅从JDK 导入它(密钥工具不适用于“密钥项”,但对于“受信任的项”来说就很好)。

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

3
非常感谢你!其他人帮助我指出了正确的方向,但最终您是我采取的方法。我完成了将PEM证书文件转换为JKS Java密钥库文件的艰巨任务,在这里我找到了帮助:stackoverflow.com/questions/722931/…–
skiphoppy

1
我很高兴它解决了。对不起,您在密钥存储区中苦苦挣扎;我应该在回答中包括它。使用CertificateFactory并不难。实际上,我想我会为以后再来的任何人进行更新。
埃里克森

1
这些代码所做的所有事情都是通过设置JSSE参考指南中所述的三个系统属性来复制您可以完成的工作。
罗恩侯爵

2
@EJP:这是前一阵子,所以我不确定,但是我猜测其基本原理是在“大型Java应用程序”中,可能还会建立其他HTTP连接。设置全局属性可能会干扰正常工作的连接,或者使该一方欺骗服务器。这是根据情况使用内置信任管理器的示例。
艾里克森(Erickson)2012年

2
正如OP所说,“我的代码需要教Java接受针对应用程序中这一点的这一自签名证书,并且别无其他。”
艾里克森(Erickson)2012年

18

我通过在线阅读很多地方来解决此问题。这是我编写的使它起作用的代码:

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-----";

我测试过,只要您保持上面的确切结构,就可以在证书字符串中放置任何字符(如果它是自签名的)。我通过笔记本电脑的“终端”命令行获得了证书字符串。


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

14

如果创建a SSLSocketFactory不是一个选择,只需将密钥导入JVM

  1. 检索公用密钥: $openssl s_client -connect dev-server:443,然后创建一个看起来像下面的文件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?


1
我认为这不能解决原始问题,但可以解决我的问题,所以谢谢!
Ed Norris

这样做也不是一个好主意:现在,您将拥有这个随机的,系统范围外的自签名证书,所有 Java进程默认都会信任该证书。
user268396 '18 -10-5

12

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

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

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

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


12

当使用commons-httpclient访问具有自签名证书的内部https服务器时,我必须做这样的事情。是的,我们的解决方案是创建一个自定义的TrustManager,它仅传递所有内容(记录调试消息)。

这归结于拥有我们自己的SSLSocketFactory,该SSLSocketFactory从我们的本地SSLContext创建SSL套接字,该SSLSocketFactory设置为仅与我们的本地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);
}

与其他实现SecureProtocolSocketFactory的方法一起。LocalSSLTrustManager是前面提到的虚拟信任管理器实现。


8
如果禁用所有信任验证,则首先使用SSL / TLS毫无意义。可以在本地进行测试,但如果要在外部进行连接,则不能。
布鲁诺

在Java 7上运行此异常时,会出现此异常。javax.net.ssl.SSLHandshakeException:没有通用的密码套件您能帮忙吗?
Uri Lukach 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.