How to Make Your iOS Apps More Secure with SSL Pinning

how-to-make-your-ios-apps-more-secure-with-ssl-pinning-0

SSL pinning plays a major role in building highly secure mobile apps which users will be able to use even in countless insecure wireless networks that they encounter every day while using their mobile devices.

We’ve published an updated article, read about SSL pinning in iOS – Swift edition

Here at Infinum, we have a solid chunk of experience in working on apps that require high-security standards, such as mobile banking apps. This article covers the SSL pinning technique that helps us deal with one of the most common security attacks – MITM or man-in-the-middle.

title

At a glance

SSL (Secure Socket Layer) ensures encrypted client-server communication over HTTP – specified by HTTPS (HTTP over SSL). The encryption is based on PKI (Public Key Infrastructure) and a session key. The session key was introduced because encrypting and decrypting a public/private key uses a lot of processing power and it would slow down the whole communication process.

Instead of having to asymmetrically encrypt data at the source and decrypt it at the destination, a symmetric session key, which is exchanged with the SSL handshake when the communication starts, is used.

SSL Security – Identification

The security aspect of SSL is based on the certificate’s “chain of trust”. When the communication starts, the client examines the server’s SSL certificate and checks if the received certificate is trusted by the Trusted Root CA store or other user-trusted certificates.

MITM

Although SSL communication is considered pretty much secure and unbreakable in realistic time frames, the man-in-the-middle attack still poses an actual threat. It can be carried out using several methods, which include ARP cache poisoning and DNS spoofing.

With ARP cache poisoning, it is possible to carry out an MITM attack by using the nature of the Address Resolution Protocol which is responsible for mapping the IP address to the device’s MAC address.

For example, let’s describe a simple network containing these 3 main actors:

  • a common user’s device U
  • the attacker’s device A
  • and the router R

Device A can send the ARP reply packet to the device U, introducing itself as the router R. To complete the MITM attack, A sends another ARP reply to R introducing itself as the device U. In this way, the attacker’s device A is in the middle of the communication between the device U and router R and it can eavesdrop or block it. IP forwarding is often used on the attacker’s device to keep the communication flowing seamlessly between the user’s device and router.

DNS spoofing includes a broad range of attacks aimed at corrupting the name server’s domain name mapping. The attacker tries to find a way to force the DNS to return an incorrect IP address and divert traffic to their computer.

SSL pinning

We use SSL pinning to ensure that the app communicates only with the designated server itself. One of the prerequisites for SSL pinning is saving the target’s server SSL certificate within the app bundle. The saved certificate is used when defining the pinned certificate(s) upon session configuration.

We will be covering SSL pinning using NSURLSession, AlamoFire and AFNetworking (using AFHTTPRequestOperationManager).

NSURLSession

Things are a bit more tricky when it comes to NSURLSession SSL pinning. There is no way to set an array of pinned certificates and cancel all responses that don’t match our local certificate automatically. We need to perform all checks manually to implement SSL pinning on NSURLSession. We’ll happily use some of the Security’s framework C API (like all other true hackers do).

We can start by instantiating an NSURLSession object with the default session configuration.

Swift
	self.urlSession = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: self, delegateQueue: nil)
Objective C
	NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
self.urlSession = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];

NSURLSession sends requests using tasks (NSURLSessionTask). We will use the dataTaskWithURL:completionHandler: method for the SSL pinning test. The request we send will look something like this:

Swift
	self.urlSession?.dataTaskWithURL(NSURL(string:self.urlTextField.text!)!, completionHandler: { (NSData data, NSURLResponse response, NSError error) Void in
    // response management code
}).resume()
Objective C
	[[self.urlSession dataTaskWithURL:[NSURL URLWithString:self.textField.text] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        // response management code
    }] resume];

As the dataTaskWithURL:completionHandler method only returns the NSURLSessionTask object, the [NSURLSessionTask -resume] method sends the request, or in other words, executes the task.

The magic of SSL pinning is implemented within the URLSession:didReceiveChallenge:completionHandler:delegate method. Note that, upon the creation of the NSURLSession object, we assigned self as the delegate so that this method is called on our object.

Swift
	func URLSession(session: NSURLSession,  didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
    let serverTrust = challenge.protectionSpace.serverTrust
    let certificate = SecTrustGetCertificateAtIndex(serverTrust!, 0)

    // Set SSL policies for domain name check
    let policies = NSMutableArray();
    policies.addObject(SecPolicyCreateSSL(true, (challenge.protectionSpace.host)))
    SecTrustSetPolicies(serverTrust!, policies);

    // Evaluate server certificate
    var result: SecTrustResultType = 0
    SecTrustEvaluate(serverTrust!, &result)
    let isServerTrusted:Bool = (Int(result) == kSecTrustResultUnspecified || Int(result) == kSecTrustResultProceed)

    // Get local and remote cert data
    let remoteCertificateData:NSData = SecCertificateCopyData(certificate!)
    let pathToCert = NSBundle.mainBundle().pathForResource(githubCert, ofType: "cer")
    let localCertificate:NSData = NSData(contentsOfFile: pathToCert!)!

    if (isServerTrusted && remoteCertificateData.isEqualToData(localCertificate)) {
        let credential:NSURLCredential = NSURLCredential(forTrust: serverTrust!)
        completionHandler(.UseCredential, credential)
    } else {
        completionHandler(.CancelAuthenticationChallenge, nil)
    }
}

Objective C
	-(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {

    // Get remote certificate
    SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
    SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, 0);

    // Set SSL policies for domain name check
    NSMutableArray *policies = [NSMutableArray array];
    [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)challenge.protectionSpace.host)];
    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

    // Evaluate server certificate
    SecTrustResultType result;
    SecTrustEvaluate(serverTrust, &result);
    BOOL certificateIsValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);

    // Get local and remote cert data
    NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate));
    NSString *pathToCert = [[NSBundle mainBundle]pathForResource:@"github.com" ofType:@"cer"];
    NSData *localCertificate = [NSData dataWithContentsOfFile:pathToCert];

    // The pinnning check
    if ([remoteCertificateData isEqualToData:localCertificate] && certificateIsValid) {
        NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
        completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
    } else {
        completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, NULL);
    }
}

At the beginning of the method, we use SecTrustGetCertificateAtIndex to get the certificate reference from the challenge.protectionSpace.serverTrust which contains the server’s SSL certificate data. After that, we set the policies (in this case SSL) to be used in the certificate evaluation – SecTrustSetPolicies. The certificate is evaluated using SecTrustEvaluate, which can return one of the following SecTrustResultType results:

Swift
	public var kSecTrustResultInvalid: Int { get }
public var kSecTrustResultProceed: Int { get }
@available(*, deprecated)
public var kSecTrustResultConfirm: Int { get }
public var kSecTrustResultDeny: Int { get }
public var kSecTrustResultUnspecified: Int { get }
public var kSecTrustResultRecoverableTrustFailure: Int { get }
public var kSecTrustResultFatalTrustFailure: Int { get }
public var kSecTrustResultOtherError: Int { get }
Objective C
	typedef uint32_t SecTrustResultType;
enum {
    kSecTrustResultInvalid = 0,
    kSecTrustResultProceed = 1,
    kSecTrustResultConfirm SEC_DEPRECATED_ATTRIBUTE = 2,
    kSecTrustResultDeny = 3,
    kSecTrustResultUnspecified = 4,
    kSecTrustResultRecoverableTrustFailure = 5,
    kSecTrustResultFatalTrustFailure = 6,
    kSecTrustResultOtherError = 7
};

If we get anything else other than the kSecTrustResultProceed and kSecTrustResultUnspecified result, we can consider the certificate to be invalid (untrusted).

So far we’ve done nothing but checked for the remote server’s certificate evaluation. For the SSL pinning check we need to get the NSData from the SecCertificateRef which we got from the challenge.protectionSpace.serverTrust and get the NSData from the locally saved “.cer” certificate file. The magic of SSL pinning happens using one of the most basic methods of comparison – isEqual:

If the remote server’s certificate NSData isEqualToData of the local certificate, and the evaluation passes with no issues, we can verify the server’s identity and proceed with communication, as well as continue executing the request with the completionHandler(NSURLSessionAuthChallengeUseCredential, credential) method.

However, if the data objects are not equal, we cancel the execution of the dataTask with the completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, NULL) method and, in that way, reject the communication with the server.

AlamoFire

SSL pinning using AlamoFire is fairly simple. Retrieving data for the certificate is optional as AlamoFire has the ServerTrustPolicy.certificatesInBundle() method which returns all the certificates within the bundle. First we create the ServerTrustPolicy object in which we load the certificate(s). To instantiate a Manager object with SSL pinning, we need to provide the ServerTrustPolicyManager object which is instantiated with a dictionary that maps the domain name to the ServerTrustPolicy object.

This is important as it means that we are using a slightly different approach to SSl pinning with AlamoFire than with the NSURLSession and AFNetworking. In AlamoFire, we are pinning only the predefined domain(s) – in this case github.com, and we don’t use pinning for all other domains. In NSURLSession and AFNetworking implementation, we cancel all requests except the ones for the pinned server, as all other servers fail when the SSL pinning check is performed.

	func configureAlamoFireSSLPinning {
        let pathToCert = NSBundle.mainBundle().pathForResource(githubCert, ofType: "cer")
        let localCertificate:NSData = NSData(contentsOfFile: pathToCert!)!

        self.serverTrustPolicy = ServerTrustPolicy.PinCertificates(
            certificates: [SecCertificateCreateWithData(nil, localCertificate)!],
            validateCertificateChain: true,
            validateHost: true
        )

        self.serverTrustPolicies = [
            "github.com": self.serverTrustPolicy!
        ]

        self.afManager = Manager(
            configuration: NSURLSessionConfiguration.defaultSessionConfiguration(),
            serverTrustPolicyManager: ServerTrustPolicyManager(policies: self.serverTrustPolicies)
        )
}

func alamoFireRequestHandler {
        self.afManager.request(.GET, self.urlTextField.text!)
            .response { request, response, data, error in
         // response management code
     }
}

Every request for the github.com domain goes through SSL pinning validation!

AFNetworking

Using SSL pinning in AFNetworking is pretty simple and straightforward. All you need to do is assign an AFSecurityPolicy policy object with the policyWithPinningMode:AFSSLPinningModePublicKey to the AFHTTPRequestOperationManager.

	AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
manager.securityPolicy = policy;

By default, AFNetworking will scan through your bundle and check for all “.cer” files, and add them to the manager.securityPolicy.pinnedCertificates array. That’s why there’s no need to add them manually. However, if there is a reason for using a specific certificate for a specific purpose (often there is; when we add multiple targets to the project which talk to different servers), we can add them selectively with the following bit of code:

	NSString *pathToCert = [[NSBundle mainBundle]pathForResource:@"github.com" ofType:@"cer"];
NSData *localCertificate = [NSData dataWithContentsOfFile:pathToCert];
manager.securityPolicy.pinnedCertificates = @[localCertificate];

And that’s it! Every request we create using the previously configured and instanced AFHTTPRequestOperationManager will be using SSL pinning when communicating with the target server.

An example project of SSL pinning is available on GitHub:

The pinned SSL certificate which is saved within the app’s bundle belongs to github.com. Feel free to check it out!

Bottom line

Although the SSL connection is considered secure and is widely used whenever an encrypted connection is needed, another layer of protection is always welcomed when building high-risk apps. SSL pinning allows us to verify the server’s identity on top of the SSL chain of trust verification.

With SSL pinning, we can refuse all connections except the ones with the designated server whose SSL certificate we’ve saved into our local bundle. A potential drawback is that we need to update the app whenever the server’s SSL key is changed, either because it expired, or for some other reason.

With the release of the iOS 9, the App Transport Security library was also introduced. By default, ATS denies all insecure connections which do not use at least the TLS 1.2 protocol. The TLS protocol is a replacement for the SSL protocol, but still, both of them are often referred to as SSL. With SSL pinning in mind, there is no difference between having TLS or SSL as the underlying implementation – the basic concept remains the same.

Need to implement SSL pinning in an Android app? Check this out.