在Java客户端中接受服务器的自签名ssl证书


215

它看起来像一个标准问题,但是我在任何地方都找不到清晰的方向。

我有Java代码试图连接到可能带有自签名(或过期)证书的服务器。该代码报告以下错误:

[HttpMethodDirector] I/O exception (javax.net.ssl.SSLHandshakeException) caught 
when processing request: sun.security.validator.ValidatorException: PKIX path 
building failed: sun.security.provider.certpath.SunCertPathBuilderException: 
unable to find valid certification path to requested target

据我了解,我必须使用keytool并告诉java允许这种连接是可以的。

解决此问题的所有说明均假定我完全精通keytool,例如

生成服务器的私钥并将其导入密钥库

是否有人可以发布详细说明?

我正在运行Unix,所以bash脚本是最好的。

不知道它是否重要,但是代码在jboss中执行。


2
请参阅如何使用Java HttpsURLConnection接受自签名证书?。显然,最好让站点使用有效的证书。
马修·弗拉申

2
感谢您提供的链接,搜索时没有看到它。但是那里的两个解决方案都包含发送请求的特殊代码,而我使用的是现有代码(适用于Java的amazon ws客户端)。分别是我正在连接的他们的站点,我无法解决其证书问题。
Nikita Rybak 2010年

2
@MatthewFlaschen- “显然,如果让站点使用有效的证书,那就更好了……” -如果客户信任,自签名证书就是有效的证书。许多人认为将信任授予CA /浏览器卡特尔是安全缺陷。
jww

3
相关信息,请参阅 世界上最危险的代码:在非浏览器软件中验证SSL证书。(提供此链接是因为您似乎收到了那些禁用验证的垃圾邮件答案)。
jww

Answers:


306

基本上,这里有两个选择:将自签名证书添加到JVM信任库中,或将客户端配置为

选项1

从浏览器导出证书并将其导入到JVM信任库中(以建立信任链):

<JAVA_HOME>\bin\keytool -import -v -trustcacerts
-alias server-alias -file server.cer
-keystore cacerts.jks -keypass changeit
-storepass changeit 

选项2

禁用证书验证:

// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[] { 
    new X509TrustManager() {     
        public java.security.cert.X509Certificate[] getAcceptedIssuers() { 
            return new X509Certificate[0];
        } 
        public void checkClientTrusted( 
            java.security.cert.X509Certificate[] certs, String authType) {
            } 
        public void checkServerTrusted( 
            java.security.cert.X509Certificate[] certs, String authType) {
        }
    } 
}; 

// Install the all-trusting trust manager
try {
    SSLContext sc = SSLContext.getInstance("SSL"); 
    sc.init(null, trustAllCerts, new java.security.SecureRandom()); 
    HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
} catch (GeneralSecurityException e) {
} 
// Now you can access an https URL without having the certificate in the truststore
try { 
    URL url = new URL("https://hostname/index.html"); 
} catch (MalformedURLException e) {
} 

请注意,我完全不建议使用选项2。禁用信任管理器会破坏SSL的某些部分,并使您容易受到中间攻击的攻击。选择选项1,或者甚至更好,让服务器使用由知名CA签名的“真实”证书。


7
不只是MIM攻击。它使您容易连接到错误的站点。这是完全不安全的。请参阅RFC2246。我始终反对发布此TrustManager。它自己的规范甚至都不正确。
罗恩侯爵

10
@EJP我真的不建议第二种选择(我已经更新了答案以明确说明)。但是,不发布它并不能解决任何问题(这是公共信息),恕我直言,不应该被否决。
Pascal Thivent,2010年

135
@EJP是通过教人来教育他们,而不是隐藏事物。因此,将事物保密或模糊不清根本不是解决方案。这段代码是公共的,Java API是公共的,谈论它比忽略它更好。但是我可以同意你的看法。
Pascal Thivent 2010年

10
另一个选择(您没有提及)是通过自己修复或致电相关支持人员来修复服务器的证书。单主机证书确实非常便宜。用自签名的东西乱搞是一分钱地愚蠢的(即,对于那些不熟悉英语习语的人来说,这是一套完全愚蠢的优先级,几乎不花钱就花了很多钱)。
Donal Fellows

2
@Rich,晚了6年,但您也可以通过单击锁定图标->更多信息->查看证书->详细信息选项卡->导出...,在firefox中获得服务器证书。它隐藏得很好。
Siddhartha

9

我将此问题归结为一个证书提供者,该证书提供者不为的默认JVM受信任主机的一部分JDK 8u74。提供者是www.identrust.com,但这不是我尝试连接的域。该域已从该提供者那里获得了证书。请参阅JDK / JRE中的默认列表是否可以跨交叉根目录覆盖信任?-阅读一些条目。另请参见哪些浏览器和操作系统支持“让我们加密”

因此,为了连接到我感兴趣的域,该域具有我发出的证书,请identrust.com执行以下步骤。基本上,我必须获得identrust.com(DST Root CA X3)证书才能被JVM信任。我能够使用Apache HttpComponents 4.5做到这一点,如下所示:

1:从证书链下载说明中的 indettrust获取证书。单击DST根CA X3链接。

2:将字符串保存到名为“ DST Root CA X3.pem”的文件中。确保在文件的开头和结尾添加“ ----- BEGIN CERTIFICATE -----”和“ ----- END CERTIFICATE -----”行。

3:使用以下命令创建一个Java密钥库文件cacerts.jks:

keytool -import -v -trustcacerts -alias IdenTrust -keypass yourpassword -file dst_root_ca_x3.pem -keystore cacerts.jks -storepass yourpassword

4:将生成的cacerts.jks密钥库复制到Java /(Maven)应用程序的resources目录中。

5:使用以下代码加载该文件并将其附加到Apache 4.5 HttpClient。这将解决所有具有从indetrust.comutil oracle 颁发的证书并将证书包含在JRE默认密钥库中的域的问题。

SSLContext sslcontext = SSLContexts.custom()
        .loadTrustMaterial(new File(CalRestClient.class.getResource("/cacerts.jks").getFile()), "yourpasword".toCharArray(),
                new TrustSelfSignedStrategy())
        .build();
// Allow TLSv1 protocol only
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
        sslcontext,
        new String[] { "TLSv1" },
        null,
        SSLConnectionSocketFactory.getDefaultHostnameVerifier());
CloseableHttpClient httpclient = HttpClients.custom()
        .setSSLSocketFactory(sslsf)
        .build();

当项目建立时,cacerts.jks将被复制到类路径并从那里加载。目前,我尚未针对其他ssl网站进行测试,但是如果此证书中的上述代码“链”也可以工作,但是我仍然不知道。

参考:自定义SSL上下文,以及如何使用Java HttpsURLConnection接受自签名证书?


问题是关于自签名证书。这个答案不是。
罗恩侯爵,

4
仔细阅读问题(和答案)。
K.Nicholas '16

9

Apache HttpClient 4.5支持接受自签名证书:

SSLContext sslContext = SSLContexts.custom()
    .loadTrustMaterial(new TrustSelfSignedStrategy())
    .build();
SSLConnectionSocketFactory socketFactory =
    new SSLConnectionSocketFactory(sslContext);
Registry<ConnectionSocketFactory> reg =
    RegistryBuilder.<ConnectionSocketFactory>create()
    .register("https", socketFactory)
    .build();
HttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(reg);        
CloseableHttpClient httpClient = HttpClients.custom()
    .setConnectionManager(cm)
    .build();
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse sslResponse = httpClient.execute(httpGet);

这将建立一个SSL套接字工厂,该工厂将使用TrustSelfSignedStrategy,向自定义连接管理器注册,然后使用该连接管理器进行HTTP GET。

我同意那些高喊“在生产中不要这样做”的人的观点,但是有一些用例可以在生产之外接受自签名证书。我们在自动集成测试中使用它们,因此即使未在生产硬件上运行,我们也正在使用SSL(例如在生产环境中)。


6

而不是设置默认的套接字工厂(这是IMO的坏事)-yhis只会影响当前连接,而不是您尝试打开的每个SSL连接:

URLConnection connection = url.openConnection();
    // JMD - this is a better way to do it that doesn't override the default SSL factory.
    if (connection instanceof HttpsURLConnection)
    {
        HttpsURLConnection conHttps = (HttpsURLConnection) connection;
        // Set up a Trust all manager
        TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager()
        {

            public java.security.cert.X509Certificate[] getAcceptedIssuers()
            {
                return null;
            }

            public void checkClientTrusted(
                java.security.cert.X509Certificate[] certs, String authType)
            {
            }

            public void checkServerTrusted(
                java.security.cert.X509Certificate[] certs, String authType)
            {
            }
        } };

        // Get a new SSL context
        SSLContext sc = SSLContext.getInstance("TLSv1.2");
        sc.init(null, trustAllCerts, new java.security.SecureRandom());
        // Set our connection to use this SSL context, with the "Trust all" manager in place.
        conHttps.setSSLSocketFactory(sc.getSocketFactory());
        // Also force it to trust all hosts
        HostnameVerifier allHostsValid = new HostnameVerifier() {
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        };
        // and set the hostname verifier.
        conHttps.setHostnameVerifier(allHostsValid);
    }
InputStream stream = connection.getInputStream();

2
checkServerTrusted没有实现实际信任证书确保拒绝不信任证书的必要逻辑。这可能会读得很好:世界上最危险的代码:在非浏览器软件中验证SSL证书
jww

1
您的getAcceptedIssuers()方法不符合规范,此“解决方案”仍然完全不安全。
洛恩侯爵,

3

有一个更好的选择来信任所有证书:创建一个TrustStore专门信任给定证书的证书,并使用此证书创建一个SSLContext,以从SSLSocketFactory上设置证书HttpsURLConnection。这是完整的代码:

File crtFile = new File("server.crt");
Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(new FileInputStream(crtFile));

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("server", certificate);

TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);

HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
connection.setSSLSocketFactory(sslContext.getSocketFactory());

您也可以KeyStore直接从文件中加载或从任何受信任的源中检索X.509证书。

请注意,使用此代码,cacerts将不会使用中的证书。该特定者HttpsURLConnection将仅信任此特定证书。


1

如果“他们”使用的是自签名证书,则由他们决定采取使服务器可用的步骤。具体来说,这意味着以可信赖的方式将其证书离线提供给您。因此,请他们去做。然后,按照《 JSSE参考指南》中所述,使用keytool将其导入到信任库中。甚至不要考虑此处发布的不安全的TrustManager。

编辑为了使17个(!)投票者和下面的许多评论者受益,这些评论者显然没有真正阅读我在这里写的内容,这并不是对自签名证书的嘲讽。正确实施后 ,自签名证书没有任何问题但是,实现它们的正确方法是通过脱机过程安全地传送证书而不是通过未经身份验证的通道将它们用于身份验证。当然这很明显吗?对于我曾经工作过的每个具有安全意识的组织来说,从拥有数千家分行的银行到我自己的公司,无疑都是显而易见的。客户端代码库信任的“解决方案”所有人证书,包括绝对由任何人签名的自签名证书,或将自己设置为CA的任何专职机构,实际上都是不安全的。它只是出于安全考虑。这是没有意义的。您正在与某人进行私人,防篡改,防回复,防注入的对话。任何人 一个男人在中间。模仿者。任何人 您也可以只使用纯文本。


2
仅仅因为某些服务器决定使用https,并不意味着客户端的用户会出于自己的目的而放弃安全性。
古斯

3
我很惊讶这个答案被否决了。我想了解更多原因。EJP似乎建议您不要执行选项2,因为它会带来重大的安全漏洞。有人可以解释为什么(部署前设置除外)为什么这个答案的质量很低吗?
cr1pto

2
好吧,我猜全部。这是什么“它是由他们采取必要的步骤,使他们的服务器使用”是什么意思?服务器操作员必须做什么才能使其可用?您打算采取什么步骤?你能提供一份清单吗?但是退回到1,000英尺,这与OP提出的问题又有什么关系呢?他想知道如何使客户端接受Java中的自签名证书。这是一个简单的问题,因为OP认为证书可以接受,因此可以授予对代码的信任。
jww

2
@EJP-如果我错了,请纠正我,但是接受自签名证书是客户端的政策决定。它与服务器端操作无关。客户必须做出决定。奇偶校验必须一起解决密钥分发问题,但这与使服务器可用无关。
jww

3
@EJP-我当然不是说“信任所有证书”。也许我遗漏了一些东西(或者您对事物的了解太多了)... OP拥有证书,并且被他接受。他想知道如何信任它。我可能正在分裂头发,但是该证书不需要离线交付。应该使用带外验证,但是这与“必须离线交付给客户端”不同。他甚至可能是生产服务器和客户端的商店,因此他具有必要的先验知识。
jww

1

信任所有SSL证书:-如果要在测试服务器上进行测试,则可以绕过SSL。但是,请勿将此代码用于生产。

public static class NukeSSLCerts {
protected static final String TAG = "NukeSSLCerts";

public static void nuke() {
    try {
        TrustManager[] trustAllCerts = new TrustManager[] { 
            new X509TrustManager() {
                public X509Certificate[] getAcceptedIssuers() {
                    X509Certificate[] myTrustedAnchors = new X509Certificate[0];  
                    return myTrustedAnchors;
                }

                @Override
                public void checkClientTrusted(X509Certificate[] certs, String authType) {}

                @Override
                public void checkServerTrusted(X509Certificate[] certs, String authType) {}
            }
        };

        SSLContext sc = SSLContext.getInstance("SSL");
        sc.init(null, trustAllCerts, new SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
        HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String arg0, SSLSession arg1) {
                return true;
            }
        });
    } catch (Exception e) { 
    }
}

}

请在Activity或应用程序类的onCreate()函数中调用此函数。

NukeSSLCerts.nuke();

这可以用于Android中的Volley。


除非代码不起作用,否则不能完全确定为什么您会被否决。也许是这样的假设,即当原始问题没有标记Android时,就会以某种方式涉及Android。
罗丹(Lo-Tan)

1

可接受的答案很好,但是我想在Mac上使用IntelliJ来添加一些内容,但是不能使用JAVA_HOMEpath变量使它正常工作。

事实证明,从IntelliJ运行应用程序时,Java Home有所不同。

要弄清楚它在哪里,您可以这样做,System.getProperty("java.home")因为它是从中读取受信任证书的地方。



0

您可以使用RHEL 6的较新版本开始的update-ca-trust,而不是使用RHEL顶部注释中所建议的keytool。您需要使用pem格式的证书。然后

trust anchor <cert.pem>

编辑/etc/pki/ca-trust/source/cert.p11-kit并将“证书类别:其他条目”更改为“证书类别:权限”。(或使用sed在脚本中执行此操作。)然后执行

update-ca-trust

几个警告:

  • 我在RHEL 6服务器上找不到“信任”,并且yum也没有提供安装它。我最终在RHEL 7服务器上使用了它,并复制了.p11-kit文件。
  • 要为您完成这项工作,您可能需要做update-ca-trust enable。这将用指向/ etc / pki / ca-trust / extracted / java / cacerts的符号链接替换/ etc / pki / java / cacerts。(因此,您可能要先备份前者。)
  • 如果您的Java客户端使用存储在其他位置的cacerts,则需要将其手动替换为/ etc / pki / ca-trust / extracted / java / cacerts的符号链接,或将其替换为该文件。

0

我遇到的问题是我将URL传递到库中,该库称url.openConnection();我改编了jon-daniel的答案,

public class TrustHostUrlStreamHandler extends URLStreamHandler {

    private static final Logger LOG = LoggerFactory.getLogger(TrustHostUrlStreamHandler.class);

    @Override
    protected URLConnection openConnection(final URL url) throws IOException {

        final URLConnection urlConnection = new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getFile()).openConnection();

        // adapated from
        // /programming/2893819/accept-servers-self-signed-ssl-certificate-in-java-client
        if (urlConnection instanceof HttpsURLConnection) {
            final HttpsURLConnection conHttps = (HttpsURLConnection) urlConnection;

            try {
                // Set up a Trust all manager
                final TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {

                    @Override
                    public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                        return null;
                    }

                    @Override
                    public void checkClientTrusted(final java.security.cert.X509Certificate[] certs, final String authType) {
                    }

                    @Override
                    public void checkServerTrusted(final java.security.cert.X509Certificate[] certs, final String authType) {
                    }
                } };

                // Get a new SSL context
                final SSLContext sc = SSLContext.getInstance("TLSv1.2");
                sc.init(null, trustAllCerts, new java.security.SecureRandom());
                // Set our connection to use this SSL context, with the "Trust all" manager in place.
                conHttps.setSSLSocketFactory(sc.getSocketFactory());
                // Also force it to trust all hosts
                final HostnameVerifier allHostsValid = new HostnameVerifier() {
                    @Override
                    public boolean verify(final String hostname, final SSLSession session) {
                        return true;
                    }
                };

                // and set the hostname verifier.
                conHttps.setHostnameVerifier(allHostsValid);

            } catch (final NoSuchAlgorithmException e) {
                LOG.warn("Failed to override URLConnection.", e);
            } catch (final KeyManagementException e) {
                LOG.warn("Failed to override URLConnection.", e);
            }

        } else {
            LOG.warn("Failed to override URLConnection. Incorrect type: {}", urlConnection.getClass().getName());
        }

        return urlConnection;
    }

}

使用此类,可以使用以下方法创建新的URL:

trustedUrl = new URL(new URL(originalUrl), "", new TrustHostUrlStreamHandler());
trustedUrl.openConnection();

这样的好处是它是本地化的,而不是替换default URL.openConnection

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.