Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Programmatically add a certificate authority while keeping Android system SSL certificates

Tags:

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();         }     }  } 
like image 702
Vincent Hiribarren Avatar asked Dec 19 '14 09:12

Vincent Hiribarren


People also ask

Is it safe to install CA certificate in Android?

Android has tightly restricted this power for a while, but in Android 11 (released this week) it locks down further, making it impossible for any app, debugging tool or user action to prompt to install a CA certificate, even to the untrusted-by-default user-managed certificate store.

Where does Android store CA certificates?

Android stores CA certificates in its Java keystore in /system/etc/security/cacerts.


1 Answers

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(); 
like image 89
Dmitry Avatar answered Oct 04 '22 01:10

Dmitry