How to implement certificate pinning in Kotlin Multiplatform using Ktor
Pinning certificates is a common practice when interacting with remote APIs. It is the act of constraining which certificates you trust. This helps to defend against attacks on certificate authorities. It also helps combat man-in-the-middle attacks.
There is a multitude of information available about certificate pinning. Here is some to get you started:
Kotlin Multiplatform (KMP) is a way of writing cross-platform code in Kotlin. It is not about compiling all code for all platforms, and it doesn’t limit you to a subset of common APIs. KMP provides mechanisms for writing platform-specific implementations for a common API.
Ktor is a KMP framework for building asynchronous servers and clients. Ktor's client libraries enable you to author multiplatform code which connects to remote APIs.
Ktor provides engines that perform the platform-specific requests. These engines conform to a common interface. In common code, you can make HTTP requests which go through the engines.
For Android, you can use an OkHttp engine. Built into Ktor there is an iOS engine that uses the NSURLSession classes.
OkHttp has long supported certificate pinning against public keys. Implementation is simple, and I love their approach to this problem.
On iOS, this is not so simple. You have to override a function on NSURLSessionDataDelegateProtocol and configure an NSURLSession with it.
Represented in Kotlin:
fun invoke( session: NSURLSession, challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Unit )
Within this function, you need to check various aspects of the challenge. Ultimately checking the certificate matches your expectations.
I set out to write this certificate pinning functionality into a succinct class for KMP. Since we need to be able to override a delegate method, we are required to use at least version 1.3.2 of Ktor.
This is the class I came up with. It is inspired by OkHttp's CertificatePinner and matches pretty closely with their implementation.
To get started, configure certificate pinning with a broken configuration. Then read the expected configuration in the logs when the connection fails. Be sure to do this on a trusted network, and without man-in-the-middle tools like Charles or Fiddler.
For example, to pin https://publicobject.com, start with a broken configuration:
As expected, this fails with an exception, see the logs:
Now paste the public key hashes into the certificate pinner's configuration:
The first thing to note is that CertificatePinner implements ChallengeHandler. This means it implements the required delegate method. It will be called within the iOS engine.
This Builder and Pin classes enable the user to easily create a set of public keys to pin against. They also provide wildcard functionality. You can read about this in the class documentation.
The invoke function is where the actual pinning functionality takes place.
First I retrieve the hostname from the challenge. Then I check to see if there are any pins against that host. I then check to see if the authenticationMethod is suitable for certificate pinning.
See apple documentation for details.
I retrieve the serverTrust and check it's validity (if configured to).
Now comes the point where I check the certificates public keys against the ones that are pinned. Most of the complexity here is retrieving the public key in the format I want.
Once in the correct format, we check it matches the pinned ones and return the result.
This was a challenging problem to solve. It involved using Kotlin's C-interop features, which can be difficult to grasp.
This class is available for you to use and change as you like. Hopefully, it helps you to solve this problem too.
Check out Alistair's other musings on all things Kotlin:
There's testing for multiple platforms here
Perhaps you'd be interested in strategies for multiplatform project structure here
Coding and culture in Kotlin here
Or if you're completely new to Kotlin, have a look at our overview on it here
Search over 300 blog posts from our team
Subscribe to our monthly digest of blogs to stay in the loop and come with us on our journey to make things better!