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

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

196269 次浏览

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

缺点是,当您更新 JRE 时,您不会得到它的新信任存储自动与您的自定义信任存储合并。

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

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

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

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

在使用 commons-httpclient 访问具有自签名证书的内部 https 服务器时,我必须执行类似的操作。是的,我们的解决方案是创建一个自定义 TrustManager,它简单地传递所有内容(记录调试消息)。

这可以归结为拥有自己的 SSLSocketFactory,它从本地 SSLContext 创建 SSL 套接字,该套接字被设置为只与本地 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 是前面提到的虚拟信任管理器实现。

如果不能创建 SSLSocketFactory,只需将密钥导入到 JVM 中

  1. 检索公钥: 然后创建一个类似于

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

Source: How to solve javax.net.ssl.SSLHandshakeException?

为了解决这个问题,我在网上浏览了很多地方。 这是我写的代码,让它工作:

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

证书字符串是一个包含证书的字符串,例如:

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

我已经测试过,如果证书字符串是自签名的,只要保持上面的精确结构,就可以在证书字符串中放入任何字符。我通过笔记本电脑的 Terminal 命令行获得了证书字符串。