2

I'm trying to send a request to a web api in Xamarin.Android. The api requires a client certificate. I followed the advice in this question: xamarin.ios httpclient clientcertificate not working with https, but I get a "method not implemented" exception. Can anyone help?

Here's my code:

    string result = await CallApi(new System.Uri("myurl"));

    protected async Task<string> CallApi(Uri url)
    {
        try
        {
            AndroidClientHandler clientHandler = new AndroidClientHandler();
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Ssl3;

            using (var mmstream = new MemoryStream())
            {
                Application.Context.Assets.Open("mycert.pfx").CopyTo(mmstream);
                byte[] b = mmstream.ToArray();

                X509Certificate2 cert = new X509Certificate2(b, "password", X509KeyStorageFlags.DefaultKeySet);
                clientHandler.ClientCertificates.Add(cert);
            }

            ServicePointManager.ServerCertificateValidationCallback += new RemoteCertificateValidationCallback((sender, certificate, chain, policyErrors) => { return true; });

            HttpClient client = new HttpClient(clientHandler);
            HttpResponseMessage response = await client.GetAsync(url);
            response.EnsureSuccessStatusCode();

            string responseBody = await response.Content.ReadAsStringAsync();
            Console.WriteLine(responseBody);
            return responseBody;
        }
        catch (HttpRequestException e)
        {
            Console.WriteLine("\nException Caught!");
            return string.Empty;
        }
    }
Monica
  • 33
  • 1
  • 7
  • In which line did you get the exception? Could you please provide detailed stack trace? – Elvis Xia - MSFT Feb 26 '18 at 06:01
  • @ElvisXia-MSFT the exception is at this line: `clientHandler.ClientCertificates.Add(cert);` The exception is: `System.NotImplementedException: the method or operation is not implemented at System.Net.HttpClientHandler.get_ClientCertificates()` – Monica Feb 26 '18 at 14:49

2 Answers2

5

In the post you mentioned probably the managed handler is used. Since this handler currently doesn't support TLS 1.2 you shouldn't use it, but instead really use the AndroidClientHandler (see also Xamarin and TLS 1.2). Unfortunately ClientCertificates is indeed not implemented in AndroidClientHandler.

If you want to use client certificate with android you can extend the AndroidClientHandler:

using Java.Security;
using Java.Security.Cert;
using Javax.Net.Ssl;
using Xamarin.Android.Net; 
using Xamarin.Forms;

public class AndroidHttpsClientHandler : AndroidClientHandler
{
    private SSLContext sslContext;

    public AndroidHttpsClientHandler(byte[] customCA, byte[] keystoreRaw) : base()
    {
        IKeyManager[] keyManagers = null;
        ITrustManager[] trustManagers = null;

        // client certificate
        if (keystoreRaw != null)
        {
            using (MemoryStream memoryStream = new MemoryStream(keystoreRaw))
            {
                KeyStore keyStore = KeyStore.GetInstance("pkcs12");
                keyStore.Load(memoryStream, clientCertPassword.ToCharArray());
                KeyManagerFactory kmf = KeyManagerFactory.GetInstance("x509");
                kmf.Init(keyStore, clientCertPassword.ToCharArray());
                keyManagers = kmf.GetKeyManagers();
            }
        }

        // custom truststore if you have your own ca
        if (customCA != null)
        {
            CertificateFactory certFactory = CertificateFactory.GetInstance("X.509");

            using (MemoryStream memoryStream = new MemoryStream(customCA))
            {
                KeyStore keyStore = KeyStore.GetInstance("pkcs12");
                keyStore.Load(null, null);
                keyStore.SetCertificateEntry("MyCA", certFactory.GenerateCertificate(memoryStream));
                TrustManagerFactory tmf = TrustManagerFactory.GetInstance("x509");
                tmf.Init(keyStore);
                trustManagers = tmf.GetTrustManagers();
            }
        }
        sslContext = SSLContext.GetInstance("TLS");
        sslContext.Init(keyManagers, trustManagers, null);
    }

    protected override SSLSocketFactory ConfigureCustomSSLSocketFactory(HttpsURLConnection connection)
    {
        SSLSocketFactory socketFactory = sslContext.SocketFactory;
        if (connection != null)
        {
            connection.SSLSocketFactory = socketFactory;
        }
        return socketFactory;
    }
}
AlexS
  • 5,295
  • 3
  • 38
  • 54
  • Hi @AlexS what do I pass into the constructor? I'm assumming the customCA is the root certificate as a byte array.Is this correct? What is KeystoreRaw. Have you got an example of usage or can you explain how this works and why. Thanks – rideintothesun Nov 29 '18 at 16:07
  • 1
    @rideintothesun Right customCA is the root certificate oft the server you want to connect to. KeystoreRaw is a pkcs12 keystore as binary containing the pair of client certificate and the corresponding private key. In my case the customCA is also the root oft the certificate chain for the client certificate. But that's not neccessary. – AlexS Nov 30 '18 at 19:08
  • 1
    @rideintothesun You have to extend AndroidClientHandler with the code in my answer, since the original one doesn't support client certs AFAIK. HttpClient will use the Socketfactory provided by the handler. – AlexS Dec 06 '18 at 06:33
  • Aaahh! I understand now, I was expecting a public property. Thank you I will try it out – rideintothesun Dec 06 '18 at 10:53
  • @rideintothesun If my answer (or any other anwser), answered your question you should mark it as solution. This way other similar questions can be marked as duplicate of this one, which keeps stackoverflow more usable. – AlexS Mar 08 '19 at 10:17
0

If you refer to AndroidClientHandler Source Code, you can find following statement:

AndroidClientHandler also supports requests to servers with "invalid" (e.g. self-signed) SSL certificates. Since this process is a bit convoluted using the Java APIs, AndroidClientHandler defines two ways to handle the situation. First, easier, is to store the necessary certificates (either CA or server certificates) in the collection or, after deriving a custom class from AndroidClientHandler, by overriding one or more methods provided for this purpose(, and ). The former method should be sufficient for most use cases...

So, for usage of AndroidClientHandler you should use clientHandler.TrustedCerts together with Java.Security.Cert.X509Certificate:

Java.Security.Cert.X509Certificate cert = null;
try
{
    CertificateFactory factory = CertificateFactory.GetInstance("X.509");
    using (var stream = Application.Context.Assets.Open("MyCert.pfx"))
    {
         cert = (Java.Security.Cert.X509Certificate)factory.GenerateCertificate(stream);
    }
    } catch (Exception e)
    {
         System.Console.WriteLine(e.Message);
    }
    if (clientHandler.TrustedCerts != null)
    {
         clientHandler.TrustedCerts.Add(cert);
    }
    else
    {
         clientHandler.TrustedCerts = new List<Certificate>();
         clientHandler.TrustedCerts.Add(cert);
    }

Notes: don't use Application.Context.Assets.Open("ca.pfx").CopyTo(mmstream); otherwise you will get inputstream is empty exception.

Elvis Xia - MSFT
  • 10,801
  • 1
  • 13
  • 24
  • I tried your solution, but now I get a different exception saying that my stream is empty, even though I can see that it's not. {Java.Security.Cert.CertificateException: com.android.org.conscrypt.OpenSSLX509CertificateFactory$ParsingException: com.android.org.conscrypt.OpenSSLX509CertificateFactory$ParsingException: inStream is empty ---> – Monica Feb 27 '18 at 16:42
  • Do not use `Application.Context.Assets.Open("ca.pfx").CopyTo(mmstream);` please recheck my answer. – Elvis Xia - MSFT Feb 28 '18 at 02:44
  • @Elvis-Xia-MSFT thank you for replying! Your solution had `Application.Context.Assets.Open()` so I thought it was correct. Any suggestions on what to use instead? I'm searching google, but so far I haven't found what I need. – Monica Feb 28 '18 at 14:24
  • @ElvisXia-MSFT Your answer is about server certificate. The question is about client certificate. – AlexS Jun 26 '18 at 14:29
  • OK I now understand about managing the case where we have self signed certifictates. But I want to know like @AlexS how do I set up Xamarin HttpClient using AndroidClientHandler to pass the client certificate. I have the client certificate as an Java.Security.Cert.X509Certificate but I don't know how to use it. Has someone got an example. – rideintothesun Dec 01 '18 at 20:12
  • @rideintothesun You should be fine using your raw bytes that you generate the X509Certificate from with my snippet. There is no need for X509Certificate in Android. I only usw UT in UWP and WPF. If it doesn't work you'd have to examine the certificate setup of the Server and the client. – AlexS Dec 03 '18 at 07:20
  • Hi @AlexS, still not sure about this. how do I associate my client certificate with the AndroidClientHandler. Can someone explain the principles or provide a simple example – rideintothesun Dec 03 '18 at 20:25
  • Is it the case we cannot make a request using Xamarin HttpClient and AndroidClientHandler? Do we have to use HttpsUrlConnection? If not how do I use HttpClient? – rideintothesun Dec 04 '18 at 15:59