DEV Community

Cover image for Securing HTTPS with Certificate Pinning on Android
Paulo Renato
Paulo Renato

Posted on • Updated on • Originally published at blog.approov.io

Securing HTTPS with Certificate Pinning on Android

In a previous article we saw how we could steal an API key by performing a man in the middle (MitM) attack to intercept the https traffic between the mobile app and the API server. In this article we will learn how to mitigate this type of attack by using a technique known as certificate pinning.

In order to demonstrate how to use certificate pinning for protecting the https traffic between your mobile app and your API server, we will use the same Currency Converter Demo mobile app that I used in the previous article.

In this article we will learn what certificate pinning is, when to use it, how to implement it in an Android app, and how it can prevent a MitM attack.

What is Certificate Pinning?

Certificate pinning is the mechanism of associating a domain name with an expected SSL/TLS certificate, technically and more accurately known as an X.509 certificate.

Whenever the user clicks on a link, the device needs to establish a connection with the server hosting that domain name, and for this to happen, a TLS handshake takes place in order that both parties can exchange messages, so that they can verify each other, establish the encryption algorithms to use, and finally to set the session keys to be used thereafter. During the TLS handshake, when the device receives the server certificate, it only establishes the connection if it trusts on that specific certificate, hence it is said that the connection is pinned.

What to pin?

The process of performing certificate pinning can be achieved by pinning against any of the certificates presented in the chain of trust for the domain of the API server as shown below. Normally it is preferable to pin against the leaf certificate, that in the below picture is referred to as the end-entity certificate.

Certificate chain of trust

Source: Wikipedia — chain of trust: image originally via Gary Stevens of HostingCanada.org

The easiest way to pin is to use the server’s public key or the hash of that public key, The hashed public key is the most flexible and maintainable approach since it allows certificates to be rotated in the server, by signing the new one with the same public key. Thus the mobile app does not have to be updated with a new pin because the hash for the public key of the new certificate will continue to match the pin provided in the network security config file. We will see an example of this later when we talk about how to setup certificate pinning.

Why do we need Certificate Pinning?

While https gives you confidentiality, integrity and authenticity in the communication channel between the mobile app and the API server, certificate pinning will protect this same guarantees from being broken.

To prevent trust based assumptions

Incorrectly issuing leaf certificates to the wrong domain names by Root and Intermediate Certificate Authorities (CAs) would allow an attacker to intercept any https traffic using them, without the end user noticing anything.

This is possible because any mobile device comes pre-installed with the root certificates for all known Root CA's, which are then trusted by all other Intermediate CA's, thus we now have a chain of trust that are used by the devices to validate the certificates presented in the handshakes that take place to establish a secure connection with an API server. This means that if any of the CA’s in the chain get compromised or issue certificates incorrectly, the chain of trust is tainted and broken, just like in the famous cases of DigiNotar, GlobalSign and Comodo.

To protect against use in hostile environments

A good example of a hostile environment is public wifi, where users can be tricked by an attacker into installing a self signed root certificate authority into the trusted store of the device as a requirement for them to have internet for free. This will allow the attacker to perform a MitM attack and intercept, modify or redirect all https traffic, because the device will now accept all intercept traffic which is now signed by the root CA of the attacker - now trusted by the device. From Android 7 onwards, the operating system no longer trusts user supplied certificates unless the app developer explicitly opts-in to trust them in the network security config file. Even with this huge improvement in security, it is still important to pin the leaf certificate to protect against certificates issued by an attacker’s self signed root certificate, when the developer have opt-in to trust in user provided certificates, and to protect against compromised CA’s, that incorrectly have issued certificates to an attacker.

Other hostile situations certificate pinning can protect us from are DNS cache poisoning and DNS spoofing. In simple terms this is where your device asks where it can find a certain domain name but it gets the wrong reply, for example that google.com is at an attacker’s servers and not at Google’s servers, as in this incident. This is not an isolated incident and DNS Hijacking attacks are becoming more frequent and with harmful consequences.

When to use Certificate Pinning?

The OWASP page on certificate pinning has a good answer for this question:

You should pin anytime you want to be relatively certain of the remote host's identity or when operating in a hostile environment. Since one or both are almost always true, you should probably pin all the time.

So if your mobile app is dealing with Personal Identifiable Information (PII) and/or other sensitive data, which is pretty much true in any mobile app, then you should absolutely pin.

Preventing MitM attacks with Certificate Pinning

Man in the Middle Attack

Now that you know what certificate pinning is and when you should use it, it’s time to learn how to implement it in an Android mobile app. For this we will use the Currency Converter Demo app, and if you remember from the previous article, the mobile app retrieves the currency rates directly from a free API, that is rate limited, and requires an API key to access it. Therefore we note that if we needed to replace the API key we would need to release a new version of the mobile app and expect all users to update it.

A smart and more secure approach is to always delegate any access to third party services to an API server under your control. This approach allows us to keep all secrets secured in a private place, instead of having them shipped with the mobile app and making them vulnerable to extraction by reverse engineering techniques or by a MitM attack.

The Currency Converter Demo app has been upgraded to extract all the currency conversion logic to a dedicated API server which is under our control, and at the same time allows us to have more control over the certificates we will pin against.

How to Implement Certificate Pinning

Now that you are aware of the changes made to the Currency Converter Demo app used in the previous article, it is time to see how certificate pinning was implemented on it, and if you want to take a closer look, just clone the project from Github:

git clone --branch 0.2.0 [https://github.com/approov/currency-converter-demo.git](https://github.com/approov/currency-converter-demo.git) && cd currency-converter-demo
Enter fullscreen mode Exit fullscreen mode

Implementing certificate pinning on Android API level 24 and above

From Android Nougat onwards, implementing certificate pinning for any mobile app that targets API level 24 and above was made easier with the introduction of the network security config file, as detailed in this blog article by Google.

So, all that is needed is to create this file src/main/res/xml/network_security_config.xml:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>

    <!-- Official Android N API -->
    <!--https://android-developers.googleblog.com/2016/07/changes-to-trusted-certificate.html-->
    <domain-config>
        <domain>currency-converter-demo.pdm.approov.io</domain>
        <trust-anchors>
            <!--<certificates src="user" />-->
            <certificates src="system" />
        </trust-anchors>
        <pin-set>
            <!-- Pin for: currency-converter-demo.pdm.approov.io -->
            <pin digest="SHA-256">qXHiE7hFX2Kj4ZCtnr8u8yffl8w9CTv6kE0U5j0o1XY=</pin>

            <!-- Backup Pin for: currency-converter-demo.pdm.approov.io -->
            <pin digest="SHA-256">47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=</pin>
        </pin-set>

        <!-- TrustKit Android API -->
        <!-- enforce pinning validation -->
        <trustkit-config enforcePinning="true" disableDefaultReportUri="true">
            <!-- Add a reporting URL for pin validation reports -->
            <report-uri>https://report.pdm.approov.io/pinning-violation/report</report-uri>
        </trustkit-config>
    </domain-config>

</network-security-config>
Enter fullscreen mode Exit fullscreen mode

In this file we can see a section dedicated to configure TrustKit, but using TrustKit in your mobile app is only necessary if you want to target devices below API level 24, where TrustKit assumes the role of ensuring that the connection is pinned against the correct certificate.

So now we need to include the network security config file in the AndroidManifest.xml by adding this line into the application tag:

<application
...
    android:networkSecurityConfig="@xml/network_security_config"
...
>
Enter fullscreen mode Exit fullscreen mode

If your mobile app is only targeting Android API level 24 or above, then you are done with your certification pinning implementation. Nowyou just need to rebuild your mobile app and try to MitM it, so that you can see how certificate pinning is protecting the secure communication channel between the mobile app and the API server.

Implementing certificate pinning below Android API level 24

Until now the certificate pinning has been agnostic of the http transport layer being used, but to handle certificate pinning below API level 24 we need to get our hands dirty. Specifically we need to code in the chosen http stack, with the risk that we introduce security flaws which will render certificate pinning useless and worst case turn https into an insecure channel.

In order to avoid all the pitfalls, bugs and security risks that we might introduce with our own implementation, it is best to delegate that responsibility to a community trusted package, and here is where the TrustKit package comes into play, ensuring a secure and well maintained certificate pinning implementation for your mobile app.

Adding TrustKit to an Android App

Unfortunately the README for TrustKit does not include instructions in how to use it with Volley, but it turns out that is not that hard.

So the Currency Converter Demo app is using the Google official Volley library with the request queue singleton pattern, which in their words allows for a more efficient handling of all network activity in the mobile app.

In order to add TrustKit into Volley we need to change the VolleyQueueSingleton class in this method:

public RequestQueue getRequestQueue() {
    if (requestQueue == null) {
        // getApplicationContext() is key, it keeps you from leaking the
        // Activity or BroadcastReceiver if someone passes one in.
        requestQueue = Volley.newRequestQueue(ctx.getApplicationContext());
    }
    return requestQueue;
}
Enter fullscreen mode Exit fullscreen mode

To look like this:

public RequestQueue getRequestQueue() {

    if (requestQueue == null) {

        Context context = ctx.getApplicationContext();

        // TRUSTKIT
        TrustKit.initializeWithNetworkSecurityConfiguration(context);

        String serverHostname = null;

        try {
            URL url = new URL(baseUrl);
            serverHostname = url.getHost();
            Log.i(LOG_TAG, "Server Hostname: " + serverHostname);
        } catch (MalformedURLException e) {
            Log.e(LOG_TAG, e.getMessage());
        }

        // TRUSTKIT
        requestQueue = Volley.newRequestQueue(context, new HurlStack(null, TrustKit.getInstance().getSSLSocketFactory(serverHostname)));
    }

    return requestQueue;
}
Enter fullscreen mode Exit fullscreen mode

So the main difference is in how we instantiate Volley through the VolleyQueueSingleton class. We need to go from instantiating it with only the current context to instantiating it with an additional second parameter to define the http stack we want to use. This in turn lets us define which socket implementation to use, namely the TrustKit one. This will allow TrustKit to take control of all network requests in order to perform the certificate pinning validation.

Before we were instatianting Volley from the VolleyQueueSingleton class like this:

requestQueue = Volley.newRequestQueue(ctx.getApplicationContext());
Enter fullscreen mode Exit fullscreen mode

Now with TrustKit we do it like this:

requestQueue = Volley.newRequestQueue(context, new HurlStack(null, TrustKit.getInstance().getSSLSocketFactory(serverHostname)));
Enter fullscreen mode Exit fullscreen mode

Install the app

Before you are able to build and install the mobile app, it’s necessary to first configure it with the correct API key for the new API server which is accepting requests for currency conversions in this endpoint. So if your curiosity was strong and you clicked on the endpoint link, you probably got an empty response. Remember that we need an API key to query this endpoint.

To set the API key for the mobile app, you will need to execute from the root of the Currency Converter Demo:

$ ./stack setup                                                      
'.env.example' -> '.env'
'./mobile-app/android/app/src/main/cpp/api_key.h.example' -> './mobile-app/android/app/src/main/cpp/api_key.h'
Enter fullscreen mode Exit fullscreen mode

As the output suggests, you now you have two new files, but for running the mobile app, we only care about this one ./mobile-app/android/app/src/main/cpp/api_key.h, which contains the API key to be sent in the header of each request to the API server:

#ifndef API_KEY_H
#define API_KEY_H "the-api-key-will-be-here"
#endif
Enter fullscreen mode Exit fullscreen mode

Time to build and install the Currency Converter demo app, and when you are done with it, you should be presented with this screen:

image2

Note for beginners: how to build and install an Android app is out of scope for this article, but you can refer to the official docs to learn how to do it.

A quick smoke test

Just to be sure that the app is working properly, let's do a quick smoke test by tapping on the convert button, after which we should get something like this:

image7

Certificate pinning in action

In order to validate that the certificate pinning implementation is working properly, we will perform a MitM attack against the mobile app.

Setup for the MitM Attack

Let’s start by following the Setup for the MitM Attack instructions from my previous article, and you can return here after the mitmproxy server is running and the mitmproxy setup in the mobile device is completed.

Perform a MitM attack to check that certificate pinning is working

If we install the app in a device pre Android 7, like one using Android 4.3, and we have followed all the steps in the Setup for the MitM Attack instructions, then we will see this:

image5

As we can see in the image, the exception message clearly states that the pin verification has failed, and even tells us the pins it used to perform the verification. Hurray, it’s working! Now, please do not display the exception message in a production app. I am doing it here to make easier to see that pinning is working.

The exception message does not tell us that it was TrustKit that has thrown an exception, but if you look into your mitmproxy server, you should see a report like this being sent with the certificate pinning failure:

Now if we try the MitM attack with a device using Android 7 or above we will see a different error:

image4

Here we have a different message, which instead of mentioning a failure in the pin validation, says that it failed the certificate validation. If you remember I said earlier that from API level 24 onwards, Android does not trust in user supplied certificates any more. Therefore when we are MitM attacking the https connection, the handshake with the server fails when the Android OS is trying to establish the trust chain for the certificate, by validating each one with the system trust store. Since the mitmproxy certificate we provided is stored in the user trust store, it will not even arrive to a point where we are able to validate the pins for the certificate.

Now if you enable user trust anchors in the network security file, rebuild and install the app on your device, then you should see a new error message:

image9

Now we get the pin validation error because once we have enabled Android to trust in user provided certificates and so the trust chain can be established. However, when the certificate pinning validation is performed it fails as expected because even though the user has been tricked by an attacker into installing a custom certificate, the app will not proceed with the connection to the API server, therefore protecting the user from the attacker.

A word of caution here: do not enable trust anchors for user provided certificates, but if there are good reasons why you really need to do it, for example for internal mobile apps used in networks with a firewall or that are behind a proxy, then please do not forget to pin against the firewall or proxy certificate.

Conclusion

In this article you have learned that certificate pinning is the act of associating a domain name with their expected X.509 certificate, and that this is necessary to protect trust based assumptions in the certificate chain. Mistakenly issued or compromised certificates are a threat, and it is also necessary to protect the mobile app against their use in hostile environments like public wifis, or against DNS Hijacking attacks.

You also learned that certificate pinning should be used anytime you deal with Personal Identifiable Information or any other sensitive data, otherwise the communication channel between the mobile app and the API server can be inspected, modified or redirected by an attacker.

Finally you learned how to prevent MitM attacks with the implementation of certificate pinning in an Android app that makes use of a network security config file for modern Android devices, and later by using TrustKit package which supports certificate pinning for both modern and old devices.

I hope that by now you are convinced that certificate pinning is very important to implement in your mobile app in order to strengthen and harden its security.

So, see you in my next article, which will be about bypassing certificate pinning in some specific scenarios. Wait, I hear you cry, it can be bypassed? Well you need to be patient and wait for my next article to understand the limitations and what you can do about it. (Spoiler alert: there is a happy ending!) .

Oldest comments (0)