Swift Talk #57

Certificate Pinning

with special guest Rob Napier

This episode is freely available thanks to the support of our subscribers

Subscribers get exclusive access to new and all previous subscriber-only episodes, video downloads, and 30% discount for team members. Become a Subscriber

Today we're joined by Rob Napier, who explains why and how to add certificate pinning to your app.

00:06 Rob is in Berlin to give a talk at UIKonf, so we're using this opportunity to have a look at what certificate pinning is and how it works.

00:52 We've prepared a simple app that loads a webpage with URLSession. We'd usually be concerned with checking the certificate when doing API calls, but for the sake of simplicity, we're loading just our website:

class ViewController: UIViewController {

    @IBOutlet weak var webView: UIWebView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let url = URL(string: "https://www.objc.io")!
        let session = URLSession.shared
        let task = session.dataTask(with: url) { data, response, error in
            if let error = error {
                print(error)
                return
            }
            print("Loaded!")
            if let data = data {
                self.webView.load(data, mimeType: response?.mimeType ?? "", textEncodingName: response?.textEncodingName ?? "", baseURL: url)
            }
        }
        task.resume()
    }
}

01:12 This works fine, so we're ready to start making our app more secure.

About Certificates

01:26 For HTTPS, there's a long list of certificates trusted by the operating system. These certificates are used to issue individual certificates, like the one on our server. The problem is that any of the 150+ certificates on the system's list can be compromised.

02:11 We can narrow down the list of trusted certificates by pinning, meaning we'll only trust our one certificate (or an intermediate certificate we control).

02:39 We do so by adding a URLSessionDelegate, which will be asked for information during the server handshake process. This delegate could be a separate object, but for now we'll just use our view controller:

class ViewController: UIViewController, URLSessionDelegate {
    // ...
}

03:27 To assign a delegate, we can no longer use the shared session. We create a new session with the view controller as the delegate:

let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)

03:53 In an extension, we'll implement the relevant method that evaluates a challenge:

func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    
}

04:25 This method is called for all kinds of security checks, but since we're only interested in the certificate challenge, we check for a so-called trust in the challenge. This trust can contain certificates, and we check that it contains at least one. The trust object is a C data structure, so we have to use a specific method to count the certificates:

if let trust = challenge.protectionSpace.serverTrust,
     SecTrustGetCertificateCount(trust) > 0 {

}

07:12 If we have a trust, we want to pull the certificate out. There may be multiple certificates, but they're in order, with the certificate closest to us being first, so we only check the first certificate, using another C function:

if let certificate = SecTrustGetCertificateAtIndex(trust, 0) {

}

07:53 If we get a certificate, we can check if it's one of ours by comparing its content. We get the data as CFData, which is bridged to Swift's Data, so we can simply cast it for easier use:

let data = SecCertificateCopyData(certificate) as Data

09:08 We compare the data against a list of our known certificates. Because certificates expire, there may be some overlapping ones on the server. The app should check if any of the trusted certificates are found. That's why we use an array, which we'll populate later:

class ViewController: UIViewController, URLSessionDelegate {
    // ...
    let certificates: [Data] = []
    // ...
}

10:56 Back in the delegate method, we check if the found certificate data is contained in our certificates array. Finally, we have to call the completion handler to either "use a credential" if we find a trusted certificate, or "cancel" otherwise. In order to advance successfully, we can simply create a credential with the trust we found earlier. Here's the complete delegate method:

extension ViewController {
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        if let trust = challenge.protectionSpace.serverTrust, SecTrustGetCertificateCount(trust) > 0 {
            if let certificate = SecTrustGetCertificateAtIndex(trust, 0) {
                let data = SecCertificateCopyData(certificate) as Data
                if certificates.contains(data) {
                    completionHandler(.useCredential, URLCredential(trust: trust))
                    return
                }
            }
        }
        completionHandler(.cancelAuthenticationChallenge, nil)
    }
}

13:14 We're still checking against an empty certificates array, but it's good to check that the challenge fails if we want it to.

Storing a Certificate

13:52 Now we need our certificate's data, so we use openssl in Terminal to retrieve it from our server. More specifically, we use the s_client command, which can connect to any server over SSL by specifying the server address and port 443. The < /dev/null part redirects back in; otherwise, we'd be talking to the server:

openssl s_client -connect www.objc.io:443 < /dev/null

15:11 In the case of our server address, we run into a common error:

CONNECTED(00000003)
16632:error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure:/BuildRoot/Library/Caches/com.apple.xbs/Sources/OpenSSL098/OpenSSL098-64.50.6/src/ssl/s23_clnt.c:593:

15:12 This occurs because our website runs on a shared hosting machine, which may be handling lots of web addresses. We have to specify the server name, for which we can use the same address:

openssl s_client -connect www.objc.io:443 -servername www.objc.io < /dev/null

16:11 This works, and we receive a lot of data, including a list of three certificates. We confirm that we see the part of the first certificate listing that says CN=, which stands for common name:

Certificate chain
 0 s:/OU=GT09961371/OU=See www.rapidssl.com/resources/cps (c)15/OU=Domain Control Validated - RapidSSL(R)/CN=www.objc.io
   i:/C=US/O=GeoTrust Inc./CN=RapidSSL SHA256 CA - G3
 1 s:/C=US/O=GeoTrust Inc./CN=RapidSSL SHA256 CA - G3
   i:/C=US/O=GeoTrust Inc./CN=GeoTrust Global CA
 2 s:/C=US/O=GeoTrust Inc./CN=GeoTrust Global CA
   i:/C=US/O=Equifax/OU=Equifax Secure Certificate Authority

16:33 Below that, we can see the actual certificate data we need. To copy the certificate into a file, we use openssl again. We repeat the previous command and pass its output to openssl x509 (the general format name of certificates), we specify the binary output format (DER), and we output it to a new file, named objcio.cer:

openssl s_client -connect www.objc.io:443 -servername www.objc.io < /dev/null | openssl x509 -outform DER > objcio.cer

17:27 The saved file's size should be greater than zero, around 1 kB.

17:44 We add the certificate file to our project using menu option File > Add Files, which automatically adds the file to the project's target.

18:32 Finally, we load the certificate file's contents into the view controller's array of trusted certificates. By initializing the array with a closure, the property can stay immutable. We force unwrap optionals, because if reading the file fails, we want the app to crash:

let certificates: [Data] = {
    let url = Bundle.main.url(forResource: "objcio", withExtension: "cer")!
    let data = try! Data(contentsOf: url)
    return [data]
}()

Discussion

21:01 Our application now makes sure the server it connects to is really our server. This protects users from man-in-the-middle attacks, where a fake certificate gets pushed onto their device, making the system trust a potentially malignant server. It also protects us in case any of Apple's trusted certificates get compromised.

22:04 By default, we need to have a commercial certificate in order to pass ATS, the transport security system. We could allow arbitrary URLs and use a self-signed certificate, but in doing so we'd have to undergo extra review by Apple before our app is approved.

22:56 We've seen the simplest way to implement certificate pinning, in that we're checking that the one certificate of our request equals the one on our server. A more advanced option is to also check intermediate certificates.

23:21 It can be tough to work through the documentation of the necessary C functions in order to figure out what you're supposed to do with them. Rob wrote some open source code that offers more flexibility to the developer and not only compares but also evaluates certificates. This is a more foolproof starting point than figuring everything out from scratch.

Resources

  • Sample Project

    Written in Swift 3.1

  • Episode Video

    Become a subscriber to download episode videos.

Episode Details

Recent Episodes

See All

Unlock Full Access

Subscribe to Swift Talk

  • Watch All Episodes

    New subscriber-only episodes every two weeks

  • Invite Your Team

    Sign up additional team members at 30% discount

  • Support Us

    Ensure the continuous production of new episodes