java 如何检索不受信任的 SSL 服务器证书以查看和信任它?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/10577029/
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-10-31 01:38:26  来源:igfitidea点击:

How do I retrieve an untrusted SSL server certificate in order to review and trust it?

javasslssl-certificate

提问by QuantumMechanic

My problem:

我的问题:

I want to connect to servers (not limited to the HTTPS protocol -- could be LDAP-over-SSL, could be SMTPS, could be IMAPS, etc.) that may be using certificates that Java will not trust by default (because they are self-signed).

我想连接到可能使用 Java 默认不信任的证书(因为它们是自签名)。

The desired workflow is to attempt the connection, retrieve the certificate info, present that to the user and if he accepts it, add it to to the truststore so it'll be trusted going forward.

所需的工作流程是尝试连接,检索证书信息,将其呈现给用户,如果他接受,则将其添加到信任库中,以便在以后信任它。

I am stuck at retrieving the certificate. I have code (see at the end of the post) that I've cribbed from here and from sites pointed to by answers to questions about java SSL. The code simply creates an SSLSocket, starts the SSL handshake, and asks the SSL session for the Certificate[]. The code works fine when I'm connecting to a server using an already-trustable certificate. But when I connect to a server using a self-signed cert I get the usual:

我一直在检索证书。我有一些代码(见文章末尾),我从这里和从有关 java SSL 的问题的答案所指向的站点中抄录下来。该代码只是创建一个SSLSocket,开始 SSL 握手,并向 SSL 会话询问Certificate[]. 当我使用已经可信任的证书连接到服务器时,代码工作正常。但是当我使用自签名证书连接到服务器时,我得到了通常的结果:

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
        at sun.security.ssl.Alerts.getSSLException(Unknown Source)
        at sun.security.ssl.SSLSocketImpl.fatal(Unknown Source)
        at sun.security.ssl.Handshaker.fatalSE(Unknown Source)
        at sun.security.ssl.Handshaker.fatalSE(Unknown Source)
        at sun.security.ssl.ClientHandshaker.serverCertificate(Unknown Source)
        [etc]

If I run with -Djavax.net.debug=allI see that the JVM does retrieve the self-signed cert but will blow up the connection for using an untrusted cert before getting to the point where it'll return the certificates.

如果我与我一起运行,-Djavax.net.debug=all我会看到 JVM 确实检索了自签名证书,但会在到达将返回证书的点之前破坏使用不受信任的证书的连接。

Seems like a chicken-and-egg problem. It will not let me see the certificates because they are not trusted. But I need to see the certificates to be able to add them to the truststore so they will be trusted. How do you break out of this?

好像是先有鸡还是先有蛋的问题。它不会让我看到证书,因为它们不受信任。但是我需要查看证书才能将它们添加到信任库中,以便它们受到信任。你如何摆脱这种局面?

For example, if I run the program as:

例如,如果我将程序运行为:

java SSLTest www.google.com 443

I get a printout of the certs Google is using. But if I run it as

我得到了 Google 正在使用的证书的打印件。但如果我运行它

java SSLTest my.imap.server 993

I get the exception referenced above.

我得到了上面提到的异常。

The code:

代码:

import java.io.InputStream;
import java.io.OutputStream;
import java.security.cert.*;
import javax.net.SocketFactory;
import javax.net.ssl.*;

public class SSLTest
{
    public static void main(String[] args) throws Exception {
        if (args.length != 2) {
            System.err.println("Usage: SSLTest host port");
            return;
        }

        String host = args[0];
        int port = Integer.parseInt(args[1]);

        SocketFactory factory = SSLSocketFactory.getDefault();
        SSLSocket socket = (SSLSocket) factory.createSocket(host, port);

        socket.startHandshake();

        Certificate[] certs = socket.getSession().getPeerCertificates();

        System.out.println("Certs retrieved: " + certs.length);
        for (Certificate cert : certs) {
            System.out.println("Certificate is: " + cert);
            if(cert instanceof X509Certificate) {
                try {
                    ( (X509Certificate) cert).checkValidity();
                    System.out.println("Certificate is active for current date");
                } catch(CertificateExpiredException cee) {
                    System.out.println("Certificate is expired");
                }
            }
        }
    }
}

回答by xonya

You can do this implementing a temporary TrustManagerthat accepts all certificates and a temporary HostnameVerifierthat verifies all names (obviously you have to use them only to retrieve the certificate and not to send private data).

您可以实现一个TrustManager接受所有证书的临时文件和一个HostnameVerifier验证所有名称的临时文件(显然您必须使用它们来检索证书而不是发送私人数据)。

The following code retrieve the certificates from an arbitrary https url and save them to a file:

以下代码从任意 https url 检索证书并将它们保存到文件中:

URL url = new URL("https://<yoururl>");

SSLContext sslCtx = SSLContext.getInstance("TLS");
sslCtx.init(null, new TrustManager[]{ new X509TrustManager() {

    private X509Certificate[] accepted;

    @Override
    public void checkClientTrusted(X509Certificate[] xcs, String string) throws CertificateException {
    }

    @Override
    public void checkServerTrusted(X509Certificate[] xcs, String string) throws CertificateException {
        accepted = xcs;
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return accepted;
    }
}}, null);

HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();

connection.setHostnameVerifier(new HostnameVerifier() {

    @Override
    public boolean verify(String string, SSLSession ssls) {
        return true;
    }
});

connection.setSSLSocketFactory(sslCtx.getSocketFactory());

if (connection.getResponseCode() == 200) {
    Certificate[] certificates = connection.getServerCertificates();
    for (int i = 0; i < certificates.length; i++) {
        Certificate certificate = certificates[i];
        File file = new File("/tmp/newcert_" + i + ".crt");
        byte[] buf = certificate.getEncoded();

        FileOutputStream os = new FileOutputStream(file);
        os.write(buf);
        os.close();
    }
}

connection.disconnect();