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. By Lorenzo Boaro.

4.8 (37) ·

Download materials
Save for later

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

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 </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=*
   i:/C=US/O=DigiCert Inc/ SHA2 High Assurance Server CA
 1 s:/C=US/O=DigiCert Inc/ SHA2 High Assurance Server CA
   i:/C=US/O=DigiCert Inc/ High Assurance EV Root CA

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

Server certificate
subject=/C=US/ST=NY/L=New York/O=Stack Exchange, Inc./CN=*
issuer=/C=US/O=DigiCert Inc/ 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

openssl s_client -connect </dev/null \
  | openssl x509 -outform DER -out

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 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: "")
  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 = [
    PinnedCertificatesTrustEvaluator(certificates: [

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:

  .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:

  .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?


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 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 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!