20
votes

I'm writing a .NET Core app to poll a remote server and transfer data as it appears. This is working perfectly in PHP because the PHP is ignoring the certificate (which is also a problem in browsers) but we want to move this to C# .NET CORE because this is the only remaining PHP in the system.

We know the server is good, but for various reasons the certificate can't / won't be updated any time soon.

The request is using HttpClient:

        HttpClient httpClient = new HttpClient();
        try
        {
            string url = "https://URLGoesHere.php";
            MyData md = new MyData();  // this is some data we need to pass as a json
            string postBody = JsonConvert.SerializeObject(md);
            httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));                
            HttpResponseMessage wcfResponse = await httpClient.PostAsync(url, new StringContent(postBody, Encoding.UTF8, "application/json"));
            Console.WriteLine(wcfResponse.Content);
        }
        catch (HttpRequestException hre)
        {
        // This exception is being triggered
        }

Having researched this it seems the universal recommendation is to use ServicePointManager, but this is not available in .NET Core and I'm having trouble finding the recommended replacement.

Is there a simple or better way to do this in .NET Core?

4
Thanks for all the pointers to other threads - but having visited all of them in the past I still have a problem - none of them will compile. Every time there is some reference that can't be found. Clearly I'm missing something obvious here! The small snippets don't give enough info (to me) like what else needs to be included or referenced. Ctrl-. doesn't make any reasonable suggestions either.DaveEP
Instead of ignoring certificate errors, you should fix certificate errors. Otherwise, there is no need in certficate at all.Crypt32
Of course, I agree 100% with you and I have requested that, but the server is not under my control and not owned by my company, it's in a datacentre far away and I am stuck with what we have. This is a legacy system that will be obsoleted shortly and the people responsible for maintenance have said fixing the certificate not going to happen. I'm tasked with migrating the existing data and capturing new data until it's replaced.DaveEP

4 Answers

13
votes

Instead of new HttpClient() you want something akin to

var handler = new System.Net.Http.HttpClientHandler();
using (var httpClient = new System.Net.Http.HttpClient(handler))
{
    handler.ServerCertificateCustomValidationCallback = (request, cert, chain, errors) =>
    {
        // Log it, then use the same answer it would have had if we didn't make a callback.
        Console.WriteLine(cert);
        return errors == SslPolicyErrors.None;
    };

    ...
}

That should work on Windows, and on Linux where libcurl is compiled to use openssl. With other curl backends Linux will throw an exception.

7
votes

//at startup configure services add the following code

services.AddHttpClient(settings.HttpClientName, client => {
// code to configure headers etc..
}).ConfigurePrimaryHttpMessageHandler(() => {
                  var handler = new HttpClientHandler();
                  if (hostingEnvironment.IsDevelopment())
                  {
                      handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { return true; };
                  }
                  return handler;
              });

now you can use IHttpClientFactory CreateClient method within your service

6
votes

Getting Linux and macOS to work

If you are working in Linux or macOS you may encounter a scenario in which HttpClient will not allow you to access a self signed cert even if it is in your trusted store. You will likely get the following:

System.Net.Http.CurlException: Peer certificate cannot be authenticated with given CA certificates environment variable

of if you are implementing (as shown in the other answer)

handler.ServerCertificateCustomValidationCallback = (request, cert, chain, errors) =>
{
    // Log it, then use the same answer it would have had if we didn't make a callback.
    Console.WriteLine(cert);
    return errors == SslPolicyErrors.None;
};

This is due to the version of libcurl on the machine not supporting the appropriate callbacks that .Net Core needs to in order to gather the appropriate data to call the ServerCertificateCustomValidationCallback. For example, there isn't a way for the framework to create the cert object or another one of the parameters. More information can be found in the discussion for the workaround that was provided in .NET Core at issue in dotnet core's github repo:

https://github.com/dotnet/corefx/issues/19709

The workaround (which should only be used for testing or specific internal applications) is the following:

using System;
using System.Net.Http;
using System.Runtime.InteropServices;

namespace netcurl
{
    class Program
    {
        static void Main(string[] args)
        {
            var url = "https://localhost:5001/.well-known/openid-configuration";
            var handler = new HttpClientHandler();
            using (var httpClient = new HttpClient(handler))
            {
                // Only do this for testing and potentially on linux/mac machines
                if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && IsTestUrl(url))
                {
                    handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
                }

                var output = httpClient.GetStringAsync(url).Result;

                Console.WriteLine(output);
            }
        }

        static  bool IsTestUrl(string url) => url.Contains("localhost");
    }
}

There is another avenue for fixing this problem, and that is using a version of libcurl that is compiled with openssl support. For macOS, here is a good tutorial on how to do that:

https://spin.atomicobject.com/2017/09/28/net-core-osx-libcurl-openssl/

For the short version, grab a copy of the latest libcurl compiled with openssl support:

brew install curl --with-openssl

You probably don't want to force the entire OS to use the non-Apple version of libcurl, so you'll likely want to use the DYLD_LIBRARY_PATH environment variable instead of using brew to force link the binaries into the regular path of the OS.

export DYLD_LIBRARY_PATH=/usr/local/opt/curl/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}

The above command can be used to set the appropriate environment variable when running dotnet in a terminal. This doesn't really apply to GUI applications though. If you're using Visual Studio for Mac, you can set the environment variable in the project run settings:

Visual Studio for Mac project run settings

The second approach was necessary for me when using IdentityServer4 and token authorization. The .NET Core 2.0 authorization pipeline was making a call to the token authority using an HttpClient instance. Since I didn't have access to the HttpClient or its HttpClientHandler object, I needed to force the HttpClient instance to use the appropriate version of libcurl that would look into my KeyChain system roots for my trusted certificate. Otherwise, I would get the System.Net.Http.CurlException: Peer certificate cannot be authenticated with given CA certificates environment variable when trying to secure a webapi endpoint using the Authorize(AuthenticationSchemes = IdentityServerAuthenticationDefaults.AuthenticationScheme)] attribute.

I spent hours researching this before finding work arounds. My whole goal was to use a self-signed certificate during development for macOs using IdentityServer4 to secure my webapi. Hope this helps.

0
votes

Just to add another variation, you could add in your thumbprint and check it in the callback to make things a bit more secure such as:

if (!string.IsNullOrEmpty(adminConfiguration.DevIdentityServerCertThumbprint))
{
   options.BackchannelHttpHandler = new HttpClientHandler
   {
      ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) => certificate.Thumbprint.Equals(adminConfiguration.DevIdentityServerCertThumbprint, StringComparison.InvariantCultureIgnoreCase)
   };
}

adminConfiguration.DevIdentityServerCertThumbprint is the configuration that you would set with your self signed cert's thumbrint.