KeyChain.GetPrivateKey()
on AndroidStarting 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.
fullprivate 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()
.
KeyStore keyStore = KeyStore.GetInstance("PKCS12"); keyStore.Load(null, null); keyStore.SetCertificateEntry(alias, certificate);
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
.
KeyStore keyStore = KeyStore.GetInstance("BKS"); keyStore.Load(null, null); keyStore.SetKeyEntry(alias, privateKey, null, certificateChain);
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.
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);
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
:
Embedded Resourcebuild 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.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.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)
privateKey.GetEncoded()
. From February 2013.HttpURLConnection
on Android.Submit a comment or correction
02 Apr 2014 | Added discussion of protocol error, KeyStoreException… NullPointerException,and Trust anchor... not found. |
31 Mar 2014 | Posted |