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 Tue, January 31, 2017
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.
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.
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
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:
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).
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
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! :-).