Search code examples
.net-corecertificatehttpclientx509certificate

Client certificate not included by HttpClientHandler in .net core


I'm having difficulties sending a certificate using HttpClientHandler because the certificate simply won't appear on Server's request. The certificate has the proper EKU for server and client authentication, and Key Usage of "Digital Signature". @davidsh regarded the issue 26531 for the lack of logging that HttpClient had but running my project in Visual Studio (with logs set to Trace and using dotnet 3.1.401) no output error came out. I'm not very familiar at all with logman but I ran it when the issue supposed to happen as I executed my code and nothing from the log stood out indicating what the problem could be. Running out of options to test the code I attempted to add a certificate without the private key on the client request to see if the httpClientHandler.ClientCertificates.Add ... would throw any error saying something like "You need a certificate with private key to sign your request", shouldn't it say anything?

On client:

services.AddHttpClient<ILetterManClient, LetterManClient.LetterManClient>()
    .ConfigureHttpClient(client =>
    {
        client.BaseAddress = new Uri(configuration.GetValue<string>("Microservices:LetterManAPI"));
    })
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        HttpClientHandler httpClientHandler = new HttpClientHandler();

        httpClientHandler.ServerCertificateCustomValidationCallback = ValidateServiceCertficate;
        httpClientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;

        clientCertificate = new X509Certificate2("client_cert.pfx", "developer");

        httpClientHandler.ClientCertificates.Add(clientCertificate);

        return httpClientHandler;
    });

On server:

public class ValidateClientCertificates : TypeFilterAttribute
{
    public ValidateClientCertificates() : base(typeof(ValidateClientCertificatesImpl))
    {

    }

    private class ValidateClientCertificatesImpl : IAsyncAuthorizationFilter
    {
        X509Certificate2 clientCertificate;
        public ValidateClientCertificatesImpl(IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
        {
            clientCertificate = new X509Certificate2("client_cert.crt");
        }

        public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
        {
            var certificate = await context.HttpContext.Connection.GetClientCertificateAsync();

            if ((certificate == null) || (!certificate.Thumbprint.Equals(clientCertificate.Thumbprint)))
            {
                context.Result = new UnauthorizedObjectResult("");
                return;
            }
        }
    }
}

Side note: I've been also trying to debug my project using code compiled from corefx repo to see what's going but Visual Studio insists reference the code from local installed sdk instead of the project from corefx that it's referencing it but this is another issue.

I've created this project that simulates the issue. It creates the certificates and it has two projects with one service and another client implemented.

Any help will be very welcomed.


Solution

  • These are the guidelines for Kestrel to require Client certificate but it assumes that the CA is installed in the machine otherwise you have to specify the client certificate directly when configuring Kestrel server as follows:

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
                webBuilder.ConfigureKestrel(o =>
                {
                    o.ConfigureHttpsDefaults(o => {
                        o.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
                        o.ClientCertificateValidation = ValidateClientCertficate;
                    });
                });
            });
    
    public static Func<X509Certificate, X509Chain, SslPolicyErrors, bool> ValidateClientCertficate =
        delegate (X509Certificate serviceCertificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
            X509Certificate2 clientCertificate;
            clientCertificate = new X509Certificate2("client.crt");
    
            if (serviceCertificate.GetCertHashString().Equals(clientCertificate.Thumbprint))
            {
                return true;
            }
    
            return false;
        };
    

    Unfortunately, you can't require Client certificates for a specific route as I intended.