证书路径验证异常: 未找到证书路径的信任锚点 - Retrofit Android

68

我正在创建一个安卓应用,使用https与服务器通信。我使用retrofitOkHttp进行请求。这对于标准的http请求运行良好。以下是我遵循的步骤。

步骤1: 使用命令从服务器获取证书文件

echo -n | openssl s_client -connect api.****.tk:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > gtux.cert

第二步 :使用以下命令将证书转换为BKS格式

keytool -importcert -v -trustcacerts -file "gtux.cert" -alias imeto_alias -keystore "my_keystore.bks" -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath "bcprov-jdk16-146.jar" -storetype BKS

它要求我输入密码,文件成功创建。

步骤3:

创建一个OkHttpClient,并将其用于进行https请求。

public class MySSLTrust {
public static OkHttpClient trustcert(Context context){
    OkHttpClient okHttpClient = new OkHttpClient();
    try {
        KeyStore ksTrust = KeyStore.getInstance("BKS");
        InputStream instream = context.getResources().openRawResource(R.raw.my_keystore);
        ksTrust.load(instream, "secret".toCharArray());
        // TrustManager decides which certificate authorities to use.
        TrustManagerFactory tmf = TrustManagerFactory
                .getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(ksTrust);
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, tmf.getTrustManagers(), null);
        okHttpClient.setSslSocketFactory(sslContext.getSocketFactory());
    } catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException | KeyManagementException e) {
        e.printStackTrace();
    }
    return okHttpClient;
}
}

第四步:

必须创建 RestAdapter。

RestAdapter.Builder()
.setRequestInterceptor(intercept)
.setEndpoint("https://api.****.tk")
.setClient(new OkClient(this))
.setLogLevel(RestAdapter.LogLevel.FULL)
.setLog(new AndroidLog("RETROFIT"))
.build();

但最终在运行应用程序时,它抛出了CertPathValidatorException: Trust anchor for certificate path not found的异常。请帮我解决这个问题。谢谢。

其他失败尝试: 我尝试在我的Xperia Z2上安装证书,它说文件已经安装了,但当我运行应用程序时,仍然抛出相同的异常。

错误日志 以下是我执行时得到的错误日志...

错误日志

将其粘贴在那里以便于阅读..

9个回答

91

免责声明: 这个答案发布于2015年7月,使用的是那个时候的RetrofitOkHttp
点击这个链接了解关于Retrofit v2的更多信息,点击这个链接了解当前OkHttp方法。

好的,我使用Android开发者指南解决了问题。

就像楼主一样,我尝试使用RetrofitOkHttp连接到一个自签名的SSL启用服务器。

下面是使事情正常工作的代码(我已经删除了try/catch块):

public static RestAdapter createAdapter(Context context) {
  // loading CAs from an InputStream
  CertificateFactory cf = CertificateFactory.getInstance("X.509");
  InputStream cert = context.getResources().openRawResource(R.raw.my_cert);
  Certificate ca;
  try {
    ca = cf.generateCertificate(cert);
  } finally { cert.close(); }

  // creating a KeyStore containing our trusted CAs
  String keyStoreType = KeyStore.getDefaultType();
  KeyStore keyStore = KeyStore.getInstance(keyStoreType);
  keyStore.load(null, null);
  keyStore.setCertificateEntry("ca", ca);

  // creating a TrustManager that trusts the CAs in our KeyStore
  String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
  TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
  tmf.init(keyStore);

  // creating an SSLSocketFactory that uses our TrustManager
  SSLContext sslContext = SSLContext.getInstance("TLS");
  sslContext.init(null, tmf.getTrustManagers(), null);

  // creating an OkHttpClient that uses our SSLSocketFactory
  OkHttpClient okHttpClient = new OkHttpClient();
  okHttpClient.setSslSocketFactory(sslContext.getSocketFactory());

  // creating a RestAdapter that uses this custom client
  return new RestAdapter.Builder()
              .setEndpoint(UrlRepository.API_BASE)
              .setClient(new OkClient(okHttpClient))
              .build();
}
为了帮助调试,我还在我的RestAdapter创建命令中添加了.setLogLevel(RestAdapter.LogLevel.FULL),这样我就可以看到它连接并从服务器获取响应。
所需的全部就是我原来保存在main/res/raw中的原始.crt文件。 .crt文件,也称为证书,是使用openssl创建证书时创建的两个文件之一。通常是.crt或.cert文件,而另一个是.key文件。
据我所知,.crt文件是您的公钥,而.key文件是您的私钥。
据我所见,您已经有了一个.cert文件,它是相同的,因此尝试使用它。
附言:对于未来阅读本文且只有.pem文件的人,根据此答案,您只需要将其转换为另一个即可。
openssl x509 -outform der -in your-cert.pem -out your-cert.crt

PS²: 对于那些没有任何文件的人,您可以使用以下命令(Bash)从任何服务器提取公钥(又称证书):

echo -n | openssl s_client -connect your.server.com:443 | \
  sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > ~/my_cert.crt

只需替换your.server.com和端口(如果不是标准的HTTPS),然后选择一个有效路径以创建输出文件。


这对我也起作用。但是将证书文件放在客户端,是否足够安全? - DàChún
这是与服务器公钥对应的 .crt 文件。如果您没有它,可以使用 OpenSSL 从服务器轻松获取它。搜索获取 pem 文件的命令(另一种证书格式)并将其下载到您的计算机中,然后运行我的答案中的命令以将其转换为 .crt 文件。 - Paulo Avelar
1
@PauloAvelar 感谢您的建议!我注意到问题在于我需要重新启动WAMP服务器,以便使用新生成的带有域名的证书,因为我没有这样做,所以问题是我在Android中使用了新证书,但WAMP正在使用旧证书的缓存版本。 - Steve
2
如果okHttpClient.setSslSocketFactory(...)方法无法解决问题,您可以使用以下代码:okHttpClient= new OkHttpClient.Builder() .sslSocketFactory(new TLSSocketFactory()) .build(); - oguzhan
1
这不是HTTP over TLS的工作方式。客户端不需要访问每个网站的私钥。你确定你只是想连接到一个自签名的常规HTTP服务器吗?无论如何,如果一个私钥被打包在APK中,它也可以被视为公共的。与常规的HTTPS相比,它几乎没有增加任何安全性。 - Paulo Avelar
显示剩余12条评论

46
要解决 CertPathValidatorException 问题,请使用从 https://mobikul.com/android-retrofit-handling-sslhandshakeexception/ 复制的以下代码 —
 Retrofit retrofit = new Retrofit.Builder()
        .baseUrl(YOUR_BASE_URL)
        .client(getUnsafeOkHttpClient().build())
        .build();


  public static OkHttpClient.Builder getUnsafeOkHttpClient() {

    try {
        // Create a trust manager that does not validate certificate chains
        final TrustManager[] trustAllCerts = new TrustManager[]{
                new X509TrustManager() {
                    @Override
                    public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String
authType) throws CertificateException {
                    }
 
                    @Override
                    public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String
authType) throws CertificateException {
                    }
 
                    @Override
                    public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                        return new java.security.cert.X509Certificate[]{};
                    }
                }
        };
 
        // Install the all-trusting trust manager
        final SSLContext sslContext = SSLContext.getInstance("SSL");
        sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
 
        // Create an ssl socket factory with our all-trusting manager
        final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
 
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]);
        builder.hostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        });
        return builder;
    } catch (Exception e) {
        throw new RuntimeException(e);
    } }

1
@vishnuc156,我仍然遇到相同的错误...你能帮我解决吗? - Vishwa Pratap
谢谢您提供的解决方案。 - Devrath
1
这不是在禁用证书上的所有验证吗?所以说任何证书都是有效的? - Brian S
2
此实现不安全,请勿将其用于生产代码:https://stackoverflow.com/a/39032433/3808228 - Chisko
1
问题不在于如何禁用SSL证书检查。 - blinker
显示剩余3条评论

11

这里是Kotlin版本。Okhttp 4.9.0
谢谢你 :)

         fun unSafeOkHttpClient() :OkHttpClient.Builder {
            val okHttpClient = OkHttpClient.Builder()
            try {
                // Create a trust manager that does not validate certificate chains
                val trustAllCerts:  Array<TrustManager> = arrayOf(object : X509TrustManager {
                    override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?){}
                    override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
                    override fun getAcceptedIssuers(): Array<X509Certificate>  = arrayOf()
                })

                // Install the all-trusting trust manager
                val  sslContext = SSLContext.getInstance("SSL")
                sslContext.init(null, trustAllCerts, SecureRandom())

                // Create an ssl socket factory with our all-trusting manager
                val sslSocketFactory = sslContext.socketFactory
                if (trustAllCerts.isNotEmpty() &&  trustAllCerts.first() is X509TrustManager) {
                    okHttpClient.sslSocketFactory(sslSocketFactory, trustAllCerts.first() as X509TrustManager)
                    okHttpClient.hostnameVerifier {HostnameVerifier { _, _ -> true } }
                }

                return okHttpClient
            } catch (e: Exception) {
                return okHttpClient
            }
        }

4
问题不在于如何禁用SSL证书检查。 - blinker

10

改进 2.3.0

    // Load CAs from an InputStream
    CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");

    InputStream inputStream = context.getResources().openRawResource(R.raw.ssl_certificate); //(.crt)
    Certificate certificate = certificateFactory.generateCertificate(inputStream);
    inputStream.close();

    // Create a KeyStore containing our trusted CAs
    String keyStoreType = KeyStore.getDefaultType();
    KeyStore keyStore = KeyStore.getInstance(keyStoreType);
    keyStore.load(null, null);
    keyStore.setCertificateEntry("ca", certificate);

    // Create a TrustManager that trusts the CAs in our KeyStore.
    String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
    TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm);
    trustManagerFactory.init(keyStore);

    TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
    X509TrustManager x509TrustManager = (X509TrustManager) trustManagers[0];


    // Create an SSLSocketFactory that uses our TrustManager
    SSLContext sslContext = SSLContext.getInstance("SSL");
    sslContext.init(null, new TrustManager[]{x509TrustManager}, null);
    sslSocketFactory = sslContext.getSocketFactory();

    //create Okhttp client
    OkHttpClient client = new OkHttpClient.Builder()
                .sslSocketFactory(sslSocketFactory,x509TrustManager)
                .build();

    Retrofit retrofit = new Retrofit.Builder()
                    .baseUrl(url)
                    .addConverterFactory(GsonConverterFactory.create())
                    .client(client)
                    .build();

我在 Android API 21 及以上版本上遇到了错误。这个解决方案适用于我,但是当服务器更新了新证书或更新了证书时,没有重建 Android 应用程序以使其与该服务器配合工作。 - Bhadresh

10

我不使用Retrofit,对于OkHttp,这是我找到的唯一解决自签名证书问题的方法:

  1. 从我们的网站获取证书,就像Gowtham的问题中一样,并将其放入项目的res/raw目录中:

echo -n | openssl s_client -connect elkews.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > ./res/raw/elkews_cert.crt
  • 使用Paulo的答案来设置ssl工厂(现在使用OkHttpClient.Builder()),但不创建RestAdapter

  • 然后添加以下解决方案以修复:SSLPeerUnverifiedException:Hostname not verified

  • 因此,Paulo代码的末尾(在sslContext初始化之后)对我而言看起来如下:

    ...
    OkHttpClient.Builder builder = new OkHttpClient.Builder().sslSocketFactory(sslContext.getSocketFactory());
    builder.hostnameVerifier(new HostnameVerifier() {
      @Override
      public boolean verify(String hostname, SSLSession session) {
        return "secure.elkews.com".equalsIgnoreCase(hostname);
    });
    OkHttpClient okHttpClient = builder.build();
    

    3
    谢谢夸奖答案。不过有一个问题:仅检查主机名而没有SSL会话,您是否容易受到中间人攻击?我不知道还进行了哪些其他验证,但我记得您覆盖的那个非常重要。 - Paulo Avelar

    4
    您正在将证书转换为BKS密钥库,为什么不直接使用.cert文件,来自https://developer.android.com/training/articles/security-ssl.html:
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    InputStream instream = context.getResources().openRawResource(R.raw.gtux_cert);
    Certificate ca;
    try {
        ca = cf.generateCertificate(instream);
    } finally {
        caInput.close();
    }
    
    KeyStore kStore = KeyStore.getInstance(KeyStore.getDefaultType());
    kStore.load(null, null);
    kStore.setCertificateEntry("ca", ca);
    
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm(););
    tmf.init(kStore);
    
    SSLContext context = SSLContext.getInstance("TLS");
    context.init(null, tmf.getTrustManagers(), null);
    
    okHttpClient.setSslSocketFactory(context.getSocketFactory());
    

    在我的安卓应用程序中,我应该把gtux.cert放在哪里? - Gowtham Raj
    我的方法为什么没有成功,有什么特别的原因吗? - Gowtham Raj
    @moonzai,如果证书的CN为“example.com”,我无法使此代码工作。当CN为域名的证书出现时,我总是会收到“CertPathValidatorException:找不到认证路径的信任锚点”错误。只有在将CN设置为公司名称之类的内容时,它似乎才能正常工作,但在这种情况下,如何验证主机名呢? - Steve
    @Steve 可能有多种原因,但最好在官方 Google 页面上阅读此处: https://developer.android.com/training/articles/security-ssl.html#CommonProblems - moonzai

    3
    在Kotlin中的实现:Retrofit 2.3.0。
    private fun getUnsafeOkHttpClient(mContext: Context) : 
    OkHttpClient.Builder? {
    
    
    var mCertificateFactory : CertificateFactory = 
    CertificateFactory.getInstance("X.509")
    var mInputStream = mContext.resources.openRawResource(R.raw.cert)
                var mCertificate : Certificate = mCertificateFactory.generateCertificate(mInputStream)
            mInputStream.close()
    val mKeyStoreType = KeyStore.getDefaultType()
    val mKeyStore = KeyStore.getInstance(mKeyStoreType)
    mKeyStore.load(null, null)
    mKeyStore.setCertificateEntry("ca", mCertificate)
    
    val mTmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm()
    val mTrustManagerFactory = TrustManagerFactory.getInstance(mTmfAlgorithm)
    mTrustManagerFactory.init(mKeyStore)
    
    val mTrustManagers = mTrustManagerFactory.trustManagers
    
    val mSslContext = SSLContext.getInstance("SSL")
    mSslContext.init(null, mTrustManagers, null)
    val mSslSocketFactory = mSslContext.socketFactory
    
    val builder = OkHttpClient.Builder()
    builder.sslSocketFactory(mSslSocketFactory, mTrustManagers[0] as X509TrustManager)
    builder.hostnameVerifier { _, _ -> true }
    return builder
    

    }


    1

    根据Hani的回答,这对我来说完美地运作了。但是如果您遇到编译错误,需要修复一个问题。

    okHttpClient.hostnameVerifier {HostnameVerifier { _, _ -> true } }更改为okHttpClient.hostnameVerifier { _, _ -> true }

    并且这个函数

    fun unSafeOkHttpClient() :OkHttpClient.Builder {
        val okHttpClient = OkHttpClient.Builder()
        try {
            // Create a trust manager that does not validate certificate chains
            val trustAllCerts:  Array<TrustManager> = arrayOf(object : X509TrustManager {
                override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?){}
                override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
                override fun getAcceptedIssuers(): Array<X509Certificate>  = arrayOf()
            })
    
            // Install the all-trusting trust manager
            val  sslContext = SSLContext.getInstance("SSL")
            sslContext.init(null, trustAllCerts, SecureRandom())
    
            // Create an ssl socket factory with our all-trusting manager
            val sslSocketFactory = sslContext.socketFactory
            if (trustAllCerts.isNotEmpty() &&  trustAllCerts.first() is X509TrustManager) {
                okHttpClient.sslSocketFactory(sslSocketFactory, trustAllCerts.first() as X509TrustManager)
                okHttpClient.hostnameVerifier { _, _ -> true } // change here
            }
    
            return okHttpClient
        } catch (e: Exception) {
            return okHttpClient
        }
    }
    

    干得好,但这应该是对原始答案的评论。 - Den Drobiazko

    0
    经过长时间的研究和深入挖掘,我找到了 Android 中证书固定的解决方案。与 iOS 不同的是,在 Android 中我们只需要一个哈希码而不是证书本身。 如何获取证书的哈希码? 最初,只需使用错误的哈希码,您的 Java 类将抛出一个带有正确哈希码或哈希链的错误,只需将其复制并粘贴到您的代码中即可。
    这个解决方案解决了我的问题:https://dev59.com/PI7ea4cB1Zd3GeqPGdaD#45853669

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