c# - 客户端证书 - 403 - 禁止访问:访问被拒绝
问题描述
当第三方尝试使用 .cer 格式的证书调用我的 API 端点时,我从 .pfx 文件中导出并发送给他们。他们将得到 403 - Forbidden: Access is denied。您无权使用您提供的凭据查看此目录或页面。
我调查可能导致此问题的原因。
- 当我在 Personal 下的证书存储中安装/导入 .cer 格式的证书,然后尝试调用我的端点时,我可以看到我的证书不在证书列表中,然后我点击确定按钮,我将得到 403 - 禁止访问:访问被拒绝.
但
- 当我在 Personal 下的证书存储中使用密码以 .pfx 格式安装/导入证书然后我尝试在浏览器和邮递员上调用我的端点这次我可以在浏览器上的证书列表中看到我的证书,然后我选择证书并点击按钮我将成功进入目录,并且我还会在邮递员中获得 200 ok 响应,当然在邮递员中添加 .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
}
}
启动.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();
}
解决方案
让我们澄清一下 SSL 客户端证书身份验证的工作原理。下面我假设“证书”从不包含私钥,只包含公钥。
客户端在 SSL 握手期间提供他拥有的证书。服务器验证此证书是否符合某些任意标准,例如服务器可能要求它是由某个证书颁发机构颁发的,或者像您的情况一样 - 这是一个特定的证书(它的指纹与您期望的相符)。
现在,客户端必须向服务器证明他实际上拥有此证书的私钥,简单来说,通过使用该私钥签署一些信息,然后服务器使用客户端的证书(之前发送,如上所述)公钥验证这一点。
如果客户端成功证明他拥有给定证书的私钥,并且该证书符合服务器的标准 - 然后客户端通过身份验证并可以继续。
您已经可以看到为什么您当前的方法不起作用 - 您发送给客户端的 .cer 文件不包含私钥,因此它不能用于身份验证目的。
现在你有两种方法:
1 - 您生成新证书,并将证书和私钥发送给您的客户。这种情况的变化是您创建自己的证书颁发机构,然后在该机构下颁发此类客户端证书。然后在验证代码中,您可以确保证书是由您的权威机构颁发的,而不是直接比较指纹。如果您有成千上万的客户,这是合理的方式。
这种方法的缺点是您现在拥有(或曾经拥有)您不需要的安全信息 - 即为您的客户颁发的证书的私钥。如果使用该客户的私钥发生未经授权的访问(客户的私钥被盗) - 理论上客户可以声称您泄露了此密钥。另一个缺点是您必须通过一些最好是安全的通道传递敏感数据(私钥)。如果您只是将私钥通过电子邮件发送给客户端 - 任何不好的事情都可能发生(例如客户端不会删除它,然后他的电子邮件被黑客入侵并且密钥泄露给黑客)。
2 - 您的客户生成证书和私钥。如果您的客户不多,这是最好的方法。客户为自己存储私钥并向您发送不包含私钥的证书(例如 .cer 格式)。现在他如上所述进行身份验证,您只需验证 SSL 握手中提供的证书与客户端预先发送给您的证书匹配(就像您现在所做的那样,通过比较指纹)。然后 Asp.net 确保客户端具有此证书的匹配私钥。
现在,不必在任何地方发送敏感数据,并且万一客户泄露了他的私钥 - 你不能对此负责,因为你从来没有这个密钥。
旁注:如果您通过为您的客户端生成证书来进行第一条路线 - 请注意它是一个新证书,与您的服务器证书完全无关。您的服务器证书私钥当然不应该发送到任何地方。这与您的评论有关“但将 pfx 发送给他们并不危险?!”。不,因为您刚刚专门为该客户端生成了这个 pfx。
推荐阅读
- c++ - 为什么 >> 运算符在不同的编译器上显示不同的结果?
- javascript - ReactJS:映射对象数组
- bigcommerce - 轮播在 Stencil 主题的网页上为空
- javascript - 为什么输入错误的单词时不显示警告消息?
- javascript - 在执行 useEffect 清理功能之前获取“无法对未安装的组件执行 React 状态更新”
- php - 在选择类型字段中显示上一年
- c# - Azure - 无法在现有服务计划上创建新的 Web 应用程序
- powershell - 使用 Powershell 将计算机从 csv 文件移动到正确的 OU
- python - 将属性传递给基本 celery 任务的最佳实践
- azure - 如何在 Kusto 中将秒转换为 HH:mm:ss 格式