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

2022-08-31 06:57:54

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


答案 1

自己创建一个工厂,并在连接之前将其设置为。SSLSocketHttpsURLConnection

...
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 格式证书创建密钥库,您可以使用 编写自己的代码,或者直接从 JDK 导入它(keytool 不适用于“密钥条目”,但对于“可信条目”来说就好了)。CertificateFactorykeytool

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

答案 2

我在网上阅读了很多地方来解决这个问题。这是我为使其工作而编写的代码:

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

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