Prevent man-in-the-middle attacks in your app, whether you use Apple’s APIs or Alamofire
Privacy — like eating and breathing — is one of life’s basic requirements.
— Katherine Neville
Nearly every iOS app communicates with servers to retrieve information. Sometimes this information is sensitive and you don’t want anyone to be able to access them (for example, their username and password to sign in to the app).
At the launch of iOS 9, Apple introduced App Transport Security, which forces apps to use a secure network connection by adopting HTTPS protocol and Transport Layer Security (TLS). What does this mean? It means our apps can’t communicate with a server through a non-secure connection. Actually, it’s possible to bypass this restriction but to avoid potential attacks, it’s not recommended.
So my data is safe, right? The answer is no! Even if TLS protects the transmitted data, it’s easier for an attacker to set up man-in-the-middle attacks using hacked or self-signed certificates. This means they can capture data moving to and from your app.
In this article, I’ll explain how to prevent these kind of attacks in order to make your app data safe. We will cover some basic knowledge on the following topics:
- Symmetric and asymmetric encryption.
- Certificate authority.
First, we need to understand how TLS and man-in-the-middle attacks work, to understand how to prevent them. TLS allows you to transmit data over a secure network, following three phases:
- The client initiates a connection with the server by sending a message that includes the supported version of TLS and the cipher suite used for encryption. The server responds with the selected cipher suites and a digital certificate. The client verifies that those digital certificates are authentic (i.e. issued by a certificate authority).
- If the validation succeeds, the client generates a pre-master key encrypted with the server’s public key included in the certificate. The server decrypts the secret with its private key. Both use this secret to generate a master key.
- Both use encryption of plaintext and decryption of ciphertext by using the master key and symmetric encryption.
But, what is a certificate? A certificate is proof of identity of the server. It’s just a file that contains information about the server that owns the certificate and follows the X.509 standard. The client only trusts a server that can provide a valid certificate signed by one of the trusted Certificate Authorities, otherwise, the connection will be aborted.
In order to validate it, the app verifies:
- The expiry date.
- The digital signature.
A man-in-the-middle is an attack in which the attacker secretly listens and eventually alters the communication between two parties. It’s easier than you may think to use this kind of attack to intercept data that an app is sending to a server. All you need is a SSL Proxy Server.
The SSL proxy performs encryption and decryption between the client and the server (the keys are different for both direction). An example is Charles. It sits between your app and the internet and you can inspect and even change data midstream to test how your app responds.
- Your app encrypts data with the Charles ProxyCertificate public’s key, instead of the server’s. The Charles Certificate is validated because the developer can install it in his device.
- Charles is able to decrypt all data with its private key and then communicate with the server using the server’s public key.
This means that any developer can read all the data that your app is sending over the network!
Here’s how to set up Charles.
Fortunately, there’s a simple way to prevent this kind of attack through a technique called SSL Pinning.
This technique validates the server certificates again, even after SSL handshaking. The developer embeds a list of trustful certificates inside the client application and compare them with the server certificates during runtime. In case of a mismatch between the server’s certificates and the local copy, the connection is simply aborted.
This means that the app will check the Charles’s certificate and, even if it’s trusted, it will be different from the local certificate embedded inside the app.
One con is that a pinned certificate has an expiration date and this requires an app update, unless the developer also pins future certificates inside the app. Actually, instead of pinning the whole certificate, you can just use the hashed public key (this is a better approach because we can reuse the same hashed key also in future certificates).
Evaluating trust is a two-step process.
- Validate the certificate’s digital signature. Your app can rely on any of the root certificates embedded in iOS or you can supply your own.
- Testing the certificate against a trust policy. The policy indicates how particular fields or extensions of a certificate should be in order to be trusted.
All of this activity is facilitated by an instance of the SecTrust
object that you prepare with one or more certificates and policy objects.
Prepare the certificate
Apple provides the SecCertificate
to represent a X.509 certificate. If you add the certificate inside the bundle you can retrieve it with:
let filePath = Bundle.main.path(forResource: name, ofType: type)!let data = try! Data(contentsOf: URL(fileURLWithPath: filePath))let certificate = SecCertificateCreateWithData(nil, data as CFData)
Here you can find also more complex solutions — for example, using keychain and identity. There isn’t a “best” solution, because it mostly depends on the problem and on the level of security you need in your app. I suggest you read up and try them all, then decide which one suits your needs.
Prepare a policy
The best option is usually to use one of the predefined policies. For example, you could use the standard basic policy for X509 certificates with:
let policies = SecPolicyCreateBasicX509()
This is probably enough for your app, but if you need more flexibility you can create your own SSL policy with the SecPolicyCreateSSL(_:_:)
.
The SecTrust
Now it’s time to validate both certificates and policies. Apple facilitates all these operations with a SecTrust
object.
var optionalTrust: SecTrust?
let status = SecTrustCreateWithCertificates(certArray as AnyObject,
policy,
&optionalTrust)
guard status == errSecSuccess else { return }
let trust = optionalTrust!
SecTrustCreateWithCertificates(_:_:_:)
creates a trust management object based on the provided certificates and policies.
You can use SecTrustEvaluateWithError(_:_:)
to validate a trust object. From the documentation:
This method evaluates a certificate’s validity to establish trust for a particular use — for example, in creating a digital signature or to establish a Secure Sockets Layer connection. The method validates a certificate by verifying its signature plus the signatures of the certificates in its certificate chain, up to the anchor certificate, according to the policy or policies included in the trust management object.
If the trust management instance lacks some of the certificates needed to verify the leaf certificate,
SecTrustEvaluateWithError(_:_:)
searches for certificates:- In the user’s keychain.
- Among any certificates you previously provided by calling SecTrustSetAnchorCertificates(_:_:).
- In a system-provided set of keychains provided for this purpose.
- Over the network, if certain extensions are present in the certificate used to build the chain.
The only thing that you need now is to evaluate the SecTrust
that you receive during a network call with your policies and compare the certificate with the one pinned inside the app.
Let’s take a look at an example:
This is a simple example, but there are many other parameters and objects to handle all the possible validations of a SecTrust
. I suggest you carefully read all the documentation provided by Apple and test it before trying to use it in your app.
Here the main link.
As you can see, using the normal API is quite complex. I recommend using Alamofire
, an external library that provides a higher level of API making your life simpler.
Everything is based on the ServerTrustEvaluating
protocol that provides a way to perform any sort of server trust evaluation. Alamofire includes many different types of trust evaluators, for example:
DefaultTrustEvaluator
: Allows you to control whether to validate the host provided by the challenge.RevocationTrustEvaluator
: Checks the status of the received certificate to ensure it hasn’t been revoked.PinnedCertificatesTrustEvaluator
: The server trust is considered valid if one of the pinned certificates matches one of the server certificates.PublicKeysTrustEvaluator
: The server trust is considered valid if one of the pinned public keys matches one of the server certificate’s public keys.
You only have to choose which ServerTrustEvaluating
to use for each API:
let evaluators: [String: ServerTrustEvaluating] = [
"cert.example.com": PinnedCertificatesTrustEvalutor(),
]let manager = ServerTrustManager(evaluators: serverTrustPolicies)
Then create a session with them:
let serverTrustManager = ServerTrustManager(evaluators: evaluators)self.session = Session(serverTrustManager: serverTrustManager)
You can read the full documentation here.
If you want to do some tests or you need to validate a public API you can download the certificate and pin it to your app.
Open your terminal and write the following:
openssl s_client -connect <url>:443 </dev/null \
| openssl x509 -outform DER -out <filename>.der
This will download the certificate into your current folder.
I hope you enjoyed the article! Fell free to reach out to me if you have any doubts.