SSL certificate pinning on iOS using TrustKit
We have already covered why certificate pinning in mobile apps is important and have shown how to implement it both in iOS and Android. For the sake of simplicity, we had to omit a lot of nasty details and corner cases from our example. In the real world, the code might get complex with time very quickly, when you have to support various legacy iOS platforms, various popular networking libraries out there etc. Luckily, nice folks at DataTheorem have created and open-sourced a framework for SSL pinning which eliminates that simplifies most of it. The framework, called TrustKit, and makes it very easy to integrate pinning into your mobile application. In the following tutorial we'll show how to use it on iOS.
Installing the framework
It is trivial to install the TrustKit framework using CocoaPods.
Just run the following to your Podfile:
pod 'TrustKit'
and run:
pod install
Alternatively, TrustKit is available through Carthage as well.
Add the following line to your Cartfile:
github "datatheorem/TrustKit"
and run:
carthage build --platform iOS
Preparing the pins
As we explained in our introduction tutorial, you can pin either the certificate itself or the public key from within the certificate. The latter is preferred, since on many occasions, for security reasons, the certificates are being rotated regularly and we will have to re-deploy our application with the new certificate, with a risk of some users becoming locked out. We've also shown that it is better for the app to store just the hash of the public key, not the key itself. TrustKit takes the same approach. For each domain it expects to have a list of hashes of public keys to pin. It does however expects them in a base64 encoded format, so for convenience reason a special tool is provided within the TrustKit package to help prepare them.
As with the previous example, we will use www.google.com as an example. Lets first extract the certificate from the domain:
openssl s_client -connect www.google.com:443 -showcerts < /dev/null | openssl x509 -outform DER > google.der
Now lets run the provided helper script to extract the hash of the public key:
python get_pin_from_certificate.py --type DER ../google.der
In our example we are using a binary DER format when working with certificates. You have your certificate provided to you in a different text PEM format. In that case, simply omit the --type DER part and call the script on your PEM file directly.
The result is going to be the following:
CERTIFICATE INFO
----------------
subject= /C=US/ST=California/L=Mountain View/O=Google Inc/CN=www.google.com
issuer= /C=US/O=Google Inc/CN=Google Internet Authority G2
SHA1 Fingerprint=FB:BD:7E:87:A8:A6:EB:FC:3E:E0:BE:F8:2E:3A:EB:9C:7D:86:7F:AA
TRUSTKIT CONFIGURATION
----------------------
kTSKPublicKeyHashes: @[@"U9GGUC1+PGI7SLb8XVVuuBPLkQKbQ2LnfU5Wfe7WAAg="] // You will also need to configure a backup pin
kTSKPublicKeyAlgorithms: @[kTSKAlgorithmRsa2048]
Note that the output of the script has both the hash and the algorithm used. This is in fact a nice touch, TrustKit supports several algorithms and will autodetect and handle it automatically for us. Notice the output of that script is even giving us Objective-C elements so we can copy paste them into our code directly. Unfortunately we will be using Swift for our example, but TrustKit gets extra points for this.
Initializing TrustKit
Now its time to initialize the framework:
let trustKitConfig = [
kTSKSwizzleNetworkDelegates: false,
kTSKPinnedDomains: [
"google.com": [
kTSKIncludeSubdomains : true,
kTSKPublicKeyAlgorithms: [kTSKAlgorithmRsa2048],
kTSKPublicKeyHashes: [
"U9GGUC1+PGI7SLb8XVVuuBPLkQKbQ2LnfU5Wfe7WAAg=",
"WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18="
],]]] as [String : Any]
TrustKit.initialize(withConfiguration: trustKitConfig)
The pinned domains is an array, so we can actually pin several different domains in our application, each with a specific set of hashes and settings.
Why do we have 2 hashes in the hashes list? TrustKit will actually fail and not let you start with one. The idea is you have to create a backup key, so in case your primary gets compromised, you will have a way to replace the certificates on your server without locking out your users.
Validating secure connections
Now that everything is set up, we are ready to use the TrustKit mechanism to actually validate each and every connection our application is going to open. In order to do this, we will make sure our URLSession delegate implements the didReceive challenge method:
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) {
// Let TrustKit handle it
TSKPinningValidator.handle(challenge, completionHandler: completionHandler)
}
Thats it, we are done. Every time our URLSession will try to establish an SSL connection to google.com or one of its subdomains, TrustKit will make sure to extract the certificate and the public key, hash the key and compare it to the list we've passed during initialization. And it will work on all iOS/watchOS/tvOS and macOS versions.