36

I am trying to secure my RESTful WebApi service with ssl and client authentication using client certificates.

To test; I have generated a self signed certificate and placed in the local machine, trusted root certification authorities folder and i have generated a "server" and "client" certificates. Standard https to the server works without issue.

However I have some code in the server to validate the certificate, this never gets called when I connect using my test client which supplies my client certificate and the test client is returned a 403 Forbidden status.

This imples the server is failing my certificate before it reaches my validation code. However if i fire up fiddler it knows a client certificate is required and asks me to supply one to My Documents\Fiddler2. I gave it the same client certificate i use in my test client and my server now works and received the client certificate i expect! This implies that the WebApi client is not properly sending the certificate, my client code below is pretty much the same as other examples i have found.

    static async Task RunAsync()
    {
        try
        {
            var handler = new WebRequestHandler();
            handler.ClientCertificateOptions = ClientCertificateOption.Manual;
            handler.ClientCertificates.Add(GetClientCert());
            handler.ServerCertificateValidationCallback += Validate;
            handler.UseProxy = false;

            using (var client = new HttpClient(handler))
            {
                client.BaseAddress = new Uri("https://hostname:10001/");

                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));

                var response = await client.GetAsync("api/system/");
                var str = await response.Content.ReadAsStringAsync();

                Console.WriteLine(str);
            }
        } catch(Exception ex)
        {
            Console.Write(ex.Message);
        }
    }

Any ideas why it would work in fiddler but not my test client?

Edit: Here is the code to GetClientCert()

private static X509Certificate GetClientCert()
    {            
        X509Store store = null;
        try
        {
            store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
            store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);

            var certs = store.Certificates.Find(X509FindType.FindBySubjectName, "Integration Client Certificate", true);

            if (certs.Count == 1)
            {
                var cert = certs[0];
                return cert;
            }
        }
        finally
        {
            if (store != null) 
                store.Close();
        }

        return null;
    }

Granted the test code does not handle a null certificate but i am debugging to enssure that the correct certificate is located.

Tronneh
  • 361
  • 1
  • 3
  • 4
  • Have you checked that GetClientCert returns the certificate you expect? – csgero Mar 05 '14 at 12:46
  • 1
    Yes :) What the GetClientCert does is lookup the client cert from the localmachine personal store, which is the same certificate i gave to fiddler. However even if it was a different cert i would expect the sever validation code to get called? – Tronneh Mar 05 '14 at 13:00
  • Depends on how the service is hosted. IIS for example does certificate validation, and will return 403 without ever calling your code. – csgero Mar 05 '14 at 14:10
  • I am self hosting the service as it is embeded in a windows service. However what criteria would IIS return 403 for? i would assume this criteria would be applied across the board, but again the same certificate works when Fiddler is involved so i am thinking that somehow the HttpClient is not sending the certificate properly. – Tronneh Mar 05 '14 at 14:40
  • 2
    From this link: http://stackoverflow.com/questions/19125896/forcing-asp-net-webapi-client-to-send-a-client-certificate-even-when-no-ca-match : `client will only use a client certificate (from the WebRequestHandler.ClientCertificates collection) if it has chain of trust to one of the server's trusted roots` . I guess your certificate is not trusted (self signed). – Khanh TO Mar 28 '15 at 04:09
  • Did you ever find a way for HttpClient to use a non verifying certificate? – NER1808 Jan 12 '16 at 10:20
  • @Tronneh could you share the solution if any? – dipak Jun 13 '17 at 22:26
  • Is your code above running in the same user account that Fiddler's running in? If not, it may have access to the certificate file but not the correct private key. Similarly, what does your `GetClientCert()` function return? Specifically, does it have the `PrivateKey` property set? – EricLaw Mar 05 '14 at 17:30
  • Fiddler is running as my user account, the test is running under VS2013 debug, VS2013 is running as my user account. The `GetClintCert()` method returns my expected client certificate, the `HasPrivateKey` property is true – Tronneh Mar 06 '14 at 07:50
  • I have updated the original question to include the code from `GetClientCert()` – Tronneh Mar 06 '14 at 07:57
  • Interesting. Consider enabling logging for System.Net and see whether there's anything noteworthy in the logs? – EricLaw Mar 06 '14 at 11:35
  • I have found another post about the WebApi client that seems indicate that it will not send invalid certificates. My client cert does indeed return false on Verify method and some digging found that it was because the revocation list could not be verified. I created a revocation list, added to my store and the client cert now reports that it is valid, however the test app still reports forbidden with my client validation code on the server. I will see what the logging says! – Tronneh Mar 06 '14 at 12:23

3 Answers3

3

There are 2 types of certificates. The first is the public .cer file that is sent to you from the owner of the server. This file is just a long string of characters. The second is the keystore certificate, this is the selfsigned cert you create and send the cer file to the server you are calling and they install it. Depending on how much security you have, you might need to add one or both of these to the Client (Handler in your case). I've only seen the keystore cert used on one server where security is VERY secure. This code gets both certificates from the bin/deployed folder:

#region certificate Add
                // KeyStore is our self signed cert
                // TrustStore is cer file sent to you.

                // Get the path where the cert files are stored (this should handle running in debug mode in Visual Studio and deployed code) -- Not tested with deployed code
                string executableLocation = Path.GetDirectoryName(AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory);

                #region Add the TrustStore certificate

                // Get the cer file location
                string pfxLocation = executableLocation + "\\Certificates\\TheirCertificate.cer";

                // Add the certificate
                X509Certificate2 theirCert = new X509Certificate2();
                theirCert.Import(pfxLocation, "Password", X509KeyStorageFlags.DefaultKeySet);
                handler.ClientCertificates.Add(theirCert);
                #endregion

                #region Add the KeyStore 
                // Get the location
                pfxLocation = executableLocation + "\\Certificates\\YourCert.pfx";

                // Add the Certificate
                X509Certificate2 YourCert = new X509Certificate2();
                YourCert.Import(pfxLocation, "PASSWORD", X509KeyStorageFlags.DefaultKeySet);
                handler.ClientCertificates.Add(YourCert);
                #endregion

                #endregion

Also - you need to handle cert errors (note: this is BAD - it says ALL cert issues are okay) you should change this code to handle specific cert issues like Name Mismatch. it's on my list to do. There are plenty of example on how to do this.

This code at the top of your method

// Ignore Certificate errors  need to fix to only handle 
ServicePointManager.ServerCertificateValidationCallback = MyCertHandler;

Method somewhere in your class

private bool MyCertHandler(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors error)
    {
        // Ignore errors
        return true;
    }
2

In the code you are using store = new X509Store(StoreName.My, StoreLocation.LocalMachine);.

Client certificates are not picked up from LocalMachine, you should instead use StoreLocation.CurrentUser.

Checking MMC -> File -> Add or Remove Snap-ins -> Certificates -> My user account you will see the certificate that fiddler uses. If you remove it from My user account and only have it imported in Computer account you will see that Fiddler can not pick it up either.

A side note is when finding certificates you also have to address for culture.

Example:

var certificateSerialNumber= "‎83 c6 62 0a 73 c7 b1 aa 41 06 a3 ce 62 83 ae 25".ToUpper().Replace(" ", string.Empty);

//0 certs
var certs = store.Certificates.Find(X509FindType.FindBySerialNumber, certificateSerialNumber, true);

//null
var cert = store.Certificates.Cast<X509Certificate>().FirstOrDefault(x => x.GetSerialNumberString() == certificateSerialNumber);

//1 cert
var cert1 = store.Certificates.Cast<X509Certificate>().FirstOrDefault(x =>
                x.GetSerialNumberString().Equals(certificateSerialNumber, StringComparison.InvariantCultureIgnoreCase));
Ogglas
  • 62,132
  • 37
  • 328
  • 418
  • 9
    If you vote down, please say why. No chance to improve answers otherwise. – Ogglas Aug 01 '18 at 14:00
  • Why would a certificate not be picked up from the Local Machine store? That's where a non user-specific certificate should be stored. – Suncat2000 Jan 06 '23 at 14:06
1
 try this.

Cert should be with the current user store. Or give full rights and read from a file as it is a console application.

// Load the client certificate from a file.
X509Certificate x509 = X509Certificate.CreateFromCertFile(@"c:\user.cer");

Read from the user store.

 private static X509Certificate2 GetClientCertificate()
    {
        X509Store userCaStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
        try
        {
            userCaStore.Open(OpenFlags.ReadOnly);
            X509Certificate2Collection certificatesInStore = userCaStore.Certificates;
            X509Certificate2Collection findResult = certificatesInStore.Find(X509FindType.FindBySubjectName, "localtestclientcert", true);
            X509Certificate2 clientCertificate = null;
            if (findResult.Count == 1)
            {
                clientCertificate = findResult[0];
            }
            else
            {
                throw new Exception("Unable to locate the correct client certificate.");
            }
            return clientCertificate;
        }
        catch
        {
            throw;
        }
        finally
        {
            userCaStore.Close();
        }
    }
Jin Thakur
  • 2,711
  • 18
  • 15