SSLHandshakeException на Зефир
ситуация
Я создал небольшое приложение для 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
Я не могу сделать большую часть этого исключения, и, ища это в Интернете, я также не мог найти ничего полезного.
Вопрос
В чем может быть проблема и как я могу это исправить? Пожалуйста, постарайтесь сделать это простым, чтобы я мог понять это.
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;
}
У меня была такая же проблема при работе с Волей. Никакие HTTPS-соединения не будут работать с Android Marshmallow и ниже.
Согласно документации Android:
По умолчанию безопасные соединения (с использованием протоколов, таких как TLS и HTTPS) из всех приложений доверяют предварительно установленным системным ЦС, а приложения, ориентированные на Android 6.0 (уровень API 23) и ниже, также доверяют добавленному хранилищу ЦС по умолчанию. Приложение может настроить свои собственные соединения, используя base-config (для настройки всего приложения) или domain-config (для настройки для каждого домена).
Таким образом, это имеет смысл работать по-разному под зефир. Как сказал @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_certificate);
}
}
return queue;
}
private static void useSSLCertificate(final Resources resources, final int rawCertificateResourceId) {
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;
}
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;
}
final KeyStore keyStore;
try {
keyStore = buildKeyStore(certificate);
} 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 certificate) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
final String keyStoreType = KeyStore.getDefaultType();
final KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", certificate);
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. Я уверен, что этот код может быть улучшен, например, для поддержки добавления большего количества сертификатов одновременно.
Просто замените ваш 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);
}
}