SSL certificate pinning in iOS applications

Dmitry Fink
June 15, 2017

What is certificate pinning?

In this day and age more and more user data is stored electronically. Users are expecting end-to-end security from every application they are installing on their devices. Application developers too, are seeking to secure all communications between their apps and backends in order to prevents hackers from reverse engineering their protocols and getting access to their databases.

The most basic form of security when transferring data between the application and the service backend is SSL/TLS encryption, and it is very common for developers today to switch their traffic to https and declare their communications as secure. In fact mobile platforms today make it really hard for developers not to use https. That by itself, however, is not enough. Encryption is useless when communicating parties can not validate the identity of their piers.

There are mechanisms that try to solve the identity problem by means of chain of trust, having certificate authorities sign the certificates and having operating systems validating the certificates/identities when establishing SSL connections, however these are not fool proof and known to be easily bypassed in hostile environments. Man In The Middle Attack(MITM) is always a possibility when you are relying on others to validate the address and the identity of the second party.

The idea of certificate pinning is to stop relying on these third-parties (certificate authorities, operating systems) to validate the identity of the backend server and to take matter into our own hands. Knowing in advance the certificate of the server our application is communication with, we can hard-wire it into the application itself and refuse communicating unless it is a perfect match.

At Bugsee we rely heavily on certificate pinning. The nature of the data we collect might be sensitive at times, after all we do record the last minute of screen video, console logs, network traffic and other activity that happened in our customers apps right before a bug or a crash. Even though we do have mechanisms to remove private data the logs and to hide any sensitive views from the videos, we still take security seriously and make sure the data from SDKs will only arrive at our servers and could not be intercepted in transit.

How to pin?

The basic idea is not to reinvent the wheel and keep using the existing secure channels and protocols, but to harden the security further by introducing an extra identity check performed by the application itself. The application will obtain the identity of the server upon connection and compare it to the one hardcoded in the application itself. We may even decide to white list a set of such identities within the application if its required by our environment. As to what exactly to pin, there are two options available:

  • Certificate: This is easy to obtain and implement, however comes with a major drawback. Certificates do tend to expire every year or two. If we control the certificate, we must update the application and add the new future certificate to our pin set prior to switching the on the server. It may still not be enough, as user adoption of the new versions of the applications is never 100%, there will be a chance users with older versions of the application will get locked out, this has to be handled explicitly. Some services may decide to rotate certificates even more frequently (Google rotates its SSL certificates once a month). Playing constant catch up and being forced to release a new version of the application just to update the pinned certificates might not be a good idea in such cases.
  • Public key: Even though certificates are being rotated, the underlying public key within the certificate remains the same. Or at least you have the option to keep the same one. Google's one does remain static. Therefore pinning the key makes the design more flexible, but a bit trickier to implement, as now we have to extract the key from the certificate, both at pinning time and at every connection.
In either case, we will not store the actual certificates and/or keys in the application, we will create a sha256 hashes and store them instead. This has several advantages, its easier to manage due to size and it allows shipping an application with a hash of a future (or a backup) certificate or key without exposing them ahead of time.

Obtaining the certificate and the public key

For the sake of this example we will try to pin www.google.com and use it from our example application.

Obtain the certificate and create a sha256 hash from it, for convenience we will encode the hash with base64:

openssl s_client -connect www.google.com:443 -showcerts < /dev/null | openssl x509 -outform DER > google.der
python -sBc "from __future__ import print_function;import hashlib;print(hashlib.sha256(open('google.der','rb').read()).digest(), end='')" | base64
KjLxfxajzmBH0fTH1/oujb6R5fqBiLxl0zrl2xyFT2E=

Extract the public key from the certificate and hash is as well:

openssl x509 -pubkey -noout -in google.der -inform DER | openssl rsa -outform DER -pubin -in /dev/stdin 2>/dev/null > googlekey.der
python -sBc "from __future__ import print_function;import hashlib;print(hashlib.sha256(open('googlekey.der','rb').read()).digest(), end='')" | base64
4xVxzbEegwDBoyoGoJlKcwGM7hyquoFg4l+9um5oPOI=

Implementing certificate pinning on iOS

Now that we have hashes of both the certificate and the underlying public key, lets implement the checking every time the connection is being stablished.

First we will implement a URLSessionPinningDelegate class with the following code:

import Foundation
import Security
class URLSessionPinningDelegate: NSObject, URLSessionDelegate {
    let pinnedCertificateHash = "KjLxfxajzmBH0fTH1/oujb6R5fqBiLxl0zrl2xyFT2E="
    let pinnedPublicKeyHash = "4xVxzbEegwDBoyoGoJlKcwGM7hyquoFg4l+9um5oPOI="
    let rsa2048Asn1Header:[UInt8] = [
        0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
        0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00
    ]
    private func sha256(data : Data) -> String {
        var keyWithHeader = Data(bytes: rsa2048Asn1Header)
        keyWithHeader.append(data)
        var hash = [UInt8](repeating: 0,  count: Int(CC_SHA256_DIGEST_LENGTH))
        keyWithHeader.withUnsafeBytes {
            _ = CC_SHA256($0, CC_LONG(keyWithHeader.count), &hash)
        }
        return Data(hash).base64EncodedString()
    }
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) {
        if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) {
            if let serverTrust = challenge.protectionSpace.serverTrust {
                var secresult = SecTrustResultType.invalid
                let status = SecTrustEvaluate(serverTrust, &secresult)
                if(errSecSuccess == status) {
                    print(SecTrustGetCertificateCount(serverTrust))
                    if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) {
                        // Certificate pinning, uncomment to use this instead of public key pinning
//                        let serverCertificateData:NSData = SecCertificateCopyData(serverCertificate)
//                        let certHash = sha256(data: serverCertificateData as Data)
//                        if (certHash == pinnedCertificateHash) {
//                            // Success! This is our server
//                            completionHandler(.useCredential, URLCredential(trust:serverTrust))
//                            return
//                        }
                        // Public key pinning
                        let serverPublicKey = SecCertificateCopyPublicKey(serverCertificate)
                        let serverPublicKeyData:NSData = SecKeyCopyExternalRepresentation(serverPublicKey!, nil )!
                        let keyHash = sha256(data: serverPublicKeyData as Data)
                        if (keyHash == pinnedPublicKeyHash) {
                            // Success! This is our server
                            completionHandler(.useCredential, URLCredential(trust:serverTrust))
                            return
                        }
                    }
                }
            }
        }
        // Pinning failed
        completionHandler(.cancelAuthenticationChallenge, nil)
    }
}

Now when we create URLSession, we must set this as the delegate to make sure connection is verified properly:

        if let url = NSURL(string: "https://www.google.com/") {
            let session = URLSession(
                configuration: URLSessionConfiguration.ephemeral,
                delegate: URLSessionPinningDelegate(),
                delegateQueue: nil)
            let task = session.dataTask(with: url as URL, completionHandler: { (data, response, error) -> Void in
                if error != nil {
                    print("error: \(error!.localizedDescription))")
                } else if data != nil {
                    if let str = NSString(data: data!, encoding: String.Encoding.utf8.rawValue) {
                        print("Received data:\n\(str)")
                    }
                    else {
                        print("Unable to convert data to text")
                    }
                }
            })
            
            task.resume()
        }
        else {
            print("Unable to create NSURL")
        }

Summary

Even though SSL/TLS is considered to be mandatory when implementing client/server communications these days, relying on chain of trust to verify the identity of the server is not enough, as MITM (Man In The Middle) attacks are still a possibility. It is much safer to rely on additional layer of protection since we have the luxury to pin the exact certificate or the public key we are expecting on the other side. We've learned how to extract a server key as well as how to implement the pinning on iOS. Stay tuned, in our next tutorial we will cover the methods to implement certificate pinning on Android.

For convenience, full source of the example is available on Github.