Limitations with private keys from KeyChain.GetPrivateKey() on Android

Summary

Starting with Android 4.1, if you want to use a client certificate to perform SSL/TLS authentication for an HTTPS request on Xamarin.Android, you might run into a problem with the private keys returned by KeyChain.GetPrivateKey(). In particular, these private keys are not complete, and therefore are only compatible with the Android web request APIs. Two remaining options are:

Keep using the incomplete private key from the Android KeyChain. In this case, you must use the Android web request APIs. Specifically, you can use the Javax.Net.Ssl.HttpsURLConnection class, with a custom SSLSocketFactory (see the Providing an application specific X509KeyManager section in the docs). Depending on your particular setup, you might need to customize the HostnameVerifier property as well, or provide a custom TrustManager for the SSLSocketFactory.

Store a copy of the private key from the client certificate somewhere on the file system, outside the Android KeyChain (for example, as an Embedded Resource). With this option, you can load the private key into an X509Certificate2 under the PrivateKey property, and then use a System.Net.HttpWebRequest to perform the request.

Importantly, there is no way to use the private key obtained from KeyChain.GetPrivateKey() with the C# HttpWebRequest class. This problem is closely related to the fact that calling IKey.GetEncoded() on the private key returns null in Android 4.1. See also: http://mono-for-android.1047100.n5.nabble.com/KeyChain-API-on-Android-4-1-and-client-certificate-authentication-td5712844.html. That mailing list thread includes a link to a CustomRSA class that attempts to work around the problem by using the Java Cipher API. This is a clever idea, but unfortunately the Cipher API also fails when using the incomplete private key from the KeyChain. This failure happens in exactly the same way regardless of whether the app is a pure Java Android app or a Xamarin.Android app.

CustomRSA class that works correctly, but only with a full private key (download: CustomRSA.cs)

Here is a modified version of the CustomRSA class from the mailing list thread. It switches around the behavior of the EncryptValue() and the DecryptValue() methods to use the private key for decryption. As mentioned on the mailing list thread, this is the proper convention for RSA public key encryption. This class allows successful client certificate authorization when used as the private key for a X509Certificate2, but only if it is initialized with a fullIPrivateKey read in from the file system.

This error can happen if you're using a KeyStore that only contains a certificate, and no private key. For client certificate authorization, the certificate must be loaded with its private key via SetKeyEntry().

java.security.KeyStoreException: java.lang.NullPointerException

This error has the same underlying cause as the NullPointerException we saw for the Cipher API. In particular, using a Bouncy Castle Key Store (BKS) requires that the full private key be available. It's possible to avoid the problem by changing BKS to PKCS12.

One cause of this error is if the HttpsURLConnection uses a TrustManager that does not trust the server-side certificate (most likely because the server-side certificate is self-signed). In the example code below, the keyStoredoes trust the client-side certificate, but it's missing the server-side certificate.

Another way to fix the problem would be to create a separate key store for the TrustManager that only contains the server certificate. The TrustManager doesn't actually need to know about the client certificate at all.

There are at least 2 ways to load the serverCert:

Embed the certificate in the application using the Embedded Resource build action. Then load the certificate from the resource stream using CertificateFactory.GenerateCertificate(). Since the server certificate only contains the public key, there aren't as many security concerns with embedding it directly in the app as for the client certificate.

Install the server certificate in the key chain, and access it just like the client certificate. Note that this will require an additional call to KeyChain.ChoosePrivateKeyAlias(). This also requires that you install the private key of the server certificate. Installing this private key might be undesirable or impossible. At best, installing the server's private key on every device seems inelegant.