Thomas Bandt

Certificate And Public Key Pinning With Xamarin

Are you securing the communication between your app and its backend with HTTPS (SSL/TLS)? Fantastic. But how do you make sure the other side is authentic? Read on on how to do this with Xamarin for iOS and Android.

Published on Tuesday, January 31, 2017

The Problem: Possibly Untrusted Certificates

Pinning a server's certificate (or its public key) enables you to make sure the server your app is talking with is exactly the server you expect it to be.

It is not a silver bullet, but nevertheless fundamentally important because not doing it makes it much easier to attack your app's users with so called Man In The Middle Attacks.

If you want to learn more, take a look at this Pinning Cheat Sheet.

The Solution: Pinning The Cert You Trust

It's easy to do this with Xamarin, isn't it? Yes. And no.

There is a document describing the multiple options we have when it comes to chosing an HttpClient implementation and which way of SSL/TLS implemenation we want to go.

Unfortunately there's (currently) no word about pinning. And that's a bummer, because not every configuration enables us to do this. More on that later, but first a short example on how to validate a certificate by its public key with .NET/Xamarin.

Plan A: ServerCertificateValidationCallback

Set up this method as early as possible in your application, before the first call to your server is being made.

public static class ServicePointConfiguration
{
    private const string SupportedPublicKey = "{PLACE HERE THE PUBLIC KEY}";

    public static void SetUp()
    {
        ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
        ServicePointManager.ServerCertificateValidationCallback = ValidateServerCertficate;
    }

    private static bool ValidateServerCertficate(
        object sender,
        X509Certificate certificate,
        X509Chain chain,
        SslPolicyErrors sslPolicyErrors
    )
    {
        return SupportedPublicKey == certificate?.GetPublicKeyString();
    }
}

If you don't know how to get the public key of a certificate:

openssl s_client -connect {YOUR DOMAIN}:443 | openssl x509 -pubkey -noout

The Problem With Plan A: The Many Flavors Of HttpClient

Besides giving an excellent talk on the topic [DE], Kerry also wrote about all the options we have to set up HttpClient.

At the end of the day it all depends on chosing the right HttpClientHandler:

  • HttpClientHandler: The most compatible. But also very slow and not as sophisticated as their native counterparts. But it fully supports ServerCertificateValidationCallback.
  • CFNetworkHandler and NSUrlSessionHandler: Smaller, faster, using the underlying native code for network communications and transport. But: Not supporting ServerCertificateValidationCallback!
  • AndroidClientHandler: Providing TLS 1.2 support, but not supporting ServerCertificateValidationCallback (out of the box)!

Plan B: ModernHttpClient To The Rescue!

There is a fantastic library called ModernHttpClient. Set it up ...

var httpClient = new HttpClient(new NativeMessageHandler(
    throwOnCaptiveNetwork: false, 
    customSSLVerification: true
));

... and you are done.

Full support for ServerCertificateValidationCallback on both iOS and Android, while leveraging the underlying network stacks of both platforms (and therefore not suffering from all the drawbacks of the default Mono implementation of HttpClient).

Except For The Problem With Plan B On Android

Yep, it's not that easy. We ran into a problem which is described here, so I had to figure out another way.

Fortunately, AndroidClientHandler isn't as closed as its iOS counterparts, so there is a way to implement proper certificate validation:

using System.IO;
using Java.Security;
using Java.Security.Cert;
using Javax.Net.Ssl;
using Xamarin.Android.Net;

namespace NeunundsechzigGrad.Foo
{
    public class DroidTlsClientHandler : AndroidClientHandler
    {
        private TrustManagerFactory _trustManagerFactory;
        private KeyManagerFactory _keyManagerFactory;
        private KeyStore _keyStore;

        protected override TrustManagerFactory ConfigureTrustManagerFactory(KeyStore keyStore)
        {
            if (_trustManagerFactory != null)
            {
                return _trustManagerFactory;
            }
            
            _trustManagerFactory = TrustManagerFactory
                .GetInstance(TrustManagerFactory.DefaultAlgorithm);

            _trustManagerFactory.Init(keyStore);

            return _trustManagerFactory;
        }

        protected override KeyManagerFactory ConfigureKeyManagerFactory(KeyStore keyStore)
        {
            if (_keyManagerFactory != null)
            {
                return _keyManagerFactory;
            }

            _keyManagerFactory = KeyManagerFactory
                .GetInstance(KeyManagerFactory.DefaultAlgorithm);

            _keyManagerFactory.Init(keyStore, null);

            return _keyManagerFactory;
        }

        protected override KeyStore ConfigureKeyStore(KeyStore keyStore)
        {
            if (_keyStore != null)
            {
                return _keyStore;
            }

            _keyStore = KeyStore.GetInstance(KeyStore.DefaultType);
            _keyStore.Load(null, null);

            CertificateFactory cff = CertificateFactory.GetInstance("X.509");
            Certificate cert;

            // Add your Certificate to the Assets folder and address it here by its name
            using (Stream certStream = Android.App.Application.Context.Assets.Open("your_certificate.cert"))
            {
                cert = cff.GenerateCertificate(certStream);
            }

            _keyStore.SetCertificateEntry("TrustedCert", cert);

            return _keyStore;
        }
    }
}

Note: We're not checking the public key here anymore, but the actual certificate. If you don't know how to obtain this cert, here you go (replace google.com with your actual domain):

echo -n | openssl s_client -connect google.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > /Users/$USER/Downloads/google-com.cert

Conclusion

Pinning certificates (or their public keys) is important, and against all the odds it's also possible to do with the current Xamarin bits. So there is no reason not to do it. Go, pin your certs! :-).

What do you think? Drop me a line and let me know!