1

I have created CertificateTestController and ValuesController from this example How to use a client certificate to authenticate and authorize in a Web API. If you scroll down to "Update" from user Ogglas. I have taken his example and gotten "CertificateTestController" to work where I can grab the Certificate from my store and add it to the "handler". When I call "ValuesController", there is no cert being initialized by

X509Certificate2 cert = actionContext.Request.GetClientCertificate();

Here is the complete code that I have

ValuesController

{
    [RequireSpecificCert]
    public class ValuesController : ApiController
    {
        // GET api/values
        public IHttpActionResult Get()
        {
            return Ok("It works!");
        }

        public class RequireSpecificCertAttribute : AuthorizationFilterAttribute
        {
            public override void OnAuthorization(HttpActionContext actionContext)
            {

                if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps)
                {
                    actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                    {
                        ReasonPhrase = "HTTPS Required"
                    };
                }
                else
                {
                    X509Certificate2 cert = actionContext.Request.GetClientCertificate();
                    X509Certificate2 cert2 = actionContext.RequestContext.ClientCertificate;

                    if (cert == null)
                    {
                        actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                        {
                            ReasonPhrase = "Client Certificate Required"
                        };
                    }
                    else
                    {
                        X509Chain chain = new X509Chain();

                        //Needed because the error "The revocation function was unable to check revocation for the certificate" happened to me otherwise
                        chain.ChainPolicy = new X509ChainPolicy()
                        {
                            RevocationMode = X509RevocationMode.NoCheck,
                        };
                        try
                        {
                            var chainBuilt = chain.Build(cert);
                            Debug.WriteLine(string.Format("Chain building status: {0}", chainBuilt));

                            var validCert = CheckCertificate(chain, cert);

                            if (chainBuilt == false || validCert == false)
                            {
                                actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                                {
                                    ReasonPhrase = "Client Certificate not valid"
                                };
                                foreach (X509ChainStatus chainStatus in chain.ChainStatus)
                                {
                                    Debug.WriteLine(string.Format("Chain error: {0} {1}", chainStatus.Status, chainStatus.StatusInformation));
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                            Debug.WriteLine(ex.ToString());
                        }
                    }

                    base.OnAuthorization(actionContext);
                }
            }

            private bool CheckCertificate(X509Chain chain, X509Certificate2 cert)
            {
                var rootThumbprint = WebConfigurationManager.AppSettings["rootThumbprint"].ToUpper().Replace(" ", string.Empty);

                var clientThumbprint = WebConfigurationManager.AppSettings["clientThumbprint"].ToUpper().Replace(" ", string.Empty);

                //Check that the certificate have been issued by a specific Root Certificate
                var validRoot = chain.ChainElements.Cast<X509ChainElement>().Any(x => x.Certificate.Thumbprint.Equals(rootThumbprint, StringComparison.InvariantCultureIgnoreCase));

                //Check that the certificate thumbprint matches our expected thumbprint
                var validCert = cert.Thumbprint.Equals(clientThumbprint, StringComparison.InvariantCultureIgnoreCase);

                return validRoot && validCert;
            }
        }

calling above ValuesController with below CertificateTestController

{
    [RoutePrefix("api/certificatetest")]
    public class CertificateTestController : ApiController
    {
        public IHttpActionResult Get()
        {
            var handler = new WebRequestHandler();
            handler.ClientCertificateOptions = ClientCertificateOption.Manual;
            handler.ClientCertificates.Add(GetClientCert());
            handler.UseProxy = false;
            var client = new HttpClient(handler);
            var result = client.GetAsync("https://localhost:44301//values").GetAwaiter().GetResult();
            var resultString = result.Content.ReadAsStringAsync().GetAwaiter().GetResult();
            return Ok(resultString);
        }

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

                var certificateSerialNumber = "2bc034466b6960d2fee84d86e6c2532a".ToUpper().Replace(" ", string.Empty);

                var cert = store.Certificates.Cast<X509Certificate>().FirstOrDefault(x => x.GetSerialNumberString().Equals(certificateSerialNumber, StringComparison.InvariantCultureIgnoreCase));
                return cert;
            }
            finally
            {
                store.Close();
            }
        }
    }
}

Please help!

Dheeraj Dixit
  • 73
  • 1
  • 11
  • Did you open the certificate and validate the serial is the one you are using in your code? – jdweng Mar 01 '19 at 18:00
  • Sorry, if I wasn't clear before. It is not the about verifying certificate at this point. It is about, I am not even able to pull the cert from the actionContext. – Dheeraj Dixit Mar 01 '19 at 22:14
  • I meant manually : IE : Tools : Internet Options : Content : Certificates : Select Any One : View Details. Look at the certificates on the Trusted Root Certification Authorities. – jdweng Mar 02 '19 at 08:35
  • Yes, but that is not the problem. Problem is when I make a call to valuescontroller --> var result = client.GetAsync("https://localhost:44301//values").GetAwaiter().GetResult();. I don't see the certificate in actionContext in the ValuesController. @Ogglas... would you happen to know anything? – Dheeraj Dixit Mar 05 '19 at 21:05
  • Who owns the certificate? Who is running the app? What is full path of certificate? – jdweng Mar 06 '19 at 04:30
  • certificate is owned my department. I am running the app on my work computer. Comodo RSA is the root of the path. – Dheeraj Dixit Mar 07 '19 at 00:05
  • So who installed the certificate and with what credentials? You should be able to see the credentials using IE with the account that is running the c# application on client. – jdweng Mar 07 '19 at 06:20
  • Did you ever get this resolved? If so, could you answer your question and help others out? – Ron Apr 02 '19 at 15:20
  • I got it to work. please see my response and let me know if you have any questions. – Dheeraj Dixit Jun 03 '19 at 20:21

1 Answers1

0

Here are the issues/problems I answered along the way when I was trying to fix this issue.

Q1 .

Why wasn't my certificate getting to the client side (code)?

A1.

VS does a initial SSL negotiation that happens before the OnAuthorization(HttpActionContext actionContext) is even hit. At that point, server searches for a client certificate in the certificate store that has a private key installed to verify. If there is no private key then it fails. I found the issue by turning on the verbose. Please see below.

Here are configurations that need to be changes in order to get this working.

1. Web Config file changes

• SSL Negotiation

Below configuration tells visual studio that there is a ssl negotiation expected for “api/values” URL. This helps us limit when and where is certificate negotiation is expected. Location path property

<location path="api/values">
  <system.webServer>
     <security>
        <access sslFlags="SslNegotiateCert" />
        <authentication>
          <iisClientCertificateMappingAuthentication enabled="true">
          </iisClientCertificateMappingAuthentication>
        </authentication>
      </security>
    </system.webServer>
  </location>

• Certificate Verbose

Below diagnostics helps us during troubleshooting and spits out any issues that might be causing with certificates validation.

<system.diagnostics>
    <trace autoflush="true" />
    <sources>
      <source name="System.Net">
        <listeners>
          <add name="System.Net"/>
        </listeners>
      </source>
      <source name="System.Net.HttpListener">
        <listeners>
          <add name="System.Net"/>
        </listeners>
      </source>
      <source name="System.Net.Sockets">
        <listeners>
          <add name="System.Net"/>
        </listeners>
      </source>
      <source name="System.Net.Cache">
        <listeners>
          <add name="System.Net"/>
        </listeners>
      </source>
    </sources>
    <sharedListeners>
      <add name="System.Net"
       type="System.Diagnostics.TextWriterTraceListener"
       initializeData="System.Net.trace.log"
       traceOutputOptions = "ProcessId, DateTime"/>
    </sharedListeners>
    <switches>
      <add name="System.Net" value="Verbose" />
      <add name="System.Net.Sockets" value="Verbose" />
      <add name="System.Net.Cache" value="Verbose" />
      <add name="System.Net.HttpListener" value="Verbose" />
    </switches>
  </system.diagnostics>

• App settings

Change rootThumbprint value to whatever server certificates’s thumbprint is and clientThumprint would be whatever client certificate thumbprint is. ceritificateSerialNumber should be outgoing certificate serial number.

<appSettings>
            <add key="webpages:Version" value="3.0.0.0" />
            <add key="webpages:Enabled" value="false" />
            <add key="ClientValidationEnabled" value="true" />
            <add key="UnobtrusiveJavaScriptEnabled" value="true" />
            <add key="rootThumbprint" value="change"/>
            <add key="clientThumbprint" value="change"/>
<add key="certificateSerialNumber" value="change"/>
</appSettings>

2. Local Visual Studio and IIS express

1) Applicationhost.config

 Path Visual studio earlier than 2015 but after 2008 Location --> c:\Users\e#\Documents\IISExpress\Config Visual Studio 2015 & 2017 {project_name}.vs\config\config.host.config
 Changes Make sure below properties are set to “Allow”

2) Enable SSL

Enable SSL in the visual studio properties window . this is important because URL changes from http to https.

enter image description here

3) WebApiConfig.cs

Create a class filters folder. Name it appropriately. Certificate validation needs to be the very first check before any of the other code is ran. This is where WebApiConfig.cs comes in handy. For example I called my class RequireHttpsAttribute. In order to run the check just put the below line in WebApiConfig.cs config.Filters.Add(new RequireHttpsAttribute()); enter image description here

4) CertificateTestController.cs

This is class acts as a client. This class is used to attach the cert with the request and send it out. There is one change in this class. This is was only tested locally on the computer. var result = client.GetAsync("https://localhost:44300//api//values").GetAwaiter().GetResult(); Change the URL. We are attaching the certificate based off of serial number below. Certificate can also be attached based off of Thumprint, Subject, Expiration/Revocation, Chain Validation etc.

var certificateSerialNumber = WebConfigurationManager.AppSettings["certificateSerialNumber"].ToUpper().Replace(" ", string.Empty);

5) ValueController.cs

This class acts as a server. This is where the certificate validation happens. There are two changes in “CheckCertificate” method/function that refers to the webApiConfig.cs. if you followed App Settings changes from above under WebApiConfig.cs then you are good to go.

3. Files

1) CertificateTestController.cs

2) ValuesController.cs

Dheeraj Dixit
  • 73
  • 1
  • 11