c# - 客户端证书 - 403 - 禁止 : Access is denied

标签 c# asp.net-core ssl client-certificates

当第三方尝试使用 .cer 格式的证书调用我的 API 端点时,我从 .pfx 文件导出该证书并将其发送给他们。 他们将收到 403 - 禁止:访问被拒绝。您无权使用您提供的凭据查看此目录或页面。

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

  • 当我在“个人”下的证书存储中安装/导入 .cer 格式的证书并且尝试调用我的端点时,我可以看到我的证书不在证书列表中,然后我点击“确定”按钮,我将收到 403 - 禁止:访问被拒绝。

但是

  • 当我在“个人”下的证书存储中使用 passepharse 安装/导入 .pfx 格式的证书时,然后我尝试在浏览器上以及通过 postman 调用我的端点,这次我可以在浏览器上的证书列表中看到我的证书,并且比我选择证书并点击按钮,我将成功进入目录,并且我还会在 postman 中收到 200 ok 响应,当然在 postman 中添加 .pfx 格式的证书。

现在我很困惑,第 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
    }

}

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://learn.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();
}

最佳答案

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

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

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

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

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

现在你有两种方法:

1 - 您生成新的证书,并将证书和私钥发送给您的客户端。其变体是您创建自己的证书颁发机构,然后在该机构下颁发此类客户端证书。然后在验证代码中,您只需确保证书是由您的机构颁发的,而不是直接比较指纹。如果您有数千个客户,这是合理的方式。

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

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

现在,任何敏感数据都不必发送到任何地方,并且万一客户端泄露了他的私钥 - 您无需为此负责,因为您从一开始就从未拥有过此 key 。

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

关于c# - 客户端证书 - 403 - 禁止 : Access is denied,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64750366/

相关文章:

c# - 如何从 net Core 中的异常中获取行号和文件名?

c# - 服务器标记格式不正确

c# - 使用 iTextSharp 提取 FlateDecode 图像

logging - IIS 托管的 ASP.NET Core 应用程序中的控制台日志在哪里

firefox - 在带有 ssl 和 firefox 的本地主机上使用 websocket

email - 单独的电子邮件服务器和 Web 托管服务器上的 SSL 证书?

c# - 获取BuiltInParameterGroup枚举值的人类可读名称

c# - 自定义标签助手如何在列表中设置 id 和名称

ssl - 是否可以在同一个独立的 Wildfly 上运行 SSL 和非 SSL Web 应用程序?

asp.net-core - Entity Framework 核心-相当于IN子句