Ktor: SSL pinning for iOS

Created on 9 Nov 2019  ·  24Comments  ·  Source: ktorio/ktor

How to make SSL pinning for iOS. As I am getting response but for other thing its not working.

Getting crash near this code
val remoteCertificateData : NSData = SecCertificateCopyData(certificate) as NSData

This is the error.

Uncaught Kotlin exception: kotlin.TypeCastException

Here is my code.

`override fun URLSession(
session: NSURLSession,
didReceiveChallenge: NSURLAuthenticationChallenge,
completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Unit
) {
val serverTrust = didReceiveChallenge.protectionSpace.serverTrust
val certificate = SecTrustGetCertificateAtIndex(serverTrust,0)

            var result: SecTrustResultType = 0u

            memScoped{
                val nativeResult = alloc<SecTrustResultTypeVar>()
                nativeResult.value = result

                SecTrustEvaluate(serverTrust!!, nativeResult.ptr)
            }


            val remoteCertificateData : NSData = SecCertificateCopyData(certificate) as NSData

            val bundle = NSBundle.bundleForClass(objc_getRequiredClass("IosClientEngine"))
            Logger.debug("pathToCert","$bundle")

            val pathToCert = bundle.pathForResource("MyCertificate","cer")

            val localCertificate : NSData = NSData.dataWithContentsOfFile(pathToCert!!)!!

            if (localCertificate == remoteCertificateData) {
                completionHandler(NSURLSessionAuthChallengeUseCredential,NSURLCredential.create(serverTrust))

            } else {
                completionHandler(NSURLSessionAuthChallengeUseCredential, null)

            }
        }`
feature

All 24 comments

Any suggestions for a workaround for self-signed certificate handling on iOS?

I recently had to tackle implementing iOS certificate pinning. Here is a blog post I just published detailing my solution. Feel free to use it to help you work out your solution. Hope it helps someone.

https://medium.com/@alistairsykes/kotlin-multiplatform-ios-certificate-pinning-fd1abba5ca8f

When I get it right, I need to add your CertificatePinner and use it like so:

HttpClient(Ios) {
 engine {
        challengeHandler = CertificatePinner.Builder()
            .add("publicobject.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
            .build()
    }
}

This would be great and easy to use but when coping the class from your article, the imports are missing and there are many unresolved references.

You're mentioning that a fix for our workaround will be available with f992dd3cad59d8d4262c0c924c59154ce03c0181 which is now merged in ktor 1.3.2.

@alistairsykes Would you please update your article with an example how to add certificate pinning for iOS using this new version?

I have update my blog @hardysim to better reflect using this with version 1.3.2.

The api changed slightly since writing my blog and 1.3.2 release. The implementation will now look more like this:

HttpClient(Ios) {

    // ...

    engine {
        val builder = CertificatePinner.Builder()
            .add("publicobject.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
        handleChallenge(builder.build())
    }
}

Not sure which imports you'd be missing. Recommend checking out:
https://ktor.io/clients/http-client/quick-start/client.html
https://ktor.io/clients/http-client/multiplatform.html

Maybe you are looking for:

import io.ktor.client.HttpClient
import io.ktor.client.engine.ios.Ios

In terms of imports for the CertificatePinner itself, it depends where you copy this class to, and what package name you use. You will see in my blog that I have set package as package com.example.util, but recommend you package it wherever best suits your project.

Also I found this, really helpful when it comes to imports.

Hope that was helpful, shout if you'd like more information on my post.

Blog link again for reference

The blog post seems not to be updated. At least, the signature of invoke() does not match the one from ktor 1.3.2. Maybe you can post the class here as well?

The imports for are missing for toNSData() and SecCertificateRef.getPublicKeyBytes() complains about the wrong return type (requires ByteArray? but found Unit). Maybe because toByteArray() is missing as well?!

Sorry about that @hardysim . Didn't update the gist correctly.

Updated main gist:

  • Should now have the correct imports
  • invoke() updated to the correct signature

Updated cutils gist:

  • Updated to include toNSData() and toByteArray()

Try again. Really sorry about that.

Yeah, getting closer 🎉.

Still missing imports for @VisibleForTesting (but I can just remove that for now), NSMutableDataand ByteVar.

And I get two errors on base64EncodedStringWithOptions():

The integer literal does not conform to the expected type NSDataBase64EncodingOptions /* = ULong */

I can use import platform.Foundation.NSMutableData but it's still missing appendBytes() and reinterpret().

main gist:

  • Removed @VisibleForTesting annotation and set functions to private.

cutils gist:

  • Updated imports

I'm not seeing the base64EncodedStringWithOptions() error. Could this be due to the missing imports.

Hopefully that resolves it. Sorry again @hardysim.

Hi @alistairsykes, could you make the PR with the CertificatePinner? It would be nice to support it out of the box.

@alistairsykes it seems to compile now 👍 But I still get those 2 errors:

image

And yes, a PR would be great.

Hi @alistairsykes, could you make the PR with the CertificatePinner? It would be nice to support it out of the box.

I'd love to.

@alistairsykes Thanks for providing this implementation. Unfortunately, I see the same error on base64EncodedStringWithOptions() as @hardysim.
Additionally, when setting up the CertificatePinner for initial failure, the request fails as expected, but with "Server trust is invalid", which does not give the certificate chain to be used. This happens on the ios simulator, if it makes a difference?!

Could you try using 0.convert()?. It looks like the type is platform dependent.

I included a fun on the builder validateTrust which might help with the emulator issue.

@e5l thanks, this works! Beforehand I had used 0u.

@alistairsykes this actually helps as well, but only partially: validateTrust does the trick I receive

HttpClient: Certificate pinning failure!
      Peer certificate chain:
        sha256/UIUWcLlepHcfOtECdRs0Hzrnu8QpOsZkFHXNOP9t2vw=: *.cognitive.microsoft.com
      Pinned certificates for text-recognizer.cognitiveservices.azure.com:
        sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=

but when I replace sha256/AAA... with the above sha256/UIUW..., it looks to me like I am back to square one without the CertificatePinner:

Exception in http request: Error Domain=NSURLErrorDomain Code=-1202 "The certificate for this server is invalid. You might be connecting to a server that is pretending to be “text-recognizer.cognitiveservices.azure.com” which could put your confidential information at risk." UserInfo={NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, _kCFStreamErrorDomainKey=3, NSErrorPeerCertificateChainKey=(
        "<cert(0x7fb49303aa00) s: *.cognitive.microsoft.com i: Microsoft IT TLS CA 5>",
        "<cert(0x7fb493034200) s: Microsoft IT TLS CA 5 i: Baltimore CyberTrust Root>"
    ), NSErrorClientCertificateStateKey=0,

Any chance you have a suggestion?

@alistairsykes I finally got around to test this on an actual iPhone and it looks like this is only an issue on the ios simulator?! Just wanted to let you know. Thanks for the advice and the great work

Here is the code that worked for me.
https://stackoverflow.com/questions/58777854/ktor-multiplatform-ssl-pinning-for-ios-in-kotlin

I hope it will help.

override fun URLSession(
    session: NSURLSession,
    didReceiveChallenge: NSURLAuthenticationChallenge,
    completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Unit) {

    val serverTrust = didReceiveChallenge.protectionSpace.serverTrust
    var result: SecTrustResultType = 0u

    memScoped{
        val nativeResult = alloc<SecTrustResultTypeVar>()
        nativeResult.value = result
        SecTrustEvaluate(serverTrust!!, nativeResult.ptr)
    }

    val serverCertificate = SecTrustGetCertificateAtIndex(serverTrust,0)
    val serverCertificateData = SecCertificateCopyData(serverCertificate)
    val data = CFDataGetBytePtr(serverCertificateData)
    val size = CFDataGetLength(serverCertificateData)

    val cert1 = NSData.dataWithBytes(data,size.toULong())
    val pathToCert = NSBundle.mainBundle.pathForResource("Your Certificate","cer")

    val localCertificate : NSData = NSData.dataWithContentsOfFile(pathToCert!!)!!

    if (localCertificate == cert1) {
        completionHandler(NSURLSessionAuthChallengeUseCredential,NSURLCredential.create(serverTrust))
    } else {
        completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, null)
    }
}

@alistairsykes is there a way to perform pinning only for the root and intermediate certificate?
Based on tries I can see that I must perform pinning in the whole certificate chain in order to get a succesful response.

After the fix, you need to pin only a single certificate in the chain

@e5l in which version I can find the fix? I suppose 1.5.2 ?

Sure, 1.5.2 on the last week of Feb

@e5l is there an alpha version I can test;

Sure, the latest EAP 22: https://ktor.io/eap/

Was this page helpful?
0 / 5 - 0 ratings

Related issues

shinriyo picture shinriyo  ·  4Comments

evgfilim1 picture evgfilim1  ·  4Comments

dedward3 picture dedward3  ·  4Comments

gabin8 picture gabin8  ·  4Comments

PatrickLemke picture PatrickLemke  ·  3Comments