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(); } } }
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.
Android stores CA certificates in its Java keystore in /system/etc/security/cacerts.
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();
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With