SSLHandshakeException на Marshmallow

Ситуация
Я создал небольшое приложение для 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
Я не могу разобраться в этом исключении, и при поиске в Интернете я также не нашел ничего полезного.

Вопрос
В чем может быть причина этой проблемы и как ее исправить? Пожалуйста, постарайтесь, чтобы это было просто, чтобы я мог понять это.


person Basti    schedule 29.03.2018    source источник
comment
Пожалуйста, обратитесь к этому ответу stackoverflow.com/questions/25122287/   -  person Sunil Kumar    schedule 29.03.2018
comment
@sunilkumar уже посмотрел это, но я не совсем понял. Как я уже сказал, я новичок в сетевых технологиях и не знаю, какие функции выполняет TrustStore и т. д. Чем отличается TrustStore между версиями? Я думал, что это определяется моим файлом network_security_config.xml, который загружается одинаково для всех версий.   -  person Basti    schedule 29.03.2018
comment
Создайте диспетчер доверия, который не проверяет цепочки сертификатов, и попробуйте вызвать свой API.   -  person sm_    schedule 29.03.2018
comment
@ss_ я не думаю, что могу повлиять на то, какой TrustManager используется моим соединением, поскольку мое соединение обеспечивается библиотекой OkHttp. Поэтому, если нет какого-либо способа вставить/переопределить TrustManager по умолчанию, я не думаю, что это сработает.   -  person Basti    schedule 29.03.2018


Ответы (3)


Итак, я понял, почему возникает ошибка и как ее эффективно и правильно исправить, вместо того, чтобы просто переопределять мое соединение и игнорировать все сертификаты, как это предлагается везде и всеми.

Оказывается, флаг android:networkSecurityConfig элемента application в AndroidManifest.xml работает только на API >= 24. Поскольку мой телефон Android 6 работал на уровне 23, он там не работал, а trust anchors не загружались.

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

Чтобы решить мою проблему, я вручную загрузил сертификаты из файлов в необработанном ресурсе (я также присвоил имя, чтобы сделать его более удобным для пользователя. Вот почему я использую здесь карту, технически Список или Массив были бы достаточно хороши)

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 ‹ 24. Если это так, я создал свои сертификаты и предложил пользователю установить их (данные для 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;
}
person Basti    schedule 29.03.2018

У меня была такая же проблема при работе с Volley. Соединения HTTPS не будут работать с Android Marshmallow и более ранними версиями. Для Nouget и выше все было в порядке, поскольку я использовал следующую конфигурацию android:networkSecurityConfig="@xml/network_security_config" со всеми сертификатами, специфичными для моего домена.

Согласно документации Android:

По умолчанию безопасные соединения (с использованием таких протоколов, как TLS и HTTPS) из всех приложений доверяют предустановленным системным ЦС, а приложения, предназначенные для Android 6.0 (уровень API 23) и ниже, по умолчанию также доверяют хранилищу ЦС, добавленному пользователем. Приложение может настраивать свои собственные соединения с помощью base-config (для настройки всего приложения) или domain-config (для настройки для каждого домена).

Таким образом, имеет смысл, что в Marshmallow все работает по-другому. Как сказал @Bastu в своем ответе:

Оказывается, флаг android:networkSecurityConfig элемента приложения в AndroidManifest.xml работает только с API >= 24

Прежде чем найти ответ на этот вопрос, я наткнулся на это замечательный урок. Немного повозившись с кодом, я собрал этот код, чтобы иметь возможность использовать список сертификатов:

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. Я уверен, что этот код можно улучшить.

person cavpollo    schedule 30.11.2018

Просто замените свой 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);
        }
    }
person sm_    schedule 29.03.2018
comment
Нашел более чистый способ без переопределения каких-либо подключений или подобных вещей. Смотрите мой ответ. - person Basti; 29.03.2018