1

Situation
I have built a small android app (sdk 21+) which connects to a server, fetches some data and displays it. For the connection i use the OkHttp library. Running in Android 7+ it all works just fine.
Also should mention that i am new to networking and do not have the biggest knowledge yet.

Problem
Running on Android 6 (in my case api 23) i get the following exception.

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

In my network_security_config.xml i have 3 certificates registered as my trust-anchors
I can't make much of this exception and when searching for it on the internet i couldn't find anything helpful either.

Question
What could case this problem and how could i fix it? Please try to keep it simple so i can understand it.

Phantômaxx
  • 37,901
  • 21
  • 84
  • 115
Basti
  • 1,117
  • 12
  • 32
  • Please refer this answer https://stackoverflow.com/questions/25122287/java-security-cert-certpathvalidatorexception-trust-anchor-for-certification-pa – Sunil Kumar Mar 29 '18 at 09:34
  • @sunilkumar already looked that up but i did not quiet understand it. As i said im new to Networking and don't know what functions the TrustStore fullfills etc. How does the TrustStore differ between the version? I thought it is defined by my network_security_config.xml which is loaded the same for all versions – Basti Mar 29 '18 at 09:39
  • Create a trust manager that does not validate certificate chains and try calling your api – Sabyasachi Mar 29 '18 at 10:36
  • @ss_ i don't think i can influence which TrustManager is used by my connection since my connection is provided by the OkHttp library. So unless there is some way to insert/override the default TrustManager i don't think this will do the job – Basti Mar 29 '18 at 10:54

3 Answers3

0

So i figured out why the error happens and how to effectively and correctly fix it instead of just overriding my connection and ignoring all certificates how it is suggested everywhere and by everyone.

Turns out the flag android:networkSecurityConfig of the application element in the AndroidManifest.xml is only working on api >= 24. Since my Android 6 phone was running on level 23 it didn't work there and the trust anchors weren't loaded.

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

To solve my problem i manually loaded the certificates from the files in the raw resource (i also assigned a name to make it more user-friendly. Thats why i use a Map here, technically a List or an Array would be good enough)

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;
}

Then before running any network operations i checked if the api level is < 24. If so, i created my certificates and prompted the user to install them (The data for KeyChain.EXTRA_NAME wouldn't be necessary but is more userfriendly)

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
  }
}

But i only prompt the user if the certificate hasn't been installed yet. I check that using the PublicKey of a certificate (in theory not 100% safe, but the chance that someone installs two certificates with the same public key is very very small)

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;
}
Basti
  • 1,117
  • 12
  • 32
0

I had the same issue while working with Volley. No HTTPS connections would work with Android Marshmallow and lower. For Nouget and above things were just fine as I was using the following config android:networkSecurityConfig="@xml/network_security_config" with all my domain specific certificates.

According to the Android documentation:

By default, secure connections (using protocols like TLS and HTTPS) from all apps trust the pre-installed system CAs, and apps targeting Android 6.0 (API level 23) and lower also trust the user-added CA store by default. An app can customize its own connections using base-config (for app-wide customization) or domain-config (for per-domain customization).

So, it makes sense things work differently under Marshmallow. As @Bastu said in his answer:

Turns out the flag android:networkSecurityConfig of the application element in the AndroidManifest.xml is only working on api >= 24

Before finding this question's answer, I stumbled upon this wonderful tutorial. Messing with the code a bit, I ended pulling together this code to be able to use a list of certificates:

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;
    }
}

Now whenever I need a Volley queue, this method will not only let me use the same queue everytime (not sure if it's a bad idea), but will also add my certificate for the https connections. I'm sure this code can be improved.

cavpollo
  • 4,071
  • 2
  • 40
  • 64
-1

Just replace your OkHttpClient with below

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);
        }
    }
Sabyasachi
  • 3,499
  • 2
  • 14
  • 21
  • Found a cleaner way without overriding any Connections or similar stuff. See my answer. – Basti Mar 29 '18 at 13:20