Android Security: SSL Pinning

Using SSL in an Android app is easy, however ensuring that the connection is actually secure is a different matter. A man-in-the-middle attack can be carried out using several methods including ARP cache poisoning and DNS spoofing.

Certificate chain for appmattus.com

Core to SSL (Secure Socket Layer) is the X509 certificate, and trust in the chain of certificates that leads from your leaf certificate through an intermediate certificate authority (CA) to a root certificate authority. This chain is determined when the SSL connection is established. The First Few Milliseconds of an HTTPS Connection talks a little about the process of creating a secure connection.

Root certificates come pre-installed on Android devices with around 150 included in Android N. You can check what’s on your own device by going to Settings > Security > Trusted Credentials. There is an assumption that none of these root CAs or the 1000’s of intermediate CAs these root certificates trust will mis-issue leaf certificates for domain names they shouldn’t. If you don’t believe me read about the CAs DigiNotar, GlobalSign and Comodo. In addition to all this the users device could be compromised with a rogue certificate installed on it through social engineering.

SSL pinning also known as Public Key Pinning is an attempt to solve these issues, ensuring that the certificate chain used is the one your app expects by checking a particular public key or certificate appears in the chain.

Implementing SSL pinning

Before writing any code there are a few decisions you need to make from the certificate you use for pinning to what you do when things go wrong. So lets start by talking about the certificates.

Which certificate should you pin against in the chain?

Your choice of certificate impacts the level of security you achieve, decreasing as you reach the root certificate. So it is important for you to choose carefully.

Leaf certificate. By pinning against your leaf certificate you are guaranteeing with close to 100% certainty that this is your certificate and thus the chain is valid. Leaf certificates tend to have a short expiry time and if, for instance, the SSL certificates are re-issued because the private key is compromised your app will be bricked until you can push an update out. Of course the same may also be true if you frequently cycle your certificates.

Intermediate certificate. By pinning against the intermediate certificate you are trusting that intermediate certificate authority to not mis-issue a certificate for your server(s). This also has the advantage that as long as you stick to the same certificate provider then any changes to your leaf certificates will work without having to update your app.

Root certificate. By pinning against the root certificate you are trusting the root certificate authority as well as any intermediaries they trust not to mis-issue certificates. Often the root and intermediate authorities are the same company in which case there’s not much difference in the number of people you are trusting, however that’s not always the case.

You don’t have to pin against just one certificate in the chain. Indeed, the general recommendation is to pin against multiple levels to decrease the chances of bricking your app at the expense of trusting more certificate issuers. In my mind it would seem prudent to pin at the intermediate and leaf levels to give a sensible balance.

Certificate or public key pinning?

In the Android training documentation about pinning their example pins against the certificate. However often it is better to pin against the public key, or more specifically the SubjectPublicKeyInfo (SPKI).

Some websites rotate their certificates on a monthly basis and so pinning against a certificate would mean an app no one can use after a month or pushing out frequent updates. Typically though the public key inside these rotated certificates stays the same. By pinning against the key you are reducing the chances of bricking your app by limiting the values that are checked.

With most network APIs you can choose to pin against the certificate or the SPKI although typically it is far easier to pin using the SPKI as most APIs provide a built in mechanism, such as OkHttp’s CertificatePinner. With HttpUrlConnection there’s not much difference in the development cost so for consistency I have shown code using the SPKI.

Handling compromise

If the pins stored in your app don’t match those returned by the HTTPS connection how do you inform the user?

Fail hard. Stop the app from establishing the connection. This is the most secure and easiest to implement but introduces the chance of self-induced denial of service and user experience issues when connections cannot be established.

Fail soft. Let the app establish a connection without pinning. This is complex to get right but one possibility is to limit app functionality in this case.

My personal preference is towards failing hard. Whichever you choose though ensure you log the compromise to your backend to keep yourself aware of it.

Handling private key leaks

If your private key were to ever get into the wrong hands you would need to setup certificates with a new key pretty quickly. This is why you are recommended to have backup SSL certificates ahead of time so you can quickly switch out the old at a moments notice.

We are checking the certificate chain to see if it contains at least one of a set of keys, after all this is how pinning against, say, both the leaf and root certificates would work — you only need one of the pins to match. This also means you can store pins for any backup certificates ahead of time so when disaster strikes your app will already continue to work with no new app release.

Where do I store the certificate/public key?

With time the pins your app needs to validate the certificate chain will change so your storage of these pins can have a big impact on the maintenance of your app.

There are a few options when it comes to this, each with their own caveats.

Preloading. Storing the pins hardcoded as part of the app is by far the easiest to implement. It can also be one of the more complex to maintain as a change in server certificates can mean you need to force your clients to upgrade your app.

Trust on first use. The app can on first execution determine the public key by making a call to the server and storing this for all future executions. This can help when your certificates rotate frequently however leads the app open to key tainting on initialisation and pin expiration. If you don’t know your endpoints ahead of time though this can be a good option however you still need to carefully manage pin failures.

Over the air. For maximum flexibility creating a pin server that returns the current set of valid pins is a great option. You still need to pin the pin server but its then incredibly easy to manage the pins stored and update this when disaster strikes.

Generally it is best to store the pins out-of-band embedded as part of the app. If you are concerned with data leakage when your app is decompiled or reverse-engineered then obfuscating the keys is not hard to do — ProGuard and DexGuard can help in this regard. Obfuscation may be especially important if you store a backup key that is not currently in use to help hide it from prying eyes.

Code examples

By now you should have a good idea of what certificate(s) you want to pin and where you might store these. So lets move on and see how you implement this in code on Android.

Retrieving your public keys

To implement the pinning examples shown here you need to know your certificates SPKI data. Some of the APIs presented here will throw an exception with the keys returned by the server when pinning fails however if you want to retrieve these yourself it is possible to use openssl.

Given a domain name, the code below prints out the public keys in the chain as a SHA-256 hash using base 64 encoding.

#!/bin/bash
certs=`openssl s_client -servername $1 -host $1 -port 443 -showcerts </dev/null 2>/dev/null | sed -n '/Certificate chain/,/Server certificate/p'`
rest=$certs
while [[ "$rest" =~ '-----BEGIN CERTIFICATE-----' ]]
do
cert="${rest%%-----END CERTIFICATE-----*}-----END CERTIFICATE-----"
rest=${rest#*-----END CERTIFICATE-----}
 echo `echo "$cert" | grep 's:' | sed 's/.*s:\(.*\)/\1/'`
 echo "$cert" | openssl x509 -pubkey -noout | 
openssl rsa -pubin -outform der 2>/dev/null |
openssl dgst -sha256 -binary | openssl enc -base64
done

Running the script on appmattus.com gives the following results:

user$ ./certs.sh www.appmattus.com
/CN=appmattus.com
4hw5tz+scE+TW+mlai5YipDfFWn1dqvfLG+nU7tq1V8=
/C=US/O=Let’s Encrypt/CN=Let’s Encrypt Authority X3
YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=

Note that this doesn’t print out the root certificate used.

Pinning on Android N

If your minimum SDK is Android N (API 24) then the implementation couldn’t be simpler as Android has a new API in town the Network Security Configuration. Even better this configuration even works for WebViews with no additional effort on your part.

Through a simple entry in your AndroidManifest.xml file you specify an XML configuration file that defines the pins you require. Of course being XML based this isn’t useful if you want to dynamically specify your pins.

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">appmattus.com</domain>
<pin-set>
<pin digest="SHA-256">4hw5tz+scE+TW+mlai5YipDfFWn1dqvfLG+nU7tq1V8=</pin>
<pin digest="SHA-256">YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=</pin>
</pin-set>
</domain-config>
</network-security-config>

It is possible to configure an expiration date by using <pin-set expiration="2017–02–28"> but it is worth noting you are then accepting insecure connections once that date has past for users that don’t/can’t upgrade your app.

The Network Security Configuration also makes it easy if you need to support self-signed certificates or certificate authorities that are not trusted system root certificates.

CWAC-NetSecurity, an unofficial back port, offers support for this file back to Android 4.2 (API 17) however the current version, 0.3, has limited support if you use HttpUrlConnection but might be worth investigating if you use OkHttp.

Pinning with OkHttp

Implementation with OkHttp is pretty straightforward with the CertificatePinner class.

CertificatePinner certPinner = new CertificatePinner.Builder()
.add("appmattus.com",
"sha256/4hw5tz+scE+TW+mlai5YipDfFWn1dqvfLG+nU7tq1V8=")
.build();

OkHttpClient okHttpClient = new OkHttpClient.Builder()
.certificatePinner(certPinner)
.build();

OkHttp has offered certificate pinning since OkHttp 2.1. Unfortunately early versions suffer from a Vulnerability in OkHttp’s Certificate Pinner so ensure you use at least OkHttp 3.2.0 or OkHttp 2.7.5.

Pinning with Retrofit

With Retrofit being built on top of OkHttp, configuring it for pinning is as simple as setting up an OkHttpClient as shown above and supplying that to your Retrofit.Builder.

Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://appmattus.com")
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build();

Pinning with Picasso

Picasso, as with Retrofit, is just a matter of configuring the downloader.

If you use OkHttp then you just provide the configured OkHttpClient. Currently Picasso 2 doesn’t support OkHttp 3 out of the box so you may need to use the Picasso 2 OkHttp3 Downloader.

Picasso picasso = new Picasso.Builder(getApplicationContext())
.downloader(new OkHttpDownloader(okHttpClient))
.build();
Picasso.setSingletonInstance(picasso);

The implementation with the UrlConnectionDownloader is slightly more work but you can implement a similar technique as shown for Volley by overloading the openConnection method of the downloader and overriding the HostnameVerifier.

Pinning with HttpUrlConnection

Firstly if you are still using HttpUrlConnection consider upgrading to OkHttp. The version built into Android, naturally, is a fixed version so you won’t get any security updates or bug fixes.

In the Android training document, Security with HTTPS and SSL, the implementation suggested is based off pinning your certificates through a custom TrustManager and SSLSocketFactory. However as with the other APIs presented here I will show you how to pin against the SPKI instead.

A more elegant solution is put forward by PayPal in Key Pinning in Mobile Applications however this suffers from the security flaw discussed in An Examination of Ineffective Certificate Pinning Implementations. The code proposed checks the pins against httpsUrlConnection.getServerCertificates() which returns a list of certificate suggestions from the server and are not necessarily trusted by the device.

Instead the certificates must be sanitised using X509TrustManagerExtensions introduced in API 17 to return a trusted chain back to the devices trust store that is checked instead. Below is a modified version doing just this along with base 64 encoding for the pins to match the other APIs presented here.

private void validatePinning(
X509TrustManagerExtensions trustManagerExt,
HttpsURLConnection conn, Set<String> validPins)
throws SSLException {
String certChainMsg = "";
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
        List<X509Certificate> trustedChain =
trustedChain(trustManagerExt, conn);
        for (Certificate cert : trustedChain) {
byte[] publicKey = cert.getPublicKey().getEncoded();
md.update(publicKey, 0, publicKey.length);
            String pin = Base64.encodeToString(md.digest(),
Base64.NO_WRAP);
            certChainMsg += "    sha256/" + pin + " : " +
cert.getSubjectDN().toString() + "\n";
            if (validPins.contains(pin)) {
return;
}
}
} catch (NoSuchAlgorithmException e) {
throw new SSLException(e);
}
    throw new SSLPeerUnverifiedException("Certificate pinning " +
"failure\n Peer certificate chain:\n" + certChainMsg);
}
private List<X509Certificate> trustedChain(
X509TrustManagerExtensions trustManagerExt,
HttpsURLConnection conn) throws SSLException {
Certificate[] serverCerts = conn.getServerCertificates();
X509Certificate[] untrustedCerts = Arrays.copyOf(serverCerts,
serverCerts.length, X509Certificate[].class);
    String host = conn.getURL().getHost();
try {
return trustManagerExt.checkServerTrusted(untrustedCerts,
"RSA", host);
} catch (CertificateException e) {
throw new SSLException(e);
}
}

This would then be called as follows:

TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
// Find first X509TrustManager in the TrustManagerFactory
X509TrustManager x509TrustManager = null;
for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) {
if (trustManager instanceof X509TrustManager) {
x509TrustManager = (X509TrustManager) trustManager;
break;
}
}
X509TrustManagerExtensions trustManagerExt =
new X509TrustManagerExtensions(x509TrustManager);
...
URL url = new URL("https://www.appmattus.com/");
HttpsURLConnection urlConnection =
(HttpsURLConnection) url.openConnection();
urlConnection.connect();
Set<String> validPins = Collections.singleton
("4hw5tz+scE+TW+mlai5YipDfFWn1dqvfLG+nU7tq1V8=");
validatePinning(trustManagerExt, urlConnection, validPins);

The call to urlConnection.connect() performs the SSL handshake however does not transmit any data until you call urlConnection.getInputStream().

Pinning with Volley

The usual suggested route for pinning with Volley is to pin against certificates as shown in Public Key Pinning with Volley Library. This involves creating a custom SSLSocketFactory which adds in an inherent risk of introducing other security vulnerabilities.

The Github project Public Key Pinning with Android Volley library shows how you setup an SSLSocketFactory to pin against the SPKI however suffers from security issues such as using the certificates returned by the server without cleaning those first, as outlined above for HttpUrlConnection.

There is an alternative technique that can be used that I have not seen documented elsewhere but on brief testing with mitmproxy looks to work well for default implementations. If you look at the interface of an HttpsUrlConnection along with the SSLSocketFactory you will also see a HostnameVerifier. This class is used to verify that the hostname in the URL matches that of the peer certificates. This relies on good implementations of SSLSocketFactory that call the HostnameVerifier however this is not always the case as highlighted in Java/Android SSLSocket Vulnerable to MitM Attacks so it is left as an exercise for the reader to validate their own implementation.

Overriding the HostnameVerifier can be achieved with the following:

RequestQueue requestQueue = Volley.newRequestQueue(appContext,
new HurlStack() {
@Override
protected HttpURLConnection createConnection(URL url) throws IOException {
HttpURLConnection connection = super.createConnection(url);

if (connection instanceof HttpsURLConnection) {
HostnameVerifier delegate =
urlConnection.getHostnameVerifier();
            HostnameVerifier pinningVerifier =
new PinningHostnameVerifier(delegate);

urlConnection.setHostnameVerifier(pinningVerifier);
}

return connection;
}
});
...
public static class PinningHostnameVerifier
implements HostnameVerifier {
private final HostnameVerifier delegate;

private PinningHostnameVerifier(HostnameVerifier delegate) {
this.delegate = delegate;
}

@Override
public boolean verify(String host, SSLSession sslSession) {
if (delegate.verify(host, sslSession)) {
try {
validatePinning(sslSession.getPeerCertificates(),
host, validPins);
return true;
} catch (SSLException e) {
throw new RuntimeException(e);
}
}

return false;
}
}

The above can be used on a plain implementation of HttpUrlConnection also if you don’t want to setup

Pinning with Apache HttpClient

As with HttpUrlConnection you really shouldn’t be using HttpClient anymore especially with the Apache HTTP Client Removal in Android 6.0. Android was frozen at Apache HttpClient v4.0 since API 1.

The same technique shown for Volley also works for Apache HttpClient with some tweaks to the PinningHostnameVerifier:

SSLSocketFactory socketFactory = (SSLSocketFactory) client
.getConnectionManager()
.getSchemeRegistry()
.getScheme("https")
.getSocketFactory();

X509HostnameVerifier delegate = socketFactory.getHostnameVerifier();

X509HostnameVerifier pinningHostnameVerifier =
new PinningHostnameVerifier(delegate);

socketFactory.setHostnameVerifier(pinningHostnameVerifier);

Pinning in WebViews

WebViews are tricky, not least because there is no perfect way to implement pinning them except with Android N (see above). The best you can do is override shouldInterceptRequest and implement the network calls yourself using one of the above methods, however this only intercepts GET requests so if your WebViews use POST requests then you are out of luck. Android-SSL-Pinning-WebViews shows an example of doing this.

Testing

Given you have implemented SSL Pinning how do you ensure that your implementation actually works?

mitmproxy

This is where a tool such as mitmproxy comes into play. This is a man-in-the-middle proxy for HTTP and HTTPS with an interactive console interface that allows network traffic to be intercepted, inspected, modified and replayed.

Start mitmproxy using the command below and install mitmproxy’s root cert by visiting http://mitm.it/ on your device.

mitmproxy --add-upstream-certs-to-client-chain --insecure

If when you run your app the connection is refused because of a pinning failure then you are all set. In this scenario you also shouldn’t see your requests in the mitmproxy console window. Note that apps on Android N devices do not trust user installed certificates by default so in debug mode you have to enable this through the Network Security Configuration.

Alternative tools

Another tool worth investigating is SSLsplit — transparent SSL/TLS interception. On a rooted device you can also try apps such as Android-SSL-TrustKiller or JustTrustMe to see if your SSL pinning can be beaten.

Conclusions

SSL pinning helps build secure mobile apps but it will not secure connections if the pinned host is compromised. It mainly protects the client however it also helps protect your servers by making it harder for hackers to snoop on the traffic and figure out your API and exploit other security holes.

You should never trust the client talking to your server especially when it is possible to circumvent SSL pinning on a rooted device with such ease. With a little more technical know-how a user of your app can reverse engineer your app to disable it so they will be able to inspect the traffic. As an example see Bypassing Certificate Pinning on Android for fun and profit and Bypassing SSL Pinning on Android via Reverse Engineering.

Techniques such as code obfuscation can be used but will only make a hacker’s job harder not impossible. For more details on some of these techniques read Securing Android LVL Applications.

#buildsecureapps