首页 > 解决方案 > 客户端证书 - 403 - 禁止访问:访问被拒绝

问题描述

当第三方尝试使用 .cer 格式的证书调用我的 API 端点时,我从 .pfx 文件中导出并发送给他们。他们将得到 403 - Forbidden: Access is denied。您无权使用您提供的凭据查看此目录或页面。

我调查可能导致此问题的原因。

我现在很困惑,第 3 方只接受 .cer 格式的客户证书,据我所知,.pfx 是用于内部组织而不是外部组织。

** 我应该注意我的客户证书不包含私钥,它只包含公钥。
** 我确定服务器和 IIS 上的所有配置都是正确的。
** 我不确定如何使用 .cer 格式将私钥添加到我的客户端证书中!还是我应该?!

我在这里错过了什么吗!它已经为此工作了 4 天,但仍然没有运气:(

任何人都可以帮助我或指出我正确的方向!谢谢 :)

这就是我在 ASP.NET Core 3.1 中获取客户端证书的方式:

MyCertificateValidationService.cs

我将我拥有的客户端证书与从请求中获得的客户端证书进行了比较:

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;
    }

我的 API 端点:

[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
    }

}

启动.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;
}

程序.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();
}

标签: c#asp.net-coresslclient-certificates

解决方案


让我们澄清一下 SSL 客户端证书身份验证的工作原理。下面我假设“证书”从不包含私钥,只包含公钥。

客户端在 SSL 握手期间提供他拥有的证书。服务器验证此证书是否符合某些任意标准,例如服务器可能要求它是由某个证书颁发机构颁发的,或者像您的情况一样 - 这是一个特定的证书(它的指纹与您期望的相符)。

现在,客户端必须向服务器证明他实际上拥有此证书的私钥,简单来说,通过使用该私钥签署一些信息,然后服务器使用客户端的证书(之前发送,如上所述)公钥验证这一点。

如果客户端成功证明他拥有给定证书的私钥,并且该证书符合服务器的标准 - 然后客户端通过身份验证并可以继续。

您已经可以看到为什么您当前的方法不起作用 - 您发送给客户端的 .cer 文件不包含私钥,因此它不能用于身份验证目的。

现在你有两种方法:

1 - 您生成新证书,并将证书和私钥发送给您的客户。这种情况的变化是您创建自己的证书颁发机构,然后在该机构下颁发此类客户端证书。然后在验证代码中,您可以确保证书是由您的权威机构颁发的,而不是直接比较指纹。如果您有成千上万的客户,这是合理的方式。

这种方法的缺点是您现在拥有(或曾经拥有)您不需要的安全信息 - 即为您的客户颁发的证书的私钥。如果使用该客户的私钥发生未经授权的访问(客户的私钥被盗) - 理论上客户可以声称您泄露了此密钥。另一个缺点是您必须通过一些最好是安全的通道传递敏感数据(私钥)。如果您只是将私钥通过电子邮件发送给客户端 - 任何不好的事情都可能发生(例如客户端不会删除它,然后他的电子邮件被黑客入侵并且密钥泄露给黑客)。

2 - 您的客户生成证书和私钥。如果您的客户不多,这是最好的方法。客户为自己存储私钥并向您发送不包含私钥的证书(例如 .cer 格式)。现在他如上所述进行身份验证,您只需验证 SSL 握手中提供的证书与客户端预先发送给您的证书匹配(就像您现在所做的那样,通过比较指纹)。然后 Asp.net 确保客户端具有此证书的匹配私钥。

现在,不必在任何地方发送敏感数据,并且万一客户泄露了他的私钥 - 你不能对此负责,因为你从来没有这个密钥。

旁注:如果您通过为您的客户端生成证书来进行第一条路线 - 请注意它是一个新证书,与您的服务器证书完全无关。您的服务器证书私钥当然不应该发送到任何地方。这与您的评论有关“但将 pfx 发送给他们并不危险?!”。不,因为您刚刚专门为该客户端生成了这个 pfx。


推荐阅读