Your app shouldn't suffer SSL's problems

Dec 05, 2011

In recent months, Comodo has been hacked repeatedly, DigiNotar was compromised, and the security of CAs as a whole has been found to be not altogether inspiring. The consensus finally seems to be shifting from the notion that CAs are merely a ripoff, to the notion that they are a ripoff, a security problem, and that we want them dead as immediately as possible. The only question that remains is how to replace them.

The Trouble Comes With Generality

We need CA signatures (or an alternative authenticity infrastructure) for general purpose network communication tools: things like your web browser, command-line SSL clients, mail clients, and IM clients. These are applications that can make arbitrary SSL connections to whatever destination you specify, and could not possibly have any advance knowledge of what SSL certificate they should expect to receive from those arbitrary locations.

Instead, these clients are given advance knowledge of a handful of CA certificates, which then sign the certificates for all of the arbitrary locations a general purpose network client would like to connect. As they say, all problems in computer science can be solved by another layer of indirection.

But of course, you really strike out when that abstraction layer fails.

Mobile Is An Escape

One of the interesting things about the mobile environment is that we actually have an opportunity to write client-side software again. For better or worse, this means that mobile developers are no longer constrained by the generality of the web browser. This has obviously been explosive in its opportunity for integration with hardware-backed services like location information, accelerometer data, camera support, and IPC with other client-side software.

But it also creates a number of security-related opportunities, one of which is the way that mobile apps do secure communication.

If you have a mobile app that makes SSL connections to a service you control, there is really no reason to be validating your service’s certificate using CA signatures. Remember, CA signatures are for general purpose network communication, and that’s not what is happening here. We need an authenticity infrastructure when there is no way to have advance knowledge of what SSL certificate a client should expect to see, but your app knows where it will be connecting, and it knows exactly what it should expect.

Google is already doing this. They have an “app” called Chrome, and when their app makes SSL connections to their own services, it checks to make sure that the certificates it sees are the ones it knows Google is using. They call this “pinning,” and you should do it for your mobile apps.

There are two possible ways to do this on Android, here’s how!

Option 1: Wipe The Page Clean

The first option is to leave CA certificates behind all together. In addition to providing enhanced security, it feels refreshing.

On the server side, you’ll want to create your own 4096bit signing certificate that you keep offline, and use it to sign certificates that you generate for your web services. If you’ve got cash, you can do this with an HSM, otherwise you can just use OpenSSL and keep the signing certificate’s key offline.

On the client side, you simply need to distribute the signing certificate with your app and validate against it. On Android, to distribute it, first create a keystore using keytool:

$ wget http://bouncycastle.org/download/bcprov-jdk16-146.jar
$ keytool -importcert -file your_signing_certificate.pem -keystore yourapp.store -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath bcprov-jdk16-146.jar -storetype BKS

Put yourapp.store in the “assets” directory of your Android app. Now all you need to do is validate against it. To make a standard HTTPS request:

private InputStream makeRequest(Context context, URL url) {
  AssetManager assetManager       = context.getAssets();
  InputStream keyStoreInputStream = assetManager.open("yourapp.store");
  KeyStore trustStore             = KeyStore.getInstance("BKS");

  trustStore.load(keyStoreInputStream, "somepass".toCharArray());

  TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
  tmf.init(trustStore);

  SSLContext sslContext = SSLContext.getInstance("TLS");
  sslContext.init(null, tmf.getTrustManagers(), null);

  HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection();
  urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());

  return urlConnection.getInputStream();
}

Or in order to open a straight SSL socket, your code would be similar:

private Socket constructSSLSocket(Context context, String host, int port) {
  AssetManager assetManager        = context.getAssets();
  InputStream keyStoreInputStream  = assetManager.open("yourapp.store");			
  KeyStore trustStore              = KeyStore.getInstance("BKS");

  trustStore.load(keyStoreInputStream, "somepass".toCharArray());

  SSLSocketFactory sslSocketFactory = new SSLSocketFactory(trustStore);
  sslSocketFactory.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);

  return sslSocketFactory.connectSocket(sslSocketFactory.createSocket(), host, port, null, 0, new BasicHttpParams());
}

In both cases, you would obviously want to cache your SSLSocketFactory, and there’s some exception handling to be done. But by implementing something similar to the above, your App has securely opted out of the CA system entirely.

Option 2: Trust But Verify

If you need to stick with CA-signed certificates for some reason, you can limit the scope of your exposure. By default, your App likely validates against all of the CA certificates that ship with Android, but that means any single compromised CA in the total set can potentially compromise your communication (even if it’s not the CA you’re using). Your App knows what CA you’re using, however, so it can limit trust to signatures from that CA only (or a small set of CAs as backup).

As explained in Adam Langley’s nice Chrome writeup, you’ll want to pin the SubjectPublicKeyInfo of the CA certificate you’re using (and perhaps one or two others as a backup). I’ve put together a small Python script for generating a pin for a certificate, available here (these are compatible with Chrome’s pins — thanks are due to Adam Langley for providing his reference implementation in Go).

In order to generate your pin, you would do something similar to:

$ git clone https://github.com/moxie0/AndroidPinning.git
$ cd AndroidPinning
$ python ./pin.py /path/to/cacert.pem

On the Android side, I’ve put together a simple TrustManager implementation that is layered on top of the default system TrustManager, so that it continues validating against the system CA certificate store, but can additionally be made to enforce pins (as generated by you, above).

You can use this PinningTrustManager as follows:

TrustManager[] trustManagers = new TrustManager[1];
trustManagers[0]             = new PinningTrustManager(new String[] {"f30012bbc18c231ac1a44b788e410ce754182513"});

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagers, null);

HttpsURLConnection urlConnection = (HttpsURLConnection)new URL("https://encrypted.google.com/").openConnection();
urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());

InputStream in = urlConnection.getInputStream();

…where you would, of course, substitute your own pins and destination URL.

By doing this, your App is still vulnerable to the security and whims of the CA certificates you pin, but at least you’re no longer exposed to the security and whims of all 650 different CAs.

In Conclusion

Either by leaving CAs behind entirely (!), or by limiting exposure to them, anyone doing secure communication via mobile apps today has an opportunity to move beyond the constraints of the web browser and protect themselves. Give it a shot.

In the future, hopefully we’ll see solutions to the “general purpose” validation problem (such as Convergence ) integrated into general purpose clients, along with “general purpose” certificate pinning solutions (such TACK ).

© 2012 Moxie Marlinspike