【问题标题】:SSLHandshakeException on Marshmallow棉花糖上的 SSLHandshakeException
【发布时间】:2018-09-08 04:45:59
【问题描述】:

情况
我已经构建了一个小型 android 应用程序(sdk 21+),它连接到服务器,获取一些数据并显示它。对于连接,我使用OkHttp 库。在 Android 7+ 中运行一切正常。
还应该提到我是网络新手,还没有最丰富的知识。

问题
在 Android 6 上运行(在我的情况下是 api 23)我得到以下异常。

java.security.cert.CertificateException:
java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

在我的network_security_config.xml 中,我有 3 个certificates 注册为我的trust-anchors
我无法理解这个例外,当我在互联网上搜索它时,我也找不到任何有用的东西。

问题
什么可能会导致这个问题,我该如何解决?请尽量保持简单,以便我理解。

【问题讨论】:

  • @sunilkumar 已经查过了,但我没听懂。正如我所说,我是网络新手,不知道 TrustStore 完成了哪些功能等。不同版本之间的 TrustStore 有何不同?我认为它是由我的 network_security_config.xml 定义的,它为所有版本加载相同
  • 创建一个不验证证书链的信任管理器并尝试调用你的 api
  • @ss_ 我认为我无法影响我的连接使用哪个 TrustManager,因为我的连接是由 OkHttp 库提供的。因此,除非有某种方法可以插入/覆盖默认的 TrustManager,否则我认为这不会完成这项工作

标签: android certificate sslhandshakeexception


【解决方案1】:

我在使用 Volley 时遇到了同样的问题。没有 HTTPS 连接适用于 Android Marshmallow 及更低版本。对于 Nouget 及更高版本,一切都很好,因为我将以下配置 android:networkSecurityConfig="@xml/network_security_config" 与我的所有域特定证书一起使用。

根据the Android documentation

默认情况下,来自所有应用的安全连接(使用 TLS 和 HTTPS 等协议)信任预安装的系统 CA,而面向 Android 6.0(API 级别 23)及更低版本的应用也默认信任用户添加的 CA 商店。应用可以使用 base-config(用于应用范围的自定义)或 domain-config(用于每个域的自定义)自定义其自己的连接。

所以,在 Marshmallow 下,事情的运作方式有所不同是有道理的。正如@Bastu 在他的回答中所说:

原来 AndroidManifest.xml 中应用程序元素的标志 android:networkSecurityConfig 仅适用于 api >= 24

在找到这个问题的答案之前,我偶然发现了this wonderful tutorial。稍微弄乱了代码,我最终将这段代码放在一起,以便能够使用证书列表:

import android.content.Context;
import android.content.res.Resources;
import android.os.Build;
import android.util.Log;

import com.android.volley.RequestQueue;
import com.android.volley.toolbox.Volley;
import com.kitsord.R;

import java.io.IOException;
import java.io.InputStream;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

public class ExternalConfig {
    private static final String TAG = "ExternalConfig";
    private static RequestQueue queue;

    public static RequestQueue getRequestQueue(final Context applicationContext) {
        if (queue == null) {
            queue = Volley.newRequestQueue(applicationContext);
            if (Build.VERSION.SDK_INT < 24) {
                useSSLCertificate(context.getResources(), R.raw.my_certificate1, R.raw.my_certificate2);
            }
        }

        return queue;
    }

    private static void useSSLCertificate(final Resources resources, final int ... rawCertificateResourceIds) {
        final CertificateFactory certificateFactory;
        try {
            certificateFactory = CertificateFactory.getInstance("X.509");
        } catch (final CertificateException exception) {
            Log.e(TAG, "Failed to get an instance of the CertificateFactory.", exception);
            return;
        }
        int i = 0;
        final Certificate[] certificates = new Certificate[rawCertificateResourceIds.length];
        for (final int rawCertificateResourceId : rawCertificateResourceIds) {
            final Certificate certificate;
            try (final InputStream certificateInputStream = resources.openRawResource(rawCertificateResourceId)) {
                certificate = certificateFactory.generateCertificate(certificateInputStream);
            } catch (final IOException | CertificateException exception) {
                Log.e(TAG, "Failed to retrieve the Certificate.", exception);
                return;
            }


            certificates[i] = certificate;
            i++;
        }

        final KeyStore keyStore;
        try {
            keyStore = buildKeyStore(certificates);
        } catch (final KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException exception) {
            Log.e(TAG, "Failed to build the KeyStore with the Certificate.", exception);
            return;
        }

        final TrustManagerFactory trustManagerFactory;
        try {
            trustManagerFactory = buildTrustManager(keyStore);
        } catch (final KeyStoreException | NoSuchAlgorithmException exception) {
            Log.e(TAG, "Failed to build the TrustManagerFactory with the KeyStore.", exception);
            return;
        }

        final SSLContext sslContext;
        try {
            sslContext = buildSSLContext(trustManagerFactory);
        } catch (final KeyManagementException | NoSuchAlgorithmException exception) {
            Log.e(TAG, "Failed to build the SSLContext with the TrustManagerFactory.", exception);
            return;
        }

        HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
    }

    private static KeyStore buildKeyStore(final Certificate[] certificates) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
        final String keyStoreType = KeyStore.getDefaultType();
        final KeyStore keyStore = KeyStore.getInstance(keyStoreType);
        keyStore.load(null, null);

        int i = 0;
        for (final Certificate certificate : certificates) {
            keyStore.setCertificateEntry("ca" + i, certificate);
            i++;
        }

        return keyStore;
    }

    private static TrustManagerFactory buildTrustManager(final KeyStore keyStore) throws KeyStoreException, NoSuchAlgorithmException {
        final String trustManagerAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
        final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(trustManagerAlgorithm);
        trustManagerFactory.init(keyStore);

        return trustManagerFactory;
    }

    private static SSLContext buildSSLContext(final TrustManagerFactory trustManagerFactory) throws KeyManagementException, NoSuchAlgorithmException {
        final TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();

        final SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustManagers, null);

        return sslContext;
    }
}

现在,每当我需要 Volley 队列时,这种方法不仅可以让我每次都使用同一个队列(不确定这是否是个坏主意),而且还会为 https 连接添加我的证书。我确信这段代码可以改进。

【讨论】:

    【解决方案2】:

    所以我弄清楚了错误发生的原因以及如何有效和正确地修复它,而不是仅仅覆盖我的连接并忽略所有证书如何在任何地方和每个人都建议它。

    原来AndroidManifest.xmlapplication 元素的标志android:networkSecurityConfig 仅适用于api >= 24。由于我的Android 6 手机在第23 级上运行,它在那里不起作用,@987654324 @ 未加载。

    java.security.cert.CertificateException:
    java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
    

    为了解决我的问题,我从原始资源中的文件手动加载证书(我还分配了一个名称以使其更加用户友好。这就是我在这里使用 Map 的原因,从技术上讲,列表或数组会很好够了)

    private Map<String, Certificate> createCertificates() throws CertificateException {
        CertificateFactory factory = CertificateFactory.getInstance("X.509");
        InputStream inputProxy = getResources().openRawResource(R.raw.proxy);
        InputStream inputCa = getResources().openRawResource(R.raw.ca);
        Certificate certProxy = factory.generateCertificate(inputProxy);
        Certificate certCa = factory.generateCertificate(inputCa);
        try {
            inputProxy.close();
        } catch (IOException ignore) {
            // will be dumped anyways
        }
        try {
            inputCa.close();
        } catch (IOException ignore) {
            // will be dumped anyways
        }
        Map<String, Certificate> certificates = new HashMap<>();
        certificates.put("CA", certCa);
        certificates.put("PROXY", certProxy);
        return certificates;
    }
    

    然后在运行任何网络操作之前​​,我检查了 api 级别是否 KeyChain.EXTRA_NAME 的数据不是必需的,但更用户友好)

    if (Build.VERSION.SDK_INT < 24) {
        try {
           Map<String, Certificate> certificates = createCertificates();
           for (String key : certificates.keySet()) {
             Certificate cert = certificates.get(key);
             if (!isCertificateInstalled(cert.getPublicKey())) {
              Intent installIntent = KeyChain.createInstallIntent();
              installIntent.putExtra(KeyChain.EXTRA_CERTIFICATE, cert.getEncoded());
              installIntent.putExtra(KeyChain.EXTRA_NAME, key);
              startActivity(installIntent);
           }
         }
       } catch (CertificateException ignore) {
          // Netzwerkdialog wird später angezeigt
      }
    }
    

    但我只在证书尚未安装时提示用户。我检查使用证书的PublicKey(理论上不是100%安全,但是有人使用相同的公钥安装两个证书的机会非常小)

    private boolean isCertificateInstalled(PublicKey pPublicKey) {
        try {
            TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            tmf.init((KeyStore) null);
            X509TrustManager xtm = (X509TrustManager) tmf.getTrustManagers()[0];
            for (X509Certificate cert : xtm.getAcceptedIssuers()) {
                if (cert.getPublicKey().equals(pPublicKey)) {
                    return true;
                }
            }
        } catch (NoSuchAlgorithmException | KeyStoreException ignore) {
            // returns false
        }
        return false;
    }
    

    【讨论】:

      【解决方案3】:

      只需将您的 OkHttpClient 替换为以下

      private static OkHttpClient 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 X509Certificate[0];
                              }
                          }
                  };
      
                  // 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();
      
                  return new OkHttpClient.Builder()
                          .sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0])
                          .hostnameVerifier(new HostnameVerifier() {
                              @Override
                              public boolean verify(String hostname, SSLSession session) {
                                  return true;
                              }
                          })
                          .connectTimeout(30, TimeUnit.SECONDS)
                          .writeTimeout(30, TimeUnit.SECONDS)
                          .retryOnConnectionFailure(true)
                          .readTimeout(30, TimeUnit.SECONDS).addInterceptor(new Interceptor() {
                              @Override
                              public okhttp3.Response intercept(Chain chain) throws IOException {
                                  Request original = chain.request();
      
                                  Request request = original.newBuilder()
                                          .build();
                                  return chain.proceed(request);
                              }
                          }).build();
      
      
              } catch (Exception e) {
                  throw new RuntimeException(e);
              }
          }
      

      【讨论】:

      • 找到了一种更简洁的方式,而不会覆盖任何连接或类似的东西。看我的回答。
      猜你喜欢
      • 2016-02-06
      • 1970-01-01
      • 1970-01-01
      • 2023-03-17
      • 2017-02-02
      • 2016-03-15
      • 1970-01-01
      • 2017-05-12
      • 2019-01-29
      相关资源
      最近更新 更多