dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.23k stars 9.95k forks source link

How to accept a client certificate signed by an untrusted CA in Asp .netcore? #48099

Open alexiwonder opened 1 year ago

alexiwonder commented 1 year ago

Is there an existing issue for this?

Describe the bug

I have created a client and a server application to demonstrate Mutual TLS (mTLS). I generated a client certificate and had it signed by a local CA. However, the CA is not trusted by the server. I want to bypass any certificate validation on the server.

I can confirm that the server has received the client certificate, but the OnAuthenticationFailed is fired and the attached exception is Client certificate failed validation.

Upon debugging, I found that it is CertificateAuthenticationHandler (source code here) that rejects the certificate. Also, I think it is due the failed certificate chain validation (here).

Expected Behavior

AllowAnyClientCertificate disables further validation a client certificate.

Steps To Reproduce

 public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            builder.Services.Configure<KestrelServerOptions>(options =>
            {
                options.ConfigureHttpsDefaults(options =>
                {
                    options.AllowAnyClientCertificate();
                    options.CheckCertificateRevocation = false;
                    options.ClientCertificateValidation = ClientCertificateValidation;
                    options.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
                });
            });

            // Add services to the container.

            builder.Services.AddControllers();

            ConfigureServices(builder.Services);

            var app = builder.Build();

            // Configure the HTTP request pipeline.

            app.UseHttpsRedirection();

            app.UseAuthentication();

            app.MapControllers();

            app.Run();
        }

        private static bool ClientCertificateValidation(X509Certificate2 arg1, X509Chain? arg2, SslPolicyErrors arg3)
        {
            return true;
        }

        private static void ConfigureServices(IServiceCollection services)
        {
            services
               .AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
               .AddCertificate(options =>
               {
                   options.RevocationMode = X509RevocationMode.NoCheck;
                   options.ChainTrustValidationMode = X509ChainTrustMode.CustomRootTrust;
                   options.AllowedCertificateTypes = CertificateTypes.All;
                   options.ValidateCertificateUse = false;
                   options.ValidateValidityPeriod = false;

                   options.Events = new CertificateAuthenticationEvents
                   {
                       OnAuthenticationFailed = p =>
                       {
                           Console.WriteLine(p.Exception);
                           return Task.CompletedTask;

                       },
                       OnCertificateValidated = context =>
                       {
                           // Add certificate to HttpContext.Items
                           context.HttpContext.Items["ClientCertificate"] = context.ClientCertificate;
                           return Task.CompletedTask;
                       }
                   };
               });
        }
    }

Exceptions (if any)

No response

.NET Version

7.0.100

Anything else?

No response

mkArtakMSFT commented 1 year ago

@alexiwonder please share the logs so that we can see what is going on here. Thanks!

halter73 commented 1 year ago

Can you enable (preferably trace-level) ASP.NET Core Logging and provide the logs from when the issue occurs?

ghost commented 1 year ago

Hi @alexiwonder. We have added the "Needs: Author Feedback" label to this issue, which indicates that we have an open question for you before we can take further action. This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

ghost commented 1 year ago

This issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for 4 days. It will be closed if no further activity occurs within 3 days of this comment. If it is closed, feel free to comment when you are able to provide the additional information and we will re-investigate.

See our Issue Management Policies for more information.

alexiwonder commented 1 year ago

Sorry @halter73 and @mkArtakMSFT for late reply. Here's the logs:

dbug: Microsoft.Extensions.Hosting.Internal.Host[1]
      Hosting starting
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:54321
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\Projects\Demo\Server\
dbug: Microsoft.Extensions.Hosting.Internal.Host[2]
      Hosting started
warn: Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationHandler[2]
      Certificate validation failed, subject was CN=localhost. PartialChain A certificate chain could not be built to a trusted root authority.
System.Exception: Client certificate failed validation.
warn: Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationHandler[2]
      Certificate validation failed, subject was CN=localhost. PartialChain A certificate chain could not be built to a trusted root authority.
System.Exception: Client certificate failed validation.
halter73 commented 1 year ago

@blowdart @Tratcher Do either of you know whether this is a supported scenario for AddCertificate? This sets options.ValidateCertificateUse = false, but that just disables the EKU check.

I don't see a way to completely disable validating the chain. Do you need to add the root or self-signed certificate to CertificateAuthenticationOptions.CustomTrustStore? That'd be my guess. We probably don't want to make it too easy to accept literally any certificate with the CertificateAuthenticationHandler even if Kestrel allows it during the TLS handshake.

mkArtakMSFT commented 1 year ago

@blowdart, @Tratcher can you confirm the above ? Thanks!

blowdart commented 1 year ago

I'd still be guessing like Stephen, but ValidateCertificateUse is definitely only limited to EKU checks, as that's what the use refers to. Completely separate to chain trusts or anything of that ilk. The CustomTrustStore would be the way to go

halter73 commented 1 year ago

@blowdart Would we consider adding something like CertificateAuthenticationOptions.ClientCertificateValidation that allows you to have complete control over cert validation using a bool-returning delegate? And bypass any default cert chain or EKU checks?

This would be similar to what Kestrel (ClientCertificateValidation), HttpClient (ServerCertificateValidationCallback) and SslStream (RemoteCertificateValidationCallback) already expose. @javiercn sounds like he'd be in favor of this.

javiercn commented 1 year ago

Yeah, it's very aggressive that we don't leave a backdoor for development in these cases. Forces people into a non-trivial dev setup.

Tratcher commented 1 year ago

At that point why even use the cert auth handler?

blowdart commented 1 year ago

I have no problem with it, as long as it's off by default.

You should mirror HttpClientHandler.DangerousAcceptAnyServerCertificateValidator's approach and ensure there's one named the same way that will work for cert auth.

alexiwonder commented 1 year ago

Hello all, Sorry for interrupting your conversation. Just wanted to update you that I made a small change by removing options.ChainTrustValidationMode = X509ChainTrustMode.CustomRootTrust; line and now the server is accepting the presented client certificate. Plus, OnAuthenticationFailed callback is not invoked anymore:

.AddCertificate(options =>
{
   options.RevocationMode = X509RevocationMode.NoCheck;
   options.AllowedCertificateTypes = CertificateTypes.All;
}

I also cleaned up some of the configs that appeared to have no impact on both Kestrel and certificate authentication in the scenario of disabling all the client certificate validations.

So the final working version of the server is:

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        builder.Services.Configure<KestrelServerOptions>(options =>
        {
            options.ConfigureHttpsDefaults(options =>
            {
                options.AllowAnyClientCertificate();
                options.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
            });
        });

        // Add services to the container.

        builder.Services.AddControllers();

        ConfigureServices(builder.Services);

        var app = builder.Build();

        // Configure the HTTP request pipeline.

        app.UseHttpsRedirection();

        app.UseAuthentication();

        app.MapControllers();

        app.Run();
    }

    private static void ConfigureServices(IServiceCollection services)
    {
        services
           .AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
           .AddCertificate(options =>
           {
               options.RevocationMode = X509RevocationMode.NoCheck;
               options.AllowedCertificateTypes = CertificateTypes.All;

               options.Events = new CertificateAuthenticationEvents
               {
                   OnAuthenticationFailed = p =>
                   {
                       Console.WriteLine(p.Exception);
                       return Task.CompletedTask;

                   },
                   OnCertificateValidated = context =>
                   {
                       // Add certificate to HttpContext.Items
                       context.HttpContext.Items["ClientCertificate"] = context.ClientCertificate;
                       return Task.CompletedTask;
                   }
               };
           });
    }
}

I have no solid explanation why this is working. Any clarification is highly appreciated.

Thanks all for your attention.

Vaccano commented 1 year ago

I too would love an explanation as to why this worked for alexiwonder. I have been stuck with this same issue for a while and I cannot seem to find a way around it.

I tried alexiwonder's code and I still hit the OnAuthenticationFailed event.

I struggle on how to implement the 'CustomTrustStore`, as I don't own certificate that our company uses to sign all of our internal certificates...

Vaccano commented 1 year ago

I was able to get a copy of our root cert for the CustomTrustStore. Adding that in got it working! Here is some sample code:

var certString = "-----BEGIN CERTIFICATE-----\r\nIMI     >Your Cert Data Here<   humv\r\n-----END CERTIFICATE-----";
ReadOnlySpan<char> rootCert = new ReadOnlySpan<char>(certString .ToCharArray());
var rootCertificate2 = X509Certificate2.CreateFromPem(rootCert);
options.CustomTrustStore.Add(rootCertificate2);
options.ChainTrustValidationMode = X509ChainTrustMode.CustomRootTrust;

This is added in the AddCertificate lambda method.

ghost commented 7 months ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

stephannn commented 3 months ago

Hi @Vaccano , I am struggling with the same issue. I modified my code like that:

            options.RevocationMode = X509RevocationMode.NoCheck;
            options.AllowedCertificateTypes = CertificateTypes.All;

            List<string> customTrust = builder.Configuration.GetSection("Certificate").GetSection("TrustStore").Get<List<string>>() ?? new List<string>();  
            if (customTrust != null && customTrust.Count != 0)
            {
                foreach (var customTrustSingle in customTrust) {
                    try
                    {
                        options.CustomTrustStore.Add(new X509Certificate2(Path.Combine(customTrustSingle)));
                        Console.WriteLine($"Added custom trust certificate: {customTrustSingle}");
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"Failed to add custom trust certificate: {customTrustSingle}, Error: {ex.Message}");
                    }
                }
                options.ChainTrustValidationMode = X509ChainTrustMode.CustomRootTrust;
            }

            options.ValidateCertificateUse = false;
            options.ValidateValidityPeriod = false;

in the customTrust list I load my root and intermediate certificate but getting still the warning about it:

Certificate validation failed, subject was CN=PC1.contoso.com. PartialChain unable to get local issuer certificate
Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationHandler: Warning: Certificate validation failed, subject was CN=PC1.contoso.org. PartialChain unable to get local issuer certificate

and it hits the OnAuthenticationFailed event.

I also uploaded it to an Azure Web App and I also cannot use the certificate authentication. I have to say, I do not know how to add the root and intermediate cert to the Web App, but this is actually the CustomRootStore for, isn't it?

Edit: Issue was a new intermediate cert that I did not put. After that it hits the OnCertificateValidated event. Please ignore!

Varorbc commented 2 weeks ago

I wonder what the CustomRootStore does and it doesn't work for me either