Programmatically add a certificate authority while keeping Android system SSL certificates

Vincent Hiribarren picture Vincent Hiribarren · Dec 19, 2014 · Viewed 21.6k times · Source

There are lots of questions about this topic on StackOverflow, but I do not seem to find one related to my problem.

I have an Android application that needs to communicate with HTTPS servers: some signed with a CA registered in the Android system keystore (common HTTPS websites), and some signed with a CA I own but not in the Android system keystore (a server with an autosigned certificate for instance).

I know how to add my CA programmatically and force every HTTPS connection to use it. I use the following code:

public class SslCertificateAuthority {

    public static void addCertificateAuthority(InputStream inputStream) {

        try {
            // Load CAs from an InputStream
            // (could be from a resource or ByteArrayInputStream or ...)
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            InputStream caInput = new BufferedInputStream(inputStream);
            Certificate ca;
            try {
                ca = cf.generateCertificate(caInput);
            } finally {
                caInput.close();
            }

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

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

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

            // Tell the URLConnection to use a SocketFactory from our SSLContext
            HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());
        } catch (CertificateException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }  

    }

}

However, doing that disables the use of the Android system keystore, and I cannot query HTTPS sites signed with other CA any more.

I tried to add my CA in the Android keystore, using:

KeyStore.getInstance("AndroidCAStore")

... but I cannot then add my CA in it (an exception is launched).

I could use the instance method HttpsURLConnection.setSSLSocketFactory(...) instead of the static global HttpsURLConnection.setDefaultSSLSocketFactory(...) to tell on a case by case basis when my CA has to be used.

But it isn't practical at all, all the more since sometimes I cannot pass a preconfigured HttpsURLConnection object to some libraries.

Some ideas about how I could do that?


EDIT - ANSWER

Ok, following the given advice, here is my working code. It might need some enhancements, but it seems to work as a starting point.

public class SslCertificateAuthority {

    private static class UnifiedTrustManager implements X509TrustManager {
        private X509TrustManager defaultTrustManager;
        private X509TrustManager localTrustManager;
        public UnifiedTrustManager(KeyStore localKeyStore) throws KeyStoreException {
            try {
                this.defaultTrustManager = createTrustManager(null);
                this.localTrustManager = createTrustManager(localKeyStore);
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            }
        }
        private X509TrustManager createTrustManager(KeyStore store) throws NoSuchAlgorithmException, KeyStoreException {
            String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
            TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
            tmf.init((KeyStore) store);
            TrustManager[] trustManagers = tmf.getTrustManagers();
            return (X509TrustManager) trustManagers[0];
        }
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            try {
                defaultTrustManager.checkServerTrusted(chain, authType);
            } catch (CertificateException ce) {
                localTrustManager.checkServerTrusted(chain, authType);
            }
        }
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            try {
                defaultTrustManager.checkClientTrusted(chain, authType);
            } catch (CertificateException ce) {
                localTrustManager.checkClientTrusted(chain, authType);
            }
        }
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            X509Certificate[] first = defaultTrustManager.getAcceptedIssuers();
            X509Certificate[] second = localTrustManager.getAcceptedIssuers();
            X509Certificate[] result = Arrays.copyOf(first, first.length + second.length);
            System.arraycopy(second, 0, result, first.length, second.length);
            return result;
        }
    }

    public static void setCustomCertificateAuthority(InputStream inputStream) {

        try {
            // Load CAs from an InputStream
            // (could be from a resource or ByteArrayInputStream or ...)
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            InputStream caInput = new BufferedInputStream(inputStream);
            Certificate ca;
            try {
                ca = cf.generateCertificate(caInput);
                System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
            } finally {
                caInput.close();
            }

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

            // Create a TrustManager that trusts the CAs in our KeyStore and system CA
            UnifiedTrustManager trustManager = new UnifiedTrustManager(keyStore);

            // Create an SSLContext that uses our TrustManager
            SSLContext context = SSLContext.getInstance("TLS");
            context.init(null, new TrustManager[]{trustManager}, null);

            // Tell the URLConnection to use a SocketFactory from our SSLContext
            HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());

        } catch (CertificateException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

Answer

Dmitry picture Dmitry · Nov 24, 2018

It is an old question, but I met the same problem, so probably it is worth posting my answer. You tried to add your certificate to KeyStore.getInstance("AndroidCAStore"), but got an exception. Actually you should have done the opposite - add entries from that keystore to the one you created. My code is a bit different from yours, I just post it for the sake of complete answer even though only middle part matters.

KeyStore keyStore=KeyStore.getInstance("BKS");
InputStream in=activity.getResources().openRawResource(R.raw.my_ca);
try
{
  keyStore.load(in,"PASSWORD_HERE".toCharArray());
}
finally
{
  in.close();
}
KeyStore defaultCAs=KeyStore.getInstance("AndroidCAStore");
if(defaultCAs!=null)
{
  defaultCAs.load(null,null);
  Enumeration<String> keyAliases=defaultCAs.aliases();
  while(keyAliases.hasMoreElements())
  {
    String alias=keyAliases.nextElement();
    Certificate cert=defaultCAs.getCertificate(alias);
    try
    {
      if(!keyStore.containsAlias(alias))
        keyStore.setCertificateEntry(alias,cert);
    }
    catch(Exception e)
    {
      System.out.println("Error adding "+e);
    }
  }
}
TrustManagerFactory tmf=TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
// Get a new SSL context
SSLContext ctx = SSLContext.getInstance("SSL");
ctx.init(null,tmf.getTrustManagers(),new java.security.SecureRandom());
return ctx.getSocketFactory();