iOS & Swift Tutorials

Learn iOS development in Swift. Over 2,000 high quality tutorials!

Preventing Man-in-the-Middle Attacks in iOS with SSL Pinning

In this tutorial, you’ll learn how to prevent man-in-the-middle attacks using SSL Pinning and Alamofire. You’ll use the Charles Proxy tool to simulate the man-in-the-middle attack.

4.7/5 18 Ratings

Version

  • Swift 5, iOS 12, Xcode 10

Nowadays, most iOS apps communicate with a server to retrieve information to work with. When apps exchange information, they typically use the Transport Layer Security (TLS) protocol to provide secure communications.

Apps don’t usually determine which certificates to trust and which not to trust when they try to establish a connection with a server. Rather, they rely entirely on the certificates that iOS contains.

Even if TLS protects the transmitted data against eavesdropping and tampering, attackers can set up man-in-the-middle attacks using hacked or self-signed certificates. Through these certificates, they can capture data moving to and from your app.

In this tutorial, you’ll learn how to prevent man-in-the-middle attacks using SSL Certificate Pinning and Alamofire 5. To verify that your implementation works as expected, you’ll use Charles Proxy‘s man-in-the-middle strategy.

Note: Secure Sockets Layer (SSL) is the ancestor of TLS. TLS addresses various security vulnerabilities identified by Internet Engineering Task Force (IETF) which affected SSL version 3.0. Throughout this tutorial, you should read SSL and TLS as synonyms, but code implementation should always use TLS.

Getting Started

For this tutorial, you’ll use PinMyCert, an iOS app that uses the Stack Exchange REST API to retrieve Stack Overflow users.

Start by downloading the starter project using the Download Materials button at the top or bottom of this tutorial. Once downloaded, open PinMyCert.xcodeproj in Xcode.

To keep you focused, the starter project has everything unrelated to SSL Certificate Pinning already set up for you.

Open Main.storyboard and look at the view controllers contained within. The view controller on the left is the root navigation controller of the app. Next, you have ViewController, which includes a table that lists the users retrieved from Stack Overflow. Finally, you have DetailViewController, which displays the detail for the selected user.

ViewController uses NetworkClient. This is a wrapper around Alamofire which exposes an API that performs network requests. In NetworkClient, you’ll implement the logic for dealing with SSL Certificate Pinning. More on that later.

Build and run the app, and you’ll see this initial screen:

Initial Screen

Before diving directly into the code, let’s talk about TLS!

Understanding TLS

Off to the TLS lab!

Public key plus private key equals…

To understand SSL Certificate Pinning, you should first grasp the essence of TLS and its cryptographic underpinnings.

The main goal of TLS is to add privacy and integrity to messages exchanged between two parties. In other words, TLS allows you to transmit data over a network without exposing that data to untrusted third parties.

When a client and a server need a TLS connection, building that connection follows three phases, executed in a specific order.

TLS Handshake

The Three Phases of TLS Connections

In the first phase, the client initiates a connection with the server.

The client then sends the server a message, which lists the versions of TLS it can support along with the cipher suite it can use for encryption.

Note: A cipher suite is a set of algorithms that you need to secure a network connection through TLS. For more info, please refer to cipher suite.

The server responds with the selected cipher suite and sends one or more digital certificates back to the client.

The client verifies that those digital certificates — certificates, for short — are valid. It also verifies that the server is authentic and not someone who wants to snoop sensitive information.

If the validation succeeds, the second phase of verification begins. The client generates a pre-master secret key and encrypts it with the server’s public key — i.e., the public key included in the certificate.

The server receives the encrypted pre-master secret key and decrypts it with its private key. The server and client each generate the master secret key and session keys based on the pre-master secret key.

Note: The second phase uses public-key cryptography or asymmetric cryptography. This is a cryptographic system that uses pairs of keys: Public keys, which are widely disseminated and private keys, which only the owner knows.

That master secret key is then used in the last phase to decrypt and encrypt the information that the two actors exchange.

Note: The third phase uses symmetric-key cryptography, where you use the same key for both encryption of plaintext and decryption of ciphertext.

About Digital Certificates

As you’ve learned in the previous section, the server sends one or more certificates back to the client.

So, what’s a certificate? A certificate is a file that encapsulates information about the server that owns the certificate. It’s similar to an identification card, such as a passport or a driver license.

A Certificate Authority (CA) can issue a certificate or it can be self-signed. In the first case, the CA must validate the identity of the certificate holder both before it issues the certificate and when your app uses the certificate. In the second case, the same entity whose identity it certifies signs the certificate.

The Structure of a Digital Certificates

The structure of a certificate uses X.509 standard. Here are the main fields:

  • Subject: Provides the name of the entity (computer, user, network device, etc.) that the CA issued the certificate to.
  • Serial Number: Provides a unique identifier for each certificate that a CA issues.
  • Issuer: Provides a unique name for the CA that issued the certificate.
  • Valid From: Provides the date and time when the certificate becomes valid.
  • Valid To: Provides the date and time when the certificate is no longer considered valid.
  • Public Key: Contains the public key of the key pair that goes with the certificate.
  • Algorithm Identifier: Indicates the algorithm used to sign the certificate.
  • Digital Signature: A bit string used to verify the authenticity of the certificate.

The couple consisting of the public key and the algorithm identifier represents the subject public key info.

X.509 Digital Certificate

X.509 certificates can be encoded differently, which will affect their appearance. The most common are:

Validating Digital Certificates

When you get a certificate from a CA, that certificate is part of a chain of trust, or a chain of certificates.

The number of certificates in the chain depends on the CA’s hierarchical structure. The two-tier hierarchy is the most common. An issuing CA signs the user’s certificate and a root CA signs the issuing CA’s certificate. The root CA is self-signed and the app must trust it at the end.

Chain of Trust

During a certificate validation, the app verifies:

  • The date of evaluation, which must fall between the Valid From and Valid To fields of the certificate for the certificate to be valid.
  • The digital signature, by finding the public key of the next issuing CA or intermediate CA. The process continues until it reaches the root certificate.
Note: iOS keeps all well-known root CA certificates in its Trust Store. If you want to know the trusted root certificates that come pre-installed with iOS, please refer to Apple’s lists of available trusted root certificates in iOS.

SSL Certificate Pinning Under the Hood

SSL Certificate Pinning, or pinning for short, is the process of associating a host with its certificate or public key. Once you know a host’s certificate or public key, you pin it to that host.

In other words, you configure the app to reject all but one or a few predefined certificates or public keys. Whenever the app connects to a server, it compares the server certificate with the pinned certificate(s) or public key(s). If and only if they match, the app trusts the server and establishes the connection.

You usually add a service’s certificate or public key at development time. In other words, your mobile app should include the digital certificate or the public key within your app’s bundle. This is the preferred method, since an attacker cannot taint the pin.

Why Do You Need SSL Certificate Pinning?

Usually, you delegate setting up and maintaining TLS sessions to iOS. This means that when the app tries to establish a connection, it doesn’t determine which certificates to trust and which not to. The app relies entirely on the certificates that the iOS Trust Store provides.

This method has a weakness, however: An attacker can generate a self-signed certificate and include it in the iOS Trust Store or hack a root CA certificate. This allows such an attacker to set up a man-in-the-middle attack and capture the transmitted data moving to and from your app.

Restricting the set of trusted certificates through pinning prevents attackers from analyzing the functionality of the app and the way it communicates with the server.

Types of SSL Certificate Pinning

If you want to implement pinning — which it seems you do, since you’re reading this tutorial — you can decide between two options:

  • Pin the certificate: You can download the server’s certificate and bundle it into your app. At runtime, the app compares the server’s certificate to the one you’ve embedded.
  • Pin the public key: You can retrieve the certificate’s public key and include it in your code as a string. At runtime, the app compares the certificate’s public key to the one hard-coded in your code.

Choosing between these two options depends on your needs and server configuration. If you choose the first option, you need to upload your app when your server rotates (changes) its certificate or it will stop working. If you choose the second option, it may violate key rotation policy because the public key doesn’t change.

Note: As well as pinning the certificate or the public key, it’s also possible to pin the subject public key info. At the time of this writing, Alamofire is not able to perform this type of pinning. If you’re looking for such a solution, refer to TrustKit.

Now that you have a solid grasp on how pinning works, it’s time to see what Alamofire 5 can do for you!

Pinning in Alamofire 5

Alamofire 5 supports the pinning of both the certificate and the public key. In particular, it provides two different classes, called respectively PinnedCertificatesTrustEvaluator and PublicKeysTrustEvaluator, which allow you to deal with these cases.

Note: Hereafter, this tutorial will only cover certificate pinning. You can play around with the implementation of public key pinning once you’ve finished the tutorial, if you want to.

Storing The Certificate

To see Alamofire 5 in action, first you need to download the certificate from StackExchange.com.

Use OpenSSL to retrieve the certificate from the Stack Overflow server. More specifically, you’ll use the s_client command, which can connect to any server over SSL by specifying the server address and port 443.

Open a new Terminal and type cd followed by a space. Then, drag and drop the directory of the starter project that you downloaded in the Getting Started section and press Enter on your keyboard.

Change Directory

Still in the terminal window, type cd PinMyCert to move into your project’s root folder.

Next, copy and paste the following snippet:

openssl s_client -connect api.stackexchange.com:443 </dev/null

Once it completes, you'll receive a lot of data including a list of certificates. Each certificate in the chain has a Common Name (CN).

Certificate chain
 0 s:/C=US/ST=NY/L=New York/O=Stack Exchange, Inc./CN=*.stackexchange.com
   i:/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert SHA2 High Assurance Server CA
 1 s:/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert SHA2 High Assurance Server CA
   i:/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert High Assurance EV Root CA

Below that, you can see the actual certificate you're interested in, which is the one where CN is *.stackexchange.com.

Server certificate
-----BEGIN CERTIFICATE-----
MIIHMjCCBhqgAwIBAgIQBmgM1QeOzDnM9C33n9PrfTANBgkqhkiG9w0BAQsFADBw
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMS8wLQYDVQQDEyZEaWdpQ2VydCBTSEEyIEhpZ2ggQXNz
dXJhbmNlIFNlcnZlciBDQTAeFw0xNjA1MjEwMDAwMDBaFw0xOTA4MTQxMjAwMDBa
MGoxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJOWTERMA8GA1UEBxMITmV3IFlvcmsx
HTAbBgNVBAoTFFN0YWNrIEV4Y2hhbmdlLCBJbmMuMRwwGgYDVQQDDBMqLnN0YWNr
ZXhjaGFuZ2UuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr0YD
zscT5i6T2FaRsTGNCiLB8OtPXu8N9iAyuaROh/nS0kRRsN8wUMk1TmgZhPuYM6oF
S377V8W2LqhLBMrPXi7lnhvKt2DFWCyw38RrDbEsM5dzVGErmhux3F0QqcTI92zj
VW61DmE7NSQLiR4yonVpTpdAaO4jSPJxn8d+4p1sIlU2JGSk8LZSWFqaROc7KtXt
lWP4HahNRZtdwvL5dIEGGNWx+7B+XVAfY1ygc/UisldkA+a3D2+3WAtXgFZRZZ/1
CWFjKWJNMAI6ZBAtlbgSNgRYxdcdleIhPLCzkzWysfltfiBmsmgz6VCoFR4KgJo8
Gd3MeTWojBthM10SLwIDAQABo4IDzDCCA8gwHwYDVR0jBBgwFoAUUWj/kK8CB3U8
zNllZGKiErhZcjswHQYDVR0OBBYEFFrBQmPCYhOznZSEqjIeF8tto4Z7MIIB/AYD
VR0RBIIB8zCCAe+CEyouc3RhY2tleGNoYW5nZS5jb22CEXN0YWNrb3ZlcmZsb3cu
Y29tghMqLnN0YWNrb3ZlcmZsb3cuY29tgg1zdGFja2F1dGguY29tggtzc3RhdGlj
Lm5ldIINKi5zc3RhdGljLm5ldIIPc2VydmVyZmF1bHQuY29tghEqLnNlcnZlcmZh
dWx0LmNvbYINc3VwZXJ1c2VyLmNvbYIPKi5zdXBlcnVzZXIuY29tgg1zdGFja2Fw
cHMuY29tghRvcGVuaWQuc3RhY2thdXRoLmNvbYIRc3RhY2tleGNoYW5nZS5jb22C
GCoubWV0YS5zdGFja2V4Y2hhbmdlLmNvbYIWbWV0YS5zdGFja2V4Y2hhbmdlLmNv
bYIQbWF0aG92ZXJmbG93Lm5ldIISKi5tYXRob3ZlcmZsb3cubmV0gg1hc2t1YnVu
dHUuY29tgg8qLmFza3VidW50dS5jb22CEXN0YWNrc25pcHBldHMubmV0ghIqLmJs
b2dvdmVyZmxvdy5jb22CEGJsb2dvdmVyZmxvdy5jb22CGCoubWV0YS5zdGFja292
ZXJmbG93LmNvbYIVKi5zdGFja292ZXJmbG93LmVtYWlsghNzdGFja292ZXJmbG93
LmVtYWlsghJzdGFja292ZXJmbG93LmJsb2cwDgYDVR0PAQH/BAQDAgWgMB0GA1Ud
JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjB1BgNVHR8EbjBsMDSgMqAwhi5odHRw
Oi8vY3JsMy5kaWdpY2VydC5jb20vc2hhMi1oYS1zZXJ2ZXItZzUuY3JsMDSgMqAw
hi5odHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc2hhMi1oYS1zZXJ2ZXItZzUuY3Js
MEwGA1UdIARFMEMwNwYJYIZIAYb9bAEBMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8v
d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQICMIGDBggrBgEFBQcBAQR3MHUw
JAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBNBggrBgEFBQcw
AoZBaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMkhpZ2hB
c3N1cmFuY2VTZXJ2ZXJDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsF
AAOCAQEARIdUz7n08ZtqWscAmTXegtB6yPrU0l5IQCXQRqnEVXPKyS+w8IVOcblT
T/W2Qlp5we2BTDbRDfVokXIOSxOTAT0XN3f3c+nbvKJ3XMBH236846AY6bpfqL0/
05gcdt39d2iXTL+qnJW9P0yFKpkfGXBBTYQl4ACSeThSuSBXIVJ0v/TfR9+ggXuP
pmXiIKkPOReKu2Tu8SO7+5KRqRJvYhP9mhL4Bl+YLrTQXzM1NwVAahRT1QJJNemy
yEY1kkZOCKt0xRu4CVWhJlpNdoRZenT9BrD8Fo22kt5MxAvCVrjT/g1BHDQd4S8p
PKC8kRwmMA8mdo8TiHJQMy0DBCDCDg==
-----END CERTIFICATE-----
subject=/C=US/ST=NY/L=New York/O=Stack Exchange, Inc./CN=*.stackexchange.com
issuer=/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert SHA2 High Assurance Server CA

To copy the certificate into a file, use openssl again. Repeat the previous command and pass its output to openssl x509, specify DER encoding and output it to a new file named stackexchange.com.der:

openssl s_client -connect api.stackexchange.com:443 </dev/null \
  | openssl x509 -outform DER -out stackexchange.com.der

If you've followed the steps correctly, you should be able to see that certificate in the same folder of your project.

Saved Certificate

Implementing Certificate Pinning

Before writing the code, you need to import the certificate that you previously downloaded. Open PinMyCert.xcodeproj in Xcode, if you don't still have it open.

Right-click on the root PinMyCert folder in the Project navigator. Click Add Files to “PinMyCert”..., then in the file chooser, find and select stackexchange.com.der and click Add.

Importing Certificate

Open NetworkClient.swift and paste the following code at the end of the file:

struct Certificates {
  static let stackExchange =
    Certificates.certificate(filename: "stackexchange.com")
  
  private static func certificate(filename: String) -> SecCertificate {
    let filePath = Bundle.main.path(forResource: filename, ofType: "der")!
    let data = try! Data(contentsOf: URL(fileURLWithPath: filePath))
    let certificate = SecCertificateCreateWithData(nil, data as CFData)!
    
    return certificate
  }
}

The above struct provides a user-friendly way to retrieve a certificate from the main bundle.

SecCertificateCreateWithData is responsible for creating a certificate object from a DER-encoded file.

Still within NetworkClient.swift, find NetworkClient and replace the entire line let session = Session.default with the following code:

// 1
let evaluators = [
  "api.stackexchange.com":
    PinnedCertificatesTrustEvaluator(certificates: [
      Certificates.stackExchange
    ])
]

let session: Session

// 2
private init() {
  session = Session(
    serverTrustManager: ServerTrustManager(evaluators: evaluators)
  )
}

Here's the breakdown of the code above:

  1. You create a dictionary called evaluators, which contains a single key-value pair. The key is of type String and it represents the host you want to check. The value is of a subtype of ServerTrustEvaluating called PinnedCertificatesTrustEvaluator. It describes the evaluation policy you want to apply for that specific host. You'll use the PinnedCertificatesTrustEvaluator to validate the server trust. The server trust is valid if the pinned certificate exactly matches the server certificate.
  2. You declare a private initializer that instantiates Session using ServerTrustManager. The latter is responsible for managing the mapping declared in the evaluators dictionary.

Now, open ViewController.swift and find the code responsible for the network request:

NetworkClient.request(Router.users)
  .responseDecodable { (response: DataResponse<UserList>) in
    switch response.result {
    case .success(let value):
      self.users = value.users
    case .failure(let error):
      self.presentError(withTitle: "Oops!", message: error.localizedDescription)
    }
}

Replace it with this new implementation:

NetworkClient.request(Router.users)
  .responseDecodable { (response: DataResponse<UserList>) in
    switch response.result {
    case .success(let value):
      self.users = value.users
    case .failure(let error):
      let isServerTrustEvaluationError =
        error.asAFError?.isServerTrustEvaluationError ?? false
      let message: String
      if isServerTrustEvaluationError {
        message = "Certificate Pinning Error"
      } else {
        message = error.localizedDescription
      }
      self.presentError(withTitle: "Oops!", message: message)
    }
}

While the success case remains the same, you have enriched the failure case with an additional condition. First, you try to cast error as an AFError. If the cast succeeds, you'll evaluate isServerTrustEvaluationError. If its value is true, it means the certificate pinning has failed.

Build and run the app. Nothing should have changed visually.

But wait! If this is a tutorial that teaches you how to prevent man-in-the-middle attacks, how can you be sure you've done everything correctly when no attack has occurred?

Stop!

Trusted traffic only please

To answer this question, jump right to the next section.

Testing Certificate Pinning With Charles

In order to verify that everything runs as expected, you first need to download the latest version of Charles Proxy, which is version 4.2.8 at the time of this writing. Double-click the DMG file and drag the Charles icon to your Applications folder to install it.

Charles is a proxy server, a middleware, that sits between your app and the computer’s network connections. You can use Charles to configure your network settings to route all traffic through it. This allows Charles to inspect all network events to and from your computer.

Proxy servers are in a position of great power, which also means they have the potential for abuse. That's why TLS is so important: Data encryption prevents proxy servers and other middleware from eavesdropping on sensitive information.

Charles generates its own self-signed certificate, which you can install on your Mac and iOS devices for TLS encryption. Since this certificate isn’t issued by a trusted certificate issuer, you’ll need to tell your devices to explicitly trust it. Once installed and trusted, Charles will be able to decrypt SSL events.

In your case, however, Charles won't snoop your SSL messages. Charles’ sneaky man-in-the-middle strategy won’t work because your pinning strategy will prevent it.

Certificate Pinning in Action

To see your enhanced security in action, launch Charles. Charles starts recording network events as soon as it launches.

Note: If you need to learn more about Charles, please refer to our Charles Proxy Tutorial for iOS.

In Charles, first switch to the Sequence tab. Then enter api.stackexchange.com in the filter box to make it easier to find the request that you need, and click the broom symbol to clear the current session.

Charles User Interface

Now click Proxy on the menu bar and select macOS Proxy to turn it back on (if it doesn’t already show a check mark).

Then, click Proxy ▸ SSL Proxying Settings and add api.stackexchange.com to the list. You can leave the Port field blank. Select Enable SSL Proxying and click OK to confirm.

Note: Remember to deselect Enable SSL Proxying when you have finished your tests. Otherwise, you won't be able to run the app normally.

Next, you need to install the Charles Proxy SSL certificate to allow proxying SSL requests in the Simulator.

In Charles, click Help ▸ SSL Proxying ▸ Install Charles Root Certificate in iOS Simulators. Then, on the Simulator, open the Settings app. Tap through General ▸ About ▸ Certificate Trust Settings (it's at the bottom, so you may have to scroll). Tap the switch to turn on the Charles Proxy CA and tap Continue in the resulting warning dialog.

Back in Xcode, build and run the project. You should see an alert like the following:

Final screen

On Charles' side, you should see a failure like the one represented below:

Charles Failure

Congratulations! You now have an app that is able to prevent man-in-the-middle attacks!

Where to Go From Here?

You can download the completed version of the project using the Download Materials link at the top or the bottom of this tutorial.

You’ve learned how to achieve SSL Certificate Pinning using Alamofire 5. Your users can now be sure attackers will not able to steal sensitive information from your apps.

When you've finished experimenting with your app and Charles, it's important to remove the Charles CA certficate from the Simulator. With the Simulator active, select Hardware ▸ Erase All Content and Settings... from the menu, then click Erase.

If you want to learn more about SSL Certificate Pinning and security in general, check out OWASP Mobile Security Testing Guide. It's a comprehensive testing guide that covers the processes, techniques and tools used during mobile app security tests.

In the meantime, if you have any questions or comments, please join the forum discussion below!

Average Rating

4.7/5

Add a rating for this content

18 ratings

Contributors

Comments