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


147

我要添加到我们的大型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应用程序接受自签名证书的首选,标准或最佳方式是什么?我能否完成上述所有目标?或者我将不得不妥协?是否有涉及文件和目录和配置设置的选项,以及小到无代码?

  0

小,工作修复:http://www.rgagnon.com/javadetails/java-fix-certificate-problem-in-HTTPS。html 16 8月. 112011-08-16 14:06:49

+16

@Hasenpriester:请不要建议此页。它禁用所有信任验证。你不仅要接受你想要的自签名证书,还要接受任何MITM攻击者会向你提供的证书。 23 12月. 112011-12-23 13:23:14

150

自己创建一个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 不会适用于“密钥条目”,但适用于“可信条目”)。

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

非常感谢!其他人帮助我指出了正确的方向,但最终你的方法是我采取的。我有一个艰巨的任务,将PEM证书文件转换为JKS Java密钥库文件,我在这里找到了帮助:http://stackoverflow.com/questions/722931/ssl-socket-php-code-needs-to-被转换为java 13 5月. 092009-05-13 21:37:13

+1

我很高兴它解决了。对不起,你在关键商店挣扎;我应该把它列入我的答案。使用CertificateFactory并不困难。事实上,我想我会为以后的任何人做更新。 13 5月. 092009-05-13 21:40:51

  0

哇。这意味着将PEM转换为JKS比我所做的要容易得多!谢谢! :) 14 5月. 092009-05-14 19:19:00

+1

所有这些代码都是通过设置JSSE参考指南中描述的三个系统属性来复制您可以完成的。 31 5月. 122012-05-31 19:36:53

+1

@EJP:这是前一阵子,所以我不太确定,但我猜测其理由是在“大型Java应用程序”中,可能会建立其他HTTP连接。设置全局属性可能会干扰工作连接,或者允许这一方欺骗服务器。这是根据具体情况使用内置信任管理器的一个示例。 31 5月. 122012-05-31 22:26:58

+2

正如OP所说,“我的代码需要教Java接受这一个自签名证书,对于应用程序中的这一点,以及其他任何地方。” 31 5月. 122012-05-31 22:29:09

  0

我试图实现你的解决方案无济于事请帮助我http://stackoverflow.com/questions/20190364/how-to-bypass-certificateexception-by-java 25 11月. 132013-11-25 10:59:37

  0

如果你想它被接受“全球”使用keytool将证书安装为cacerts文件中的可信证书。然后,对于使用该JRE的任何VM启动,它都将被信任。 06 12月. 132013-12-06 19:59:08

  0

@KyleM这不是全球接受的好方法。 06 12月. 132013-12-06 20:03:33

  0

这就是为什么我把“全局”放在引号中并解释它是如何工作的。其他人都没有提到这样做,尽管这是一种非常普遍和可接受的方法。 06 12月. 132013-12-06 20:07:48

  0

许多大型组织不接受这种方法。这将是一个安全违规行为,并带来严重后果。 06 12月. 132013-12-06 21:09:37

  0

@erickson嗨 - 四年后,你教给我们的方法一直很好,并且已经用于我们通信的几台不同的服务器。现在我有了一个新的技巧,我需要弄清楚(可能会为此创建一个新的StackOverflow问题):我想同时使用服务器的自签名证书来信任默认密钥库和自定义密钥库。我们遇到了服务器升级为真实证书并且连接断开的情况,但如果我们信任默认的密钥库,那就没问题了。 13 2月. 142014-02-13 19:25:02

  0

@erickson该API有点令人误解:它意味着您可以将一个TrustManagers数组传递给SSLContext.init(),但该方法上的javadoc指示仅使用数组中的第一个TrustManager。我很乐意听到您可以分享的任何见解。 13 2月. 142014-02-13 19:26:18

  0

@skiphoppy我将不得不尝试一下,回到你身边。我以前没有做过,但我有一个想法可能会起作用。 13 2月. 142014-02-13 20:03:55

+1

@skiphoppy嗯,我发现的唯一方法是创建一个临时的'KeyStore'(在内存中),并从所有“真实”密钥库中添加root证书,或者(几乎等同)创建一个'Set <TrustAnchor>'从你信任的密钥存储区中的所有条目开始,并用它来初始化'PKIXParameters',并用它反过来''TrustManagerFactory'。 13 2月. 142014-02-13 21:48:48

  0

谢谢@erickson我还在玩它,并漫游在漫长的迷宫文件。我会给这个镜头。 13 2月. 142014-02-13 22:03:09


12

我们复制JRE的信任库并将我们的自定义证书添加到该信任库,然后告诉应用程序使用具有系统属性的自定义信任库。通过这种方式,我们只保留默认的JRE信任库。

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

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

该解决方案不是100%优雅或者简单,但它很简单,可行,并且不需要代码。


12

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

这涉及到我们自己的SSLSocketFactory从我们的本地SSLContext中创建SSL套接字,该SSLContext设置为只有我们的本地TrustManager与它关联。您根本不需要靠近密钥库/ 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是上述虚拟信任管理器的实现。

+7

如果您禁用所有信任验证,则首先使用SSL/TLS几乎没有意义。可以在本地进行测试,但如果您想要在外部进行连接,则无法进行测试。 23 12月. 112011-12-23 13:26:31

  0

在Java 7上运行时出现此异常。javax.net.ssl.SSLHandshakeException:没有密码套件共用 您能协助吗? 19 12月. 132013-12-19 12:55:10


14

如果创建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

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

+1

我不认为这解决了原来的问题,但它解决了*我的问题,所以谢谢! 15 5月. 132013-05-15 15:10:40


11

我经历的很多地方去了SO和网络来解决这件事情。这是为我工作的代码:

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

我已经测试过,你可以把任何字符的字符串证书,如果它是自签名的,只要你保持上面的确切结构。我用笔记本电脑的终端命令行获得了证书字符串。