170

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

4

5 回答 5

172

自己创建一个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
于 2009-05-13T17:22:48.710 回答
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-----";

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

于 2015-12-30T16:57:41.990 回答
14

如果创建 aSSLSocketFactory不是一个选项,只需将密钥导入 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?

于 2012-11-02T10:06:24.790 回答
12

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

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

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

该解决方案不是 100% 优雅或万无一失,但它简单、有效且不需要代码。

于 2009-05-13T17:17:23.697 回答
12

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

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

于 2009-05-13T17:25:22.593 回答