Writing SSL-Pinning by yourself

Posted on Jun 26, 2021

SSL Certificate Pinning is a mechanism that can be used to improve the security of a mobile app network connection. Pinning allows you to protect your users from man-in-the-middle attack (MITM). Yes, iOS/macOS/… and HTTPS have a high-security level, and you can’t read a whole HTTPS traffic of apps of someone’s iPhone. But it’s recommended to implement SSL-Pinning for financial or medical services because users’ data must be protected from such attacks. So let’s dive into how easily you can protect your users and what disadvantages it has.

Basic explanation

Let’s say you have a mobile app and Server API. On the server, there is a certificate that encrypts all HTTPS traffic. You can open your API address in Safari and check the details about the certificate.

Ceritificate with disabled proxy

If MITM attacks you, the certificate will change. You can check it by enabling Proxyman or Charles proxy.

Ceritificate with enabled proxy

So the idea of certificate pinning is to verify the identity of a server by comparing certificates. Consider certificates have a tree structure with one certificate authority (CA) as root.

Certificate tree

Different ways to do pinning

In terms of what certificate to pin, there are several ways:

  1. Pin root certificate of CA.
  2. Pin your server certificate.

And you can verify the identity of the certificate in different ways too:

  1. Check fingerprint – hash of whole certificate (public key + private key).
  2. Check the public key only.

Checking the public key is recommended because you don’t need to release a new version of your app when your certificate expires. If you renew the certificate properly, your public key stays the same, and only the private key will change.

How to find out a public key

A public key is public, so it should be easy to find this. But, unfortunately, you can’t just copy and paste it from Safari or elsewhere. To do that, you should use openssl in a terminal:

echo | openssl s_client -servername mobile-api.moi-service.ru -connect mobile-api.moi-service.ru:443 |\
sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > certificate.crt

This command saves *.crt file with a public key. Open it by a text editor, and you can see base64 encoded public key.


But we need *.cer binary format. The easiest way I found is to save *.crt to Keychain and export to *.cer after. Remember that *.cer format never has a private key. If you want you can ask your backend engineers to give you this format and don’t do it by yourself.

Write a code

If you are using Alamofire, I recommend reading the documentation carefully and use built-in API. But in my case, I use Apollo for GraphQL, and there is no mechanism for SSL-pinning.

Step zero is to save *.cer certificate to the main bundle because you need to verify it with server one (for fingerprint check, you can save a hash string to constant or config file).

To verify server identity you should use urlSession(_:didReceive:completionHandler:) method from URLSessionDelegate . This method is called for each request from your app.

    override func urlSession(
        _ session: URLSession,
        didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
  1. That’s important to pin several certificates if you have several hosts. Also, skip some if it’s not yours (Analytics API or similar). In this example, I verify my base host (baseURL is a private property of my class) with the request host. If it’s different, I skip all checks.
    challenge.protectionSpace.host == baseURL.host,
    challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
    let trust = challenge.protectionSpace.serverTrust
else {
    completionHandler(.performDefaultHandling, nil)
  1. Verify certificate validity and obtain your server certificate. It’s always at the index zero. The root certificate is at the last index.
var error: CFError?
let success = SecTrustEvaluateWithError(trust, &error)
    let serverCertificate = SecTrustGetCertificateAtIndex(trust, 0)
else {
    completionHandler(.cancelAuthenticationChallenge, nil)
  1. We convert the server certificate to Data and open the file of our saved certificate *.cer.
let serverCertificateCFData = SecCertificateCopyData(serverCertificate)
    let serverCertData = CFDataGetBytePtr(serverCertificateCFData),
    let filePath = Bundle.main.path(forResource: "production-api", ofType: "cer")
else {
    completionHandler(.cancelAuthenticationChallenge, nil)
let fileURL = URL(fileURLWithPath: filePath)
let serverCert = Data(bytes: serverCertData, count: CFDataGetLength(serverCertificateCFData))
  1. Saved certificate is also converted to Data and compared with the server certificate.
    let fileCert = try? Data(contentsOf: fileURL),
    serverCert == fileCert
else {
    completionHandler(.cancelAuthenticationChallenge, nil)
completionHandler(.performDefaultHandling, nil)

The entire method you can find here. That’s it! You can test it by enabling proxy on your iPhone. Then, any request to your server from your app would be interrupted with an error.

Final recommendations

  1. Do not pin certificates from 3rd party server. Google or Facebook can change it without asking you.
  2. It’s convenient to turn on SSL-pinning only on release builds. It’s more accessible to you and QA to analyze traffic to detect bugs.
  3. Pin Public Key. It’s a pain in the ass when a certificate will be renewed.