The primary purpose of this article is to provide an implementation example of public key hash pinning on iOS.
The underlying concepts and theories are not thoroughly explained in this article, although I will provide the sources I used to learn about the topic.
What is Pinning?
Pinning is the process of associating a host with their expected X509 certificate or public key. Once a certificate or public key is known or seen for a host, the certificate or public key is associated or ‘pinned’ to the host.
In an iOS development context, this means that we want to associate a certificate or a public key to the server we communicate with and only allow communication with the said server.
One application that my company was working on used certificate pinning to ensure the safety of our communication channel.
This meant that an X509 certificate was bundled with the application, and during each and every request, we compared the server’s certificate to the bundled one, and if they matched, we deemed the channel secure.
This method is considered secure, but it holds a couple of risks:
— Because of bundling the certificate in our .ipa, we basically expose it in case of decompilation or reverse-engineering. This is bad enough in itself, but
— If our server changes its certificate, our app breaks.
These risks have motivated us to find, and implement a more bulletproof method of securing our communication channels.
Another motivation was that the Android team working on the same application used a different methodology, which proved to solve the risks mentioned above: this method was to pin the hash of the Public Key of our server’s certificate.
A certificate can be changed in a way that leaves its public key (and its hashed value) intact.
This gives us an opportunity to only bundle the hash of the public key of our certificate, and to match it with the hash of the certificate’s public key received during a network request.
Note: hashing is complex and complicated, so I advise you to use algorithms provided by trusted sources, eg. OpenSSL)
This method is more complex than simply pinning the certificates, but we think that it’s well worth it.
Below you can find a list of steps for obtaining the public key hash of your server’s certificate using a handful of OpenSSL commands (common OpenSSL commands):
Acquire the certificate of your server. This can be done by either asking for it from your backend developer colleagues, or by simply downloading it from a browser (eg. if your server has a public website).
openssl s_client -connect your-server.com:443 -showcerts < /dev/null | openssl x509 -outform der > server_cert.der
— When you have the certificate, you need to extract and optionally save its public key in a PEM format.
openssl x509 -inform der -in server_cert.der -pubkey -noout > server_cert_public_key.pem
— After having the certificate, you can hash it with whatever hashing algorithm you prefer (only make sure that it is a secure algorithm). I used SHA256 to hash our key. After calculating the hash, I simply encoded it with Base64 encoding, to make it easier to store, and read.
cat server_cert_public_key.pem | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
The output of the commands listed above is the hash of your server’s public key, which can now be added to your application.
Now that we have our hash, it’s time to make some good use of it.
As you may know, there are many networking libraries used in iOS development, therefore there are many ways to integrate any kind of pinning in your app. Because of this, I will only show you the core of a possible implementation:
— extracting the public key from the received certificate,
— hashing it,
— and matching it to your stored hash.
Note: on iOS, during network communication, you receive a chain of certificates in an object used to evaluate trust (SecTrust). This provided implementation assumes that you store multiple hashes, one hash belonging to the public key of a certificate in this chain.
Let’s start with the bare structure of our PublicKeyPinner class.
We need a property to store the hashes we need to match and a method that validates our trust object. Optionally we can specify the domain from where we plan to receive our trust object.
Now let’s start to implement the body of our validate method. First, if a domain is provided, we need to set it as a SecPolicy:
Next, we need to check the validity of our SecTrust object:
Now, that we have a valid trust object, it’s time to evaluate its trustiness.
To do this, we have to iterate through the chain of certificates contained in the trust object. In each iteration, we have to retrieve the public key data of the current certificate, hash it and compare this hash against our stored hashes:
This concludes the validation part of our implementation. There may be many unknown methods in the code snippets above, for reference check out the documentation.
You may notice a reference to the hash(data: Data) method and you guessed it, this is the next and final part of our PublicKeyPinner implementation.
First and foremost, the publicKeyData we saw above is missing some key (😉) information: the ASN1 header for public keys to re-create the subject public key info (more info about this here). Basically, we need an array of unsigned integers that contain an indication of the algorithm, and any algorithm parameters, with which the public key is to be used.
If you remember from earlier in the article, we used OpenSSL-s dgst function with sha256 hashing to create our hashes. To recreate the same hashes in our code, the following bytes are needed:
Now that we have our header, we can implement our hash method. There are many libraries that provide cryptographic functions, for demonstration purposes I will use a 3rd party library (CryptoSwift) and two frameworks provided by Apple: CryptoKit (iOS 13+) and CommonCrypto.
First, let’s see how to use Apple’s new CryptoKit framework, more precisely it’s SHA256 hasher. We create a variable named `keyWithHeader` to store the header and the public key data retrieved from the certificate. If iOS 13 is available, we create a digest of our data and return it’s base64 encoded string.
Now let’s see the else branch. Here, we can see two more examples of hashing: one using CommonCrypto and one using CryptoSwift. If you were not delighted enough by the great API provided by the Security framework, CommonCrypto-s API will be a true snack for you!
CryptoSwift, just like CryptoKit, provides a more elegant way of doing things, having all the magic hidden:
And with that, our PublicKeyPinner is ready for some (secure) action. To view all of the source code in one place, check out this gist.
We’ve come a long way in this article, and have touched some complex topics, such as Cryptographic Hashing and Certificate/Public Key Hash pinning.
I’ve learned a lot while researching and solving the problems listed above, and I hope you did too by reading this article.
Note: if you prefer to use third-party libraries, the functionality of the above implementation (and many more features) can be found in an open-source library called TrustKit.
At Supercharge, we are a next-generation innovation partner working with our clients to create transformative digital solutions. If you liked this article, check out some of Supercharge’s other articles on our blog, or follow us on LinkedIn, and Facebook. If you’re interested in open positions, follow this link.