Client Certificate - 403 - Forbidden: Access is denied

Viewed 1636

When a third party tries to call my API endpoint with the certificate in .cer format, which I exported from the .pfx file and sent to them. They will get 403 - Forbidden: Access is denied. You do not have permission to view this directory or page using the credentials that you supplied.

I investigate what could be caused this problem.

  • When i Install/import Certificate in .cer format in certificate store under Personal and than i try to call my endpoint i can see my certificate is NOT on list of certificates and than i hit ok button i will get 403 - Forbidden: Access is denied.

BUT

  • When i Install/import Certificate in .pfx format with passepharse in certificate store under Personal and then i try to call my endpoint on browser and also with postman THIS time i can see my certificate in the list of certificates on browser and than i choose the certificate and hit button i will successfully coming into directory and i also get 200 ok response in postman with ofcourse add certificate in .pfx format in postman.

And im confused now 3rd party only accept Client Certificate in .cer format and as i understand .pfx is for inside organization and not for outside organization.

** I should note my Client Certificate contains no Private Key its only contains Public key.
** I'm sure All configuration on server and IIS it is correct.
** I'm not sure how to add private key into my Client certificate with .cer format! or should i ?!

Did i missed something here ! its been 4 days working on this but still no luck :(

Can anyone please help me or point me into right direction! thanks :)

This is how i get Client certificate in ASP.NET Core 3.1:

MyCertificateValidationService.cs

I compared the client certificate I have with the client certificate I get from the request:

public class MyCertificateValidationService 
{
    public bool ValidateCertificate(X509Certificate2 clientCertificate)
    {
        try
        {
            var _path = @"c:\temp\ClientCertification.cer";
            var cert2 = new X509Certificate2(File.ReadAllBytes(_path));

            if (clientCertificate.Thumbprint == cert2.Thumbprint)
            {
                return true;
            }
        }
        catch (System.Exception)
        {
           throw;
        }

        return false;
    }

My API Endpoint:

[Authorize]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{

    [Consumes("application/xml")]
    [Produces("application/xml")]
    [ProducesResponseType(typeof(DespatchAdvice), (int)HttpStatusCode.OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
    [ProducesDefaultResponseType]
    [HttpPost("SendDespatch")]
    public IActionResult SendDespatch([FromBody] DespatchAdvice despatches)
    {
       //do something
    }

}

Startup.cs

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<MyCertificateValidationService>();
    services.AddSingleton<MailHandler>();
    services.AddScoped<IDespatch, DespatchRepo>();
    services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
        .AddCertificate(options => // code from ASP.NET Core sample
        {
            // https://docs.microsoft.com/en-us/aspnet/core/security/authentication/certauth
            options.AllowedCertificateTypes = CertificateTypes.All;
            //options.RevocationMode = X509RevocationMode.NoCheck;

            options.Events = new CertificateAuthenticationEvents
            {
                OnCertificateValidated = context =>
                {
                    var validationService =
                        context.HttpContext.RequestServices.GetService<MyCertificateValidationService>();

                    if (validationService.ValidateCertificate(context.ClientCertificate))
                    {
                        var claims = new[]
                        {
                            new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer),
                            new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer)
                        };

                        context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
                        context.Success();
                    }
                    else
                    {
                      
                        context.Fail("invalid cert");
                    }
                    
                    return Task.CompletedTask;
                }
            };
        });

    services.AddAuthorization();
    services.AddCertificateForwarding(options =>
    {
        options.CertificateHeader = "X-ARR-ClientCert";
        options.HeaderConverter = (headerValue) =>
        {
            X509Certificate2 clientCertificate = null;
            if (!string.IsNullOrWhiteSpace(headerValue))
            {
                byte[] bytes = StringToByteArray(headerValue);
                clientCertificate = new X509Certificate2(bytes);
            }

            return clientCertificate;
        };
    });
    services.AddControllers().AddXmlSerializerFormatters();
  
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseRouting();
    app.UseCertificateForwarding();
    app.UseAuthentication();
    app.UseAuthorization();
  
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}
private static byte[] StringToByteArray(string hex)
{
    int NumberChars = hex.Length;
    byte[] bytes = new byte[NumberChars / 2];
    for (int i = 0; i < NumberChars; i += 2)
        bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
    return bytes;
}

Program.cs

public class Program
{
    public static void Main(string[] args)
    {
        BuildWebHost(args).Run();
    }

    public static IWebHost BuildWebHost(string[] args)
        => WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .ConfigureKestrel(options =>
        {
            var cert = new X509Certificate2(Path.Combine("cert.pfx"), "password");
            options.ConfigureHttpsDefaults(o =>
            {
                o.ServerCertificate = cert;
                o.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
            });
        })
        .Build();
}
1 Answers

Let's clarify how SSL client certificate authentication works. Below I assume that "certificate" never contains private key, only public key.

Client provides a certificate he owns during SSL handshake. Server validates that this certificate matches some arbitrary criterias, for example server might require that it was issued by certain certification authority, or, like in your case - that this is a specific certificate (it's thumbprint matches what you expect).

Now, client must prove to server that he actually owns private key for this certificate, in simple terms by signing some info with that private key, and server then verifies that with client's certificate (sent before, as described above) public key.

If client successfully proves he owns private key for given certificate, AND that certificate matches server's criterias - then client is authenticated and can proceed.

You can already see why your current approach cannot work - .cer file you are sending to client does not contain private key, so it cannot be used for authentication purposes.

Now you have two ways:

1 - YOU generate fresh certificate, and send both certificate and private key to your client. Variation of this is when you create your own certification authority and then issue such client certificate under that authority. Then in validation code you can just ensure that certificate was issued by your authority instead of direct comparision of thumbprints. This is reasonable way if you have thousands of clients.

Drawback of such approach is that you now have (or had at one point) secure information you do not need - that is private key of certificate issued for your client. If unauthorized access had happened using that client's private key (client had his private key stolen) - client in theory can claim that YOU leaked this key. Another drawback is that you have to pass sensitive data (private key) over some, preferrably secure channel. If you just email private key to the client - anything bad can happen (like client won't delete it, then later his email is hacked and key leaks to the hacker).

2 - YOUR CLIENT generates certificate and private key. This is the best way in case you have not much clients. Client stores private key for himself and sends you certificate (say in .cer format) which does not contain private key. Now he authenticates as described above, and you just validate that certificate provided in SSL handshake matches certificate sent to you by client beforehand (like you are doing now, by comparing thumbprint). Asp.net then ensures that client has matching private key for this certificate.

Now, no sensitive data has to be sent anywhere, and in case client leaks his private key - you cannot be responsible for that since you never ever had this key in the first place.

Side note: if you are going 1st route by generating certificate for your client - note that it's a fresh certificate, completely unrelated to your server certificate. Your server certificate private key should of course not be ever sent anywhere. That's related to your comment "but it’s not dangerous to send pfx to Them ?!". No, because you just generated this pfx specifically for that client.

Related