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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 3 of this article. Click here to view the first page.

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?

Trusted traffic only please

Stop!

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!