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

发布于 2024-10-03 04:44:26 字数 1780 浏览 1 评论 0原文

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

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.

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:

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.

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.

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.

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

够钟 2024-10-10 04:44:26

自己创建一个 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

Create an SSLSocket factory yourself, and set it on the HttpsURLConnection before connecting.

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

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

/* 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 keytool from the JDK (keytool won't work for a "key entry", but is just fine for a "trusted entry").

keytool -import -file selfsigned.pem -alias server -keystore server.jks
蓝咒 2024-10-10 04:44:26

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

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

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

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:

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.

请爱~陌生人 2024-10-10 04:44:26

如果无法创建 SSLSocketFactory,只需将密钥导入 JVM

  1. 检索公钥:
    $openssl s_client -connect dev-server:443,然后创建一个如下所示的文件 dev-server.pem

    <前><代码>-----开始证书-----
    lklkkkllklklklklllllkllklkl
    lklkkkllklklklklllllkllklkl
    咯咯咯咯咯....
    -----证书结束-----

  2. 导入密钥:#keytool -导入-alias dev-server -keystore $JAVA_HOME/jre/lib/security/cacerts -file dev-server.pem
    密码:changeit

  3. 重新启动 JVM

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

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

  1. Retrieve the public key:
    $openssl s_client -connect dev-server:443, then create a file dev-server.pem that 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

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

静谧 2024-10-10 04:44:26

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

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

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

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

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.

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

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.

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

小傻瓜 2024-10-10 04:44:26

当使用 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 是前面提到的虚拟信任管理器实现。

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

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.

So this is in our 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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文