SSL certificate pinning on iOS using TrustKit

Dmitry Fink
July 18, 2017

We have already covered why certificate pinning in mobile apps is important and have shown how to implement it both in iOS and Android. For the sake of simplicity, we had to omit a lot of nasty details and corner cases from our example. In the real world, the code might get complex with time very quickly, when you have to support various legacy iOS platforms, various popular networking libraries out there etc. Luckily, nice folks at DataTheorem have created and open-sourced a framework for SSL pinning which eliminates that simplifies most of it. The framework, called TrustKit, and makes it very easy to integrate pinning into your mobile application. In the following tutorial we'll show how to use it on iOS.

Installing the framework

It is trivial to install the TrustKit framework using CocoaPods.

Just run the following to your Podfile:

pod 'TrustKit'

and run:

pod install

Alternatively, TrustKit is available through Carthage as well.

Add the following line to your Cartfile:

github "datatheorem/TrustKit"

and run:

carthage build --platform iOS

Preparing the pins

As we explained in our introduction tutorial, you can pin either the certificate itself or the public key from within the certificate. The latter is preferred, since on many occasions, for security reasons, the certificates are being rotated regularly and we will have to re-deploy our application with the new certificate, with a risk of some users becoming locked out. We've also shown that it is better for the app to store just the hash of the public key, not the key itself. TrustKit takes the same approach. For each domain it expects to have a list of hashes of public keys to pin. It does however expects them in a base64 encoded format, so for convenience reason a special tool is provided within the TrustKit package to help prepare them.

As with the previous example, we will use as an example. Lets first extract the certificate from the domain:

openssl s_client -connect -showcerts < /dev/null | openssl x509 -outform DER > google.der

Now lets run the provided helper script to extract the hash of the public key:

python --type DER ../google.der
In our example we are using a binary DER format when working with certificates. You have your certificate provided to you in a different text PEM format. In that case, simply omit the --type DER part and call the script on your PEM file directly.

The result is going to be the following:

subject= /C=US/ST=California/L=Mountain View/O=Google Inc/
issuer= /C=US/O=Google Inc/CN=Google Internet Authority G2
SHA1 Fingerprint=FB:BD:7E:87:A8:A6:EB:FC:3E:E0:BE:F8:2E:3A:EB:9C:7D:86:7F:AA
kTSKPublicKeyHashes: @[@"U9GGUC1+PGI7SLb8XVVuuBPLkQKbQ2LnfU5Wfe7WAAg="] // You will also need to configure a backup pin
kTSKPublicKeyAlgorithms: @[kTSKAlgorithmRsa2048]

Note that the output of the script has both the hash and the algorithm used. This is in fact a nice touch, TrustKit supports several algorithms and will autodetect and handle it automatically for us. Notice the output of that script is even giving us Objective-C elements so we can copy paste them into our code directly. Unfortunately we will be using Swift for our example, but TrustKit gets extra points for this.

Initializing TrustKit

Now its time to initialize the framework:

        let trustKitConfig = [
            kTSKSwizzleNetworkDelegates: false,
            kTSKPinnedDomains: [
                "": [
                    kTSKIncludeSubdomains : true,
                    kTSKPublicKeyAlgorithms: [kTSKAlgorithmRsa2048],
                    kTSKPublicKeyHashes: [
                    ],]]] as [String : Any]
        TrustKit.initialize(withConfiguration: trustKitConfig)

The pinned domains is an array, so we can actually pin several different domains in our application, each with a specific set of hashes and settings.

Why do we have 2 hashes in the hashes list? TrustKit will actually fail and not let you start with one. The idea is you have to create a backup key, so in case your primary gets compromised, you will have a way to replace the certificates on your server without locking out your users.

Validating secure connections

Now that everything is set up, we are ready to use the TrustKit mechanism to actually validate each and every connection our application is going to open. In order to do this, we will make sure our URLSession delegate implements the didReceive challenge method:

    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) {
        // Let TrustKit handle it
        TSKPinningValidator.handle(challenge, completionHandler: completionHandler)

Thats it, we are done. Every time our URLSession will try to establish an SSL connection to or one of its subdomains, TrustKit will make sure to extract the certificate and the public key, hash the key and compare it to the list we've passed during initialization. And it will work on all iOS/watchOS/tvOS and macOS versions.