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:

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 full IPrivateKey read in from the file system.

public class CustomRSA : RSA
{
    private readonly IPrivateKey privateKey;
    private readonly Certificate certificate;

    public CustomRSA(IPrivateKey privateKey, Certificate certificate)
    {
        this.privateKey = privateKey;
        this.certificate = certificate;
    }

    public override int KeySize
    {
        get
        {
            // I'm not quite sure how the `privateKey.GetEncoded().Length`
            // should be used to calculate key size. I have a suspicion
            // the correct thing might be to round up to the nearest power
            // of 2.

            // For simplicity for the moment, just hard-code the key size
            result = 2048;
            return result;
        }
        set
        {

        }
    }

    protected override void Dispose(bool disposing)
    {
    }

    public override string KeyExchangeAlgorithm
    {
        get { return certificate.PublicKey.Algorithm; }
    }

    public override string SignatureAlgorithm
    {
        get { return privateKey.Algorithm; }
    }

    public override byte[] EncryptValue(byte[] rgb)
    {         
        // If `rgb.Length` is greater than `cipher.BlockSize`, this 
        // method will require additional logic.

        Cipher cipher = Cipher.GetInstance(certificate.PublicKey.Algorithm);
        cipher.Init(Javax.Crypto.CipherMode.EncryptMode, certificate.PublicKey);
        byte[] result = cipher.DoFinal(rgb);
        return result;
    }

    public override byte[] DecryptValue(byte[] rgb)
    {
        // If `rgb.Length` is greater than `cipher.BlockSize`, this 
        // method will require additional logic.

        Cipher cipher = Cipher.GetInstance(privateKey.Algorithm);
        cipher.Init(Javax.Crypto.CipherMode.DecryptMode, privateKey);
        byte[] result = cipher.DoFinal(rgb);
        return result;
    }

    public override RSAParameters ExportParameters(bool includePrivateParameters)
    {
        return new RSAParameters();
    }

    public override void ImportParameters(RSAParameters parameters)
    {
    }
}

If the class is initialized with an IPrivateKey from KeyChain.GetPrivateKey(), the TLS handshake fails with a Java.Lang.NullPointerException, presumably because privateKey.GetEncoded() is null:

UNHANDLED EXCEPTION: System.Net.WebException: Error getting response stream (Write: The authentication or decryption has failed.): SendFailure ---> System.IO.IOException: The authentication or decryption has failed. ---> Java.Lang.NullPointerException: Exception of type 'Java.Lang.NullPointerException' was thrown.
  at Android.Runtime.JNIEnv.CallObjectMethod (IntPtr jobject, IntPtr jmethod, Android.Runtime.JValue[] parms) [0x00064] in /Users/builder/data/lanes/monodroid-mlion-monodroid-4.12-series/0deb0164/source/monodroid/src/Mono.Android/src/Runtime/JNIEnv.g.cs:194 
  at Javax.Crypto.Cipher.DoFinal (System.Byte[] input) [0x00034] in /Users/builder/data/lanes/monodroid-mlion-monodroid-4.12-series/0deb0164/source/monodroid/src/Mono.Android/platforms/android-18/src/generated/Javax.Crypto.Cipher.cs:163 
  at TestApp.CustomRSA.DecryptValue (System.Byte[] rgb) [0x00021] in /Volumes/Cases/Android_ClientCert_WebRequest_PSchneider_2014_03_18/TestApp/TestApp/MainActivity.cs:188

SSL handshake terminated: ssl=0x5cad11f0: Failure in SSL library, usually a protocol error

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().

Example problem code

KeyStore keyStore = KeyStore.GetInstance("PKCS12");
keyStore.Load(null, null);
keyStore.SetCertificateEntry(alias, certificate);

Abridged stack trace

The managed exception stack trace isn't too helpful in this case, so I've snipped it out.

UNHANDLED EXCEPTION: Javax.Net.Ssl.SSLHandshakeException: Exception of type 'Javax.Net.Ssl.SSLHandshakeException' was thrown.
  [snip]
  --- End of managed exception stack trace ---
javax.net.ssl.SSLHandshakeException: javax.net.ssl.SSLProtocolException: SSL handshake terminated: ssl=0x5cad11f0: Failure in SSL library, usually a protocol error
error:14094410:SSL routines:SSL3_READ_BYTES:sslv3 alert handshake failure (external/openssl/ssl/s3_pkt.c:1290 0x5cb66f58:0x00000003)
        at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:412)
        at libcore.net.http.HttpConnection.setupSecureSocket(HttpConnection.java:209)
  [snip]

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.

Example problem code

KeyStore keyStore = KeyStore.GetInstance("BKS");
keyStore.Load(null, null);
keyStore.SetKeyEntry(alias, privateKey, null, certificateChain);

Abridged stack trace

The managed exception stack trace isn't too helpful here either, so again I've snipped it out.

UNHANDLED EXCEPTION: Java.Security.KeyStoreException: Exception of type 'Java.Security.KeyStoreException' was thrown.
  [snip]
  --- End of managed exception stack trace ---
java.security.KeyStoreException: java.lang.NullPointerException
        at com.android.org.bouncycastle.jce.provider.JDKKeyStore.engineSetKeyEntry(JDKKeyStore.java:682)
        at java.security.KeyStore.setKeyEntry(KeyStore.java:337)
  [snip]

java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

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 keyStore does trust the client-side certificate, but it's missing the server-side certificate.

Example problem code

keyStore = KeyStore.GetInstance("PKCS12");
keyStore.Load(null, null);
keyStore.SetKeyEntry(alias, privateKey, null, certificateChain);

KeyManagerFactory kmf = KeyManagerFactory.GetInstance(KeyManagerFactory.DefaultAlgorithm);
kmf.Init(keyStore, null);
IKeyManager[] keyManagers = kmf.GetKeyManagers();

TrustManagerFactory tmf = TrustManagerFactory.GetInstance("X509");
tmf.Init(keyStore);
ITrustManager[] trustManagers = tmf.GetTrustManagers();

SSLContext context = SSLContext.GetInstance("TLS");
context.Init(keyManagers, trustManagers, null);

URL url = new URL(urlString);
HttpsURLConnection urlConnection = (HttpsURLConnection)url.OpenConnection();
urlConnection.SSLSocketFactory = (context.SocketFactory);

Fix

One way to fix the problem is to add the server certificate to the same key store:

keyStore = KeyStore.GetInstance("PKCS12");
keyStore.Load(null, null);
keyStore.SetKeyEntry(alias, privateKey, null, certificateChain);
keyStore.SetCertificateEntry(serverCertAlias, serverCert);

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:

Abridged stack trace

UNHANDLED EXCEPTION: Javax.Net.Ssl.SSLHandshakeException: Exception of type 'Javax.Net.Ssl.SSLHandshakeException' was thrown.
  [snip]
  --- End of managed exception stack trace ---
javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
        at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:374)
        at libcore.net.http.HttpConnection.setupSecureSocket(HttpConnection.java:209)
  [snip]
Caused by: java.security.cert.CertificateException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
        at org.apache.harmony.xnet.provider.jsse.TrustManagerImpl.checkTrusted(TrustManagerImpl.java:192)
        at org.apache.harmony.xnet.provider.jsse.TrustManagerImpl.checkServerTrusted(TrustManagerImpl.java:163)
        at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.verifyCertificateChain(OpenSSLSocketImpl.java:573)
        at org.apache.harmony.xnet.provider.jsse.NativeCrypto.SSL_do_handshake(Native Method)
        at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:371)

Links

KeyChain API on Android 4.1 and client certificate authentication
Mailing list discussion prompted by Android 4.1's breaking change in the behavior of privateKey.GetEncoded(). From February 2013.
HTTPS with Client Certificates on Android
A closely related discussion of using client certificates with HttpURLConnection on Android.

Found a mistake?

Submit a comment or correction

Updates

02 Apr 2014 Added discussion of protocol error, KeyStoreException… NullPointerException, and Trust anchor... not found.
31 Mar 2014 Posted