Skip to content

DMT

Public key hash pinning on iOS

4 min read

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.

by OWASP - Certificate and Public Key Pinning

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.

The problem

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 servers` 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 it's 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.

Solution

A certificate can be changed in a way that leaves it's public key (and it's 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. Above 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 it's 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 whathever 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.

Implementation

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.

Public interface

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.

1public final class PublicKeyPinner {
2 /// Stored public key hashes
3 private let hashes: [String]
4
5 public init(hashes: [String]) {
6 self.hashes = hashes
7 }
8
9 /// Validates an object used to evaluate trust's certificates by comparing their public key hashes
10 /// to the known, trused key hashes stored in the app.
11 /// - Parameter serverTrust: The object used to evaluate trust.
12 /// - Parameter domain: The domain from where we expect our trust object to come from.
13 public func validate(serverTrust: SecTrust, domain: String?) -> Bool {
14 return false
15 }
16}

Validation

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:

1if let domain = domain {
2 let policies = NSMutableArray()
3 policies.add(SecPolicyCreateSSL(true, domain as CFString))
4 SecTrustSetPolicies(serverTrust, policies)
5}

Next, we need to check the validity of our SecTrust object:

1// Check if the trust is valid
2var secResult = SecTrustResultType.invalid
3let status = SecTrustEvaluate(serverTrust, &secResult)
4
5guard status == errSecSuccess else { return false }

Now, that we have a valid trust object, it's time to evaluate it's 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:

1// For each certificate in the valid trust:
2for index in 0..<SecTrustGetCertificateCount(serverTrust) {
3 // Get the public key data for the certificate at the current index of the loop.
4 guard let certificate = SecTrustGetCertificateAtIndex(serverTrust, index),
5 let publicKey = SecCertificateCopyPublicKey(certificate),
6 let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil) else {
7 return false
8 }
9
10 // Hash the key, and check it's validity.
11 let keyHash = hash(data: (publicKeyData as NSData) as Data)
12 if hashes.contains(keyHash) {
13 // Success! This is our server!
14 return true
15 }
16}
17// If none of the calculated hashes match any of our stored hashes, the connection we tried to establish is untrusted.
18return false

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.

Hashing

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:

1/// ASN1 header for our public key to re-create the subject public key info
2private let rsa2048Asn1Header: [UInt8] = [
3 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
4 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00
5]

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.

1import CryptoSwift
2import CommonCrypto
3
4#if canImport(CryptoKit)
5import CryptoKit
6#endif
7
8public final class PublicKeyPinner {
9 ...
10 /// Creates a hash from the received data using the `sha256` algorithm.
11 /// `Returns` the `base64` encoded representation of the hash.
12 ///
13 /// To replicate the output of the `openssl dgst -sha256` command, an array of specific bytes need to be appended to
14 /// the beginning of the data to be hashed.
15 /// - Parameter data: The data to be hashed.
16 private func hash(data: Data) -> String {
17 // Add the missing ASN1 header for public keys to re-create the subject public key info
18 var keyWithHeader = Data(rsa2048Asn1Header)
19 keyWithHeader.append(data)
20 // Check if iOS 13 is available, and use CryptoKit's hasher
21 if #available(iOS 13, *) {
22 return Data(SHA256.hash(data: keyWithHeader)).base64EncodedString()
23 } else {
24 ...
25 }
26 }
27}

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!

1var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
2_ = keyWithHeader.withUnsafeBytes {
3 CC_SHA256($0.baseAddress!, CC_LONG(keyWithHeader.count), &hash)
4}
5return Data(hash).base64EncodedString()

CryptoSwift, just like CryptoKit, provides a more elegant way of doing things, having all the magic hidden:

1return keyWithHeader.sha256().base64EncodedString()

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.

Summary

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.