PKIX路径构建失败:无法找到请求目标的有效认证路径

48
我正在使用以下客户端调用一些HTTPS网络服务:

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.HttpURLConnection;
import java.net.URL;

import javax.net.ssl.HttpsURLConnection;

/**
 * Handles http and https connections. It sends XML request over http (or https)
 * to SOAP web service and receive the XML reply.
 * 
 * @author mhewedy
 * @date 30.10.2010
 */
public class HttpWSXmlClient
{
    private final String ws_url;
    private byte[] requestData;

    public HttpWSXmlClient(String wsUrl)
    {
        this.ws_url = wsUrl;
    }

    public void readRequest(String xmlRequestFilePath)
    {
        try
        {
            InputStream istream = new FileInputStream(xmlRequestFilePath);
            byte[] data = stream2Bytes(istream);
            istream.close();
            this.requestData = data;
        } catch (Exception e)
        {
            throw new RuntimeException(e.getMessage());
        }
    }

    /**
     * 
     * @param ps
     *            PrintStream object to send the debugging info to.
     * @return
     * @throws IOException
     */
    public byte[] sendAndRecieve(PrintStream ps) throws IOException
    {
        if (requestData == null)
            throw new RuntimeException(
                    "the request data didn't initialized yet.");
        if (ps != null)
            ps.println("Request:\n" + new String(requestData));
        URL url = new URL(ws_url);
        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
        // or HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setDoOutput(true);
        connection.setRequestProperty("content-type", "text/xml");
        connection.connect();
        OutputStream os = connection.getOutputStream();
        os.write(requestData);
        InputStream is = connection.getInputStream();
        byte[] rply = stream2Bytes(is);
        if (ps != null)
            ps.println("Response:\n" + new String(rply));
        os.close();
        is.close();
        connection.disconnect();
        return rply;
    }

    public byte[] sendAndRecieve() throws IOException
    {
        return sendAndRecieve(null);
    }

    private byte[] stream2Bytes(InputStream istream) throws IOException
    {
        ByteArrayOutputStream outstream = new ByteArrayOutputStream();
        int c;
        while ((c = istream.read()) != -1)
        {
            if (c != 0x0A && c != 0x0D) // prevent new line character from being
            // written
            {
                if (c == 0x09)
                    c = 0x20; // prevent tab character from being written,
                // instead write single space char
                outstream.write(c);
            }
        }
        byte[] ret = outstream.toByteArray();
        outstream.close();
        return ret;
    }

}

测试:

public class Test
{
    private static final String WS_URL = "https://some_server/path/to/ws";

    public static void main(String[] args) throws Exception
    {
        HttpWSXmlClient client = new HttpWSXmlClient(WS_URL);
        client.readRequest("request.xml");
        client.sendAndRecieve(System.out);
    }
}

我得到了以下输出:
Exception in thread "Main Thread" 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 com.sun.net.ssl.internal.ssl.Alerts.getSSLException(Alerts.java:174)
    at com.sun.net.ssl.internal.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1591)
    at com.sun.net.ssl.internal.ssl.Handshaker.fatalSE(Handshaker.java:187)
    at com.sun.net.ssl.internal.ssl.Handshaker.fatalSE(Handshaker.java:181)
    at com.sun.net.ssl.internal.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1035)
    at com.sun.net.ssl.internal.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:124)
    at com.sun.net.ssl.internal.ssl.Handshaker.processLoop(Handshaker.java:516)
    at com.sun.net.ssl.internal.ssl.Handshaker.process_record(Handshaker.java:454)
    at com.sun.net.ssl.internal.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:884)
    at com.sun.net.ssl.internal.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1096)
    at com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1123)
    at com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1107)
    at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:415)
    at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:166)
    at sun.net.www.protocol.https.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:133)
    at com.se.swstest.HttpWSXmlClient.sendAndRecieve(HttpWSXmlClient.java:63)
    at com.se.swstest.Test.main(Test.java:11)
Caused by: 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.validator.PKIXValidator.doBuild(PKIXValidator.java:285)
    at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:191)
    at sun.security.validator.Validator.validate(Validator.java:218)
    at com.sun.net.ssl.internal.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:126)
    at com.sun.net.ssl.internal.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:209)
    at com.sun.net.ssl.internal.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:249)
    at com.sun.net.ssl.internal.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1014)
    ... 12 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:174)
    at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:238)
    at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:280)
    ... 18 more

我需要在jdk/jre/lib/security中放置证书吗?我有一个xxx_IE.crt和xxx_FX.crt(分别用于Firefox和IE),它们无法用于上述Java客户端,所以我是否需要为Java客户端获取特定的证书?
谢谢。

这可能会有所帮助:https://dev59.com/rmox5IYBdhLWcg3wcTzr - yegor256
13个回答

30

您需要设置证书才能访问此URL。使用以下代码来设置密钥库:

System.setProperty("javax.net.ssl.trustStore","clientTrustStore.key");

System.setProperty("javax.net.ssl.trustStorePassword","qwerty");

这对我实际起作用了,因为我将信任存储设置为我的 whatever.jks 文件,尽管我不确定原因。但还是谢谢你。 - EpicPandaForce
在这里运行得很好...虽然我甚至不知道trustStore是什么 :/ - aswzen
4
信任存储区不包含密钥,因此将其命名为.key是没有意义的。 - user207421
3
有人能告诉我 qwertyclientTrustStore.key 的真实值可能是什么吗? - ozgur
4
我必须指出,为整个JVM设置信任商店是一个非常糟糕的想法。这可能会劫持另一个组件期望的设置,并且根据您放入信任商店中的内容,可能会降低在该JVM内运行的任何代码的安全性。最好只为您打算进行的连接设置信任商店(及相关配置)。您正在使用HttpsURLConnection,可以在这些连接上轻松设置信任管理器。 - Christopher Schultz
@ozgur trustStore可能是“jssecacerts”或存储在服务器上的.jks文件。使用entpnerd答案生成的jssecacerts文件可以为我所用。 - golimar

24
Java 8解决方案:我曾遇到过这个问题,通过将远程站点的证书添加到我的Java密钥库中解决了它。我的解决方案基于myshittycode博客上的解决方案,该博客的解决方案又基于mykong博客的以前的解决方案。这些博客文章的解决方案是下载一个名为InstallCert的程序,它是一个Java类,你可以从命令行运行它来获取证书。然后你可以将证书安装在Java的密钥库中。 InstallCert Readme 对我非常有效。你只需要运行以下命令:
  1. javac InstallCert.java
  2. java InstallCert [host]:[port] (在运行命令时输入给定列表中证书的编号 - 很可能只是1)
  3. keytool -exportcert -alias [host]-1 -keystore jssecacerts -storepass changeit -file [host].cer
  4. sudo keytool -importcert -alias [host] -keystore [path to system keystore] -storepass changeit -file [host].cer
如果需要,可以参考引用的README文件中的示例。

5
请注意,系统的密钥库可能位于:$JAVA_HOME/jre/lib/security/cacerts - entpnerd
有没有办法将Java应用程序指向.cer文件,而不是修改系统的密钥库? - golimar
@golimar 我不这么认为(请参考这个Oracle页面和这个问题)。话虽如此,你的问题可能是一个值得StackOverflow社区关注的新问题。 - entpnerd
我认为我已经成功地结合了你的答案(生成jssecacerts文件)和@Vipin Kumar的答案(将应用程序指向该文件)。 - golimar
1
我使用提到的 InstallCerts.java,但是与其在应用程序部署的位置更新JDK密钥库,我将由 InstallCert.java 生成的密钥库(称为 jssecacerts 文件)与应用程序一起打包。只需将其放在类路径上即可。这样就可以避免需要更新JDK。 - Avec

10
如果您不需要SSL安全性,则可能希望将其关闭。
 /**
   * disable SSL
   */
  private void disableSslVerification() {
    try {
      // Create a trust manager that does not validate certificate chains
      TrustManager[] trustAllCerts = new TrustManager[] {
          new X509TrustManager() {
            public java.security.cert.X509Certificate[] getAcceptedIssuers() {
              return null;
            }

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

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

      // Install the all-trusting trust manager
      SSLContext sc = SSLContext.getInstance("SSL");
      sc.init(null, trustAllCerts, new java.security.SecureRandom());
      HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());

      // Create all-trusting host name verifier
      HostnameVerifier allHostsValid = new HostnameVerifier() {
        public boolean verify(String hostname, SSLSession session) {
          return true;
        }
      };

      // Install the all-trusting host verifier
      HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
    } catch (KeyManagementException e) {
      e.printStackTrace();
    }
  }

10
我遇到过这个问题几次,原因是证书链不完整。如果您使用标准的Java信任存储,它可能没有需要完成证书链的证书,这是连接到SSL站点并验证其证书所必需的。
我曾经遇到过一些DigiCert证书的问题,必须手动添加中间证书。

2
我也有DigiCert,我猜你描述的问题和我一样。在我的情况下,我无法从浏览器中导出整个证书链以将它们全部添加到信任存储中。你能分享一下你是怎么做的吗? - Alfredo Carrillo
你用的是什么浏览器?我相信我用的是Firefox,但我不记得具体的方法了。 - Casey
我之前使用IE。现在我可以用FireFox导出它,成功地添加到信任存储中,但仍然出现相同的异常。我会继续尝试。无论如何,谢谢! - Alfredo Carrillo
实际上,Web服务器应该发送链中的所有证书。这是Java客户端实现无法处理的问题吗?您了解此问题的详细信息吗? - aholbreich

7

我在尝试使用Java代码发起SOAP请求时遇到了这个问题。对我有效的方法是:

  1. 通过在浏览器中访问此URL获取服务器证书:http://docs.bvstools.com/home/ssl-documentation/exporting-certificate-authorities-cas-from-a-website该链接包含获取服务器证书的所有步骤。

  2. 一旦您拥有服务器证书,请按照http://java.globinch.com/enterprise-java/security/pkix-path-building-failed-validation-sun-security-validatorexception/#Valid-Certification-Path-to-Requested-Target的步骤操作。

如果此链接失效,可以复制链接中的文本:

您需要做的唯一修复此错误的方式是将服务器证书添加到受信任的Java密钥库中。首先,您需要从服务器下载该文档。

一旦您将证书保存到硬盘上,您可以将其导入到Java信任存储区。要将证书导入到受信任的Java密钥库中,可以使用Java“keytool”工具。在命令提示符上导航到JRE bin文件夹,例如我的路径为:C:\Program Files\Java\jdk1.7.0_75\jre\bin。然后使用以下keytool命令将证书导入到JRE中:

keytool -import -alias _alias_name_ -keystore ..\lib\security\cacerts -file _path_to_cer_file

它会要求输入密码。默认情况下,密码为“changeit”。如果密码不同,则可能无法导入证书。


7

以下是我用于将站点的公共证书安装到系统密钥库以供使用的解决方案。

使用以下命令下载证书:

unix,linux,mac

openssl s_client -connect [host]:[port|443] < /dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > [host].crt

windows

openssl s_client -connect [host]:[port|443] < NUL | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > [host].crt

这将创建一个crt文件,可用于导入到密钥库中。

使用以下命令安装新证书:

keytool -import -alias "[host]" -keystore [path to keystore] -file [host].crt

这将允许您从导致异常的网站导入新证书。

2
许多现代服务器使用SNI; 如果是这样,对于s_client获取正确的证书,请添加-servername $host。在Java7中,-importcert可以处理PEM文件中的无关文本,因此您不需要sed; 此外,在j7中,keytool -printcert -sslserver $host[:$port] -rfc获取证书链并以PEM格式打印(类似于s_client -showcerts而没有无关文本),如果您没有安装OpenSSL,这将更容易,例如Windows。 - dave_thompson_085
你的Windows命令片段使用了sed,所以在Windows上无法工作。 - Tom Ferguson

6

如果服务器仅发送其Leaf证书而不发送构建到根CA的信任链所需的所有链式证书,则也可能发生此错误。不幸的是,这是服务器常见的配置错误之一。

大多数浏览器会解决此问题,如果它们已经知道了先前访问中缺失的链式证书,或者如果在Authority Information Access(AIA)中的CA Issuers的URL链接在Leaf证书中包含,则可以下载缺失的证书。但是,此行为通常限于桌面浏览器,而其他工具则无法构建信任链而失败。

您可以通过将com.sun.security.enableAIAcaIssuers设置为true来使JRE自动下载中间证书。

要验证服务器是否发送了所有链式证书,您可以在以下SSL证书验证工具中输入主机:https://www.digicert.com/help/


3
在Mac OS上,我需要使用系统的Keychain Access工具打开服务器的自签名证书,将其导入并双击,然后选择“始终信任”(即使我在导入程序中设置了相同的选项)。 在此之前,我当然会使用“-importcert”参数运行Java Key Tool来将同一文件导入cacert存储。

2
感谢 @danny-paul 的回答(https://dev59.com/FG855IYBdhLWcg3w-JMr#60851862),我在自托管实例上解决了问题。
我使用Tomcat上的Birt应用程序,并且无法从Apache服务下载图片。
我注意到我的Windows服务器上的Apache配置不完整。第三行被注释掉了。我取消注释,重新启动Apache并解决了问题。
SSLEngine on
SSLCertificateFile "c:/Apache24/conf/ssl/cert.pem"
SSLCertificateKeyFile "c:/Apache24/conf/ssl/privkey.pem"
SSLCACertificateFile "c:/Apache24/conf/ssl/chain.pem"

1
我在从Tomcat7迁移到Tomcat9后遇到了这个问题。问题出现在Tomcat9/lib/setenv.sh文件中,以下属性被错误地设置为true。

-Djsse.enableSNIExtension=false

将其更新为false解决了我的问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接