Certificate Pinning / Security

Certificate Pinning – Part 3

Recently at Mobiconf in Kraków I saw Marcos Placona give an excellent talk about making apps more secure. One of the subjects that he covered was that of certificate pinning which can really help to secure your API calls to your server. In this short series we’ll take a look at what certificate pinning is, why it can be a good thing, some pitfalls and, of course, how to implement it on Android.

Previously we have conveyed the basics of SSL cryptography in other to understand why certificate pinning may be necessary, and looked at some things which require consideration before deciding whether certificate pinning is a good idea for your app, and in this final article we’ll look at that practicalities of implementing it.

Let’s start with a really simple app which makes a simulated API call to https://blog.stylingandroid.com. This will actually return the homepage of this blog but if we make the call using the HEAD HTTP method then it will just return the headers without the HTML body, which is more than sufficient for this example.

The Activity consists of a Button which will trigger the network call, and a TextView which will display the result:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val httpClient = HttpClient()

        button_go.setOnClickListener {
            httpClient.load(::display)
        }
    }

    private fun display(text: String) {
        runOnUiThread {
            text1.text = text
        }
    }
}

The HttpClient object performs the necessary network call:

class HttpClient(
        private val okHttpClient: OkHttpClient = createOkHttpClient()
) {

    fun load(display: (String) -> Unit) {
        val request = Request.Builder()
                .url("https://blog.stylingandroid.com")
                .head()
                .build()
        okHttpClient.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                display(e.toString())
            }

            override fun onResponse(call: Call, response: Response) {
                display("request: $request \nResponse Code: ${response.code()} Protocol: ${response.protocol()}")
            }
        })
    }
}

private fun createOkHttpClient(): OkHttpClient {
    return OkHttpClient.Builder().apply {
        addInterceptor(HttpLoggingInterceptor(Logger { println(it) }).setLevel(Level.BASIC))
    }.build()
}

I’m using OkHttp for the network call because it is a mature, robust, and extremely well implemented HTTP client library. Not only that but it can be used as the underlying HTTP layer for other popular libraries such as Retrofit and Picasso, so the techniques that will cover here can be used with any such libraries with relative ease. Also, as we shall see, it makes certificate pinning really simple.

The createOkHttpClient() function is where our OkHttpClient instance is created, and will be important later on.

So this simple app will make a network call to the server. There is a certificate in place on my server (actually it’s behind a Cloudflare CDN whose certificate is being used) which has a trust chain terminating in the root certificate that is one of those included in the trusted roots included in the Android OS, so all of the certificate validation is successful when we use the https protocol.

Pinning this certificate within our app is really easy of we are targeting API 24 or later which introduced Network Security Configuration. We first need to enable network security configuration in the manifest, and point it to the correct config file:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  package="com.stylingandroid.certificatepinning">

  <uses-permission android:name="android.permission.INTERNET" />

  <application
    android:allowBackup="false"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:networkSecurityConfig="@xml/network_security"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme"
    tools:ignore="GoogleAppIndexingWarning"
    tools:targetApi="n">
    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>

</manifest>

The config file itself is an XML file:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <domain-config>
    <domain includeSubdomains="true">stylingandroid.com</domain>
    <pin-set>
      <pin digest="SHA-256">htJkaSJB+j8Ckv7ovGieQJYqyV/M4K7YRt4je18A7T4=</pin>
      <!-- backup pin -->
      <pin digest="SHA-256">x9SZw6TwIqfmvrLZ/kz1o0Ossjmn728BnBKpUFqGNVM=</pin>
    </pin-set>
  </domain-config>
</network-security-config>

Here we define the domain that we want to pin the certificate for as stylingandroid.com and specify that it includes sub-domains which will mean that this will be applied to all sub-domains including blog.stylingandroid.com.

Within this we declare a pin-set which is comprised of 2 hashes of the pinned certificates. The first is for the certificate itself, and the second is for the intermediate certificate which was used to sign it. The reasoning behind this backup pinned certificate was explained in the previous article. We’ll look at how to determine the hashes themselves shortly.

That’s all the is to it! Our network call will now succeed, but if we were to change a single digit of each of the hashes then our network call will fail because none of the certificates in the trust chain will match either of these pinned hashes.

That’s fine if we’re minSdkVersion="24" or later, but pre-Nougat we need to implement this manually. OkHttp has got your back. Seriously, if you aren’t using OkHttp for your network calls, you’re probably doing it wrong.

When we create our OkHttpClient instance we can add a CertificatePinner which does everything for us:

private const val DOMAIN = "*.stylingandroid.com"
private const val PIN = "sha256/htJkaSJB+j8Ckv7ovGieQJYqyV/M4K7YRt4je18A7T4="
private const val BACKUP = "sha256/x9SZw6TwIqfmvrLZ/kz1o0Ossjmn728BnBKpUFqGNVM="

private fun createOkHttpClient(): OkHttpClient {
    return OkHttpClient.Builder().apply {
        addInterceptor(HttpLoggingInterceptor(Logger { println(it) }).setLevel(Level.BASIC))
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            certificatePinner(CertificatePinner.Builder()
                    .add(DOMAIN, PIN)
                    .add(DOMAIN, BACKUP)
                    .build()
            )
        }
    }.build()
}

All we need to do is provide hashes for the certificate itself and the backup, then OkHttp will perform all of the necessary certificate validation for us. I have wrapped this inside a version check block so that we only use this on pre-Nougat devices, and instead rely upon the network security configuration that we added previously for devices running Nougat or later.

That’s the actual implementation complete. All that remains is how we can obtain the correct hash to use for our pinned certificates. If we are having to implement things manually using OkHttp, then most of the work is done for us. Use dummy hashes (you can copy the hash for the main certificate from these code snippets and don’t provide a backup) and the network call will fail if you then connect to a server with a different certificate. But the Logcat messages will give us details of the trust chain (I have used dummy keys which are different from those in the above snippets to generate this error message):

I: <-- HTTP FAILED: javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
I:   Peer certificate chain:
I:     sha256/htJkaSJB+j8Ckv7ovGieQJYqyV/M4K7YRt4je18A7T4=: CN=ssl378323.cloudflaressl.com,OU=PositiveSSL Multi-Domain,OU=Domain Control Validated
I:     sha256/x9SZw6TwIqfmvrLZ/kz1o0Ossjmn728BnBKpUFqGNVM=: CN=COMODO ECC Domain Validation Secure Server CA 2,O=COMODO CA Limited,L=Salford,ST=Greater Manchester,C=GB
I:     sha256/58qRu/uxh4gFezqAcERupSkRYBlBAvfcw7mEjGPLnNU=: CN=COMODO ECC Certification Authority,O=COMODO CA Limited,L=Salford,ST=Greater Manchester,C=GB
I:   Pinned certificates for blog.stylingandroid.com:
I:     sha256/htJkaSJB+j8Ckv7ovGieQJYqyV/M4K7YRt4je18A7T0=
I:     sha256/x9SZw6TwIqfmvrLZ/kz1o0Ossjmn728BnBKpUFqGNVk=

We can see the hashes of the certificates in trust chain in the error message and these can be used for our pinned hashed. Please compare these to those used in the earlier snippets, and you’ll see how I obtained them. Once again, the value of OkHttp is apparent – because of its really useful logging, it becomes easy to work out the hashes that we need to use for our pinning.

Certificate pinning is quite straightforward to implement using a hybrid solution of Android network security configuration and OkHttp. By using them we can avoid some of the problems which can arise from misunderstanding the Java network APIs which can result in unexpected security loopholes.

The source code for this article is available here.

© 2018, Mark Allison. All rights reserved.

Copyright © 2018 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

1 Comment

  1. Hello, I have read your article is great!!! thanks . I have a question about
    private const val PIN = “sha256/htJkaSJB+j8Ckv7ovGieQJYqyV/M4K7YRt4je18A7T4=”
    private const val BACKUP = “sha256/x9SZw6TwIqfmvrLZ/kz1o0Ossjmn728BnBKpUFqGNVM=”
    here do you use public key of which certificate level leaf — Intermediate — root i

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.