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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/859111/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 20:18:07  来源:igfitidea点击:

How can I use different certificates on specific connections?

javasslkeystoretruststorejsse

提问by skiphoppy

A module I'm adding to our large Java application has to converse with another company's SSL-secured website. The problem is that the site uses a self-signed certificate. I have a copy of the certificate to verify that I'm not encountering a man-in-the-middle attack, and I need to incorporate this certificate into our code in such a way that the connection to the server will be successful.

我要添加到我们的大型 Java 应用程序的模块必须与另一家公司的 SSL 安全网站进行通信。问题是该站点使用自签名证书。我有一书副本来验证我没有遇到中间人攻击,我需要将此证书合并到我们的代码中,以便与服务器的连接成功。

Here's the basic code:

这是基本代码:

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

Without any additional handling in place for the self-signed certificate, this dies at conn.getOutputStream() with the following exception:

如果没有对自签名证书进行任何额外的处理,它会在 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

Ideally, my code needs to teach Java to accept this one self-signed certificate, for this one spot in the application, and nowhere else.

理想情况下,我的代码需要教 Java 接受这个自签名证书,用于应用程序中的这一点,而不是其他地方。

I know that I can import the certificate into the JRE's certificate authority store, and that will allow Java to accept it. That's not an approach I want to take if I can help; it seems very invasive to do on all of our customer's machines for one module they may not use; it would affect all other Java applications using the same JRE, and I don't like that even though the odds of any other Java application ever accessing this site are nil. It's also not a trivial operation: on UNIX I have to obtain access rights to modify the JRE in this way.

我知道我可以将证书导入 JRE 的证书颁发机构存储,这将允许 Java 接受它。如果我能提供帮助,这不是我想采取的方法;在我们客户的所有机器上为他们可能不使用的一个模块做这件事似乎非常具有侵入性;它会影响使用相同 JRE 的所有其他 Java 应用程序,即使任何其他 Java 应用程序访问此站点的几率为零,我也不喜欢这种情况。这也不是一个简单的操作:在 UNIX 上,我必须获得访问权限才能以这种方式修改 JRE。

I've also seen that I can create a TrustManager instance that does some custom checking. It looks like I might even be able to create a TrustManager that delegates to the real TrustManager in all instances except this one certificate. But it looks like that TrustManager gets installed globally, and I presume would affect all other connections from our application, and that doesn't smell quite right to me, either.

我还看到我可以创建一个执行一些自定义检查的 TrustManager 实例。看起来我什至可以创建一个 TrustManager,在除此证书之外的所有实例中委托给真正的 TrustManager。但看起来 TrustManager 是全局安装的,我认为会影响我们应用程序的所有其他连接,而且这对我来说也不太合适。

What is the preferred, standard, or best way to set up a Java application to accept a self-signed certificate? Can I accomplish all of the goals I have in mind above, or am I going to have to compromise? Is there an option involving files and directories and configuration settings, and little-to-no code?

设置 Java 应用程序以接受自签名证书的首选、标准或最佳方法是什么?我可以完成上面想到的所有目标,还是必须妥协?是否有涉及文件和目录和配置设置以及几乎没有代码的选项?

采纳答案by erickson

Create an SSLSocketfactory yourself, and set it on the HttpsURLConnectionbefore connecting.

SSLSocket自己创建一个工厂,并HttpsURLConnection在连接之前设置它。

...
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslFactory);
conn.setMethod("POST");
...

You'll want to create one SSLSocketFactoryand keep it around. Here's a sketch of how to initialize it:

您会想要创建一个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();

If you need help creating the key store, please comment.

如果您需要帮助创建密钥库,请发表评论。



Here's an example of loading the key store:

这是加载密钥库的示例:

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(trustStore, trustStorePassword);
trustStore.close();

To create the key store with a PEM format certificate, you can write your own code using CertificateFactory, or just import it with keytoolfrom the JDK (keytool won'twork for a "key entry", but is just fine for a "trusted entry").

要使用 PEM 格式证书创建密钥库,您可以使用 编写自己的代码CertificateFactory,或者直接keytool从 JDK 中导入它(keytool不适用于“密钥条目”,但对于“可信条目”就可以了) )。

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

回答by u7867

We copy the JRE's truststore and add our custom certificates to that truststore, then tell the application to use the custom truststore with a system property. This way we leave the default JRE truststore alone.

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

The downside is that when you update the JRE you don't get its new truststore automatically merged with your custom one.

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

You could maybe handle this scenario by having an installer or startup routine that verifies the truststore/jdk and checks for a mismatch or automatically updates the truststore. I don't know what happens if you update the truststore while the application is running.

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

This solution isn't 100% elegant or foolproof but it's simple, works, and requires no code.

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

回答by araqnid

I've had to do something like this when using commons-httpclient to access an internal https server with a self-signed certificate. Yes, our solution was to create a custom TrustManager that simply passed everything (logging a debug message).

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

This comes down to having our own SSLSocketFactory that creates SSL sockets from our local SSLContext, which is set up to have only our local TrustManager associated with it. You don't need to go near a keystore/certstore at all.

这归结为我们自己的 SSLSocketFactory 从我们的本地 SSLContext 创建 SSL 套接字,它被设置为只有我们的本地 TrustManager 与之关联。您根本不需要靠近密钥库/证书库。

So this is in our LocalSSLSocketFactory:

所以这是在我们的 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);
}

Along with other methods implementing SecureProtocolSocketFactory. LocalSSLTrustManager is the aforementioned dummy trust manager implementation.

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

回答by user454322

If creating a SSLSocketFactoryis not an option, just import the key into the JVM

如果创建 aSSLSocketFactory不是一个选项,只需将密钥导入 JVM

  1. Retrieve the public key: $openssl s_client -connect dev-server:443, then create a file dev-server.pemthat looks like

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

  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

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

来源:如何解决 javax.net.ssl.SSLHandshakeException?

回答by Josh

I read through LOTS of places online to solve this thing. This is the code I wrote to make it work:

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

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 is a String that contains the Certificate, for example:

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

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

I have tested that you can put any characters in the certificate string, if it is self signed, as long as you keep the exact structure above. I obtained the certificate string with my laptop's Terminal command line.

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