OnCertificateValidated未运行 - 自签名证书客户端身份验证 - ASP.NET Core和Kestrel

14
我希望能够使用基于证书的身份验证来验证连接到在Kestrel上运行的ASP.NET Core Web API(.NET 5)的客户端。
在我的 Startup.cs 中,我在 ConfigureServices 中有以下内容:
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate(options =>
    {
        options.AllowedCertificateTypes = CertificateTypes.All;
        options.Events = new CertificateAuthenticationEvents
        {
            OnCertificateValidated = context =>
            {
                // More code to verify certificates
            },
            OnAuthenticationFailed = context =>
            {
                // More code
            }
        };
    });

// Other services

而在配置中:

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthentication();

app.UseEndpoints(endpoints =>
{
    // Endpoints
});

Program.cs中,我已经包含了:

webBuilder.ConfigureKestrel(o =>
{
    o.ConfigureHttpsDefaults(o =>
        o.ClientCertificateMode = ClientCertificateMode.RequireCertificate);
});

如果我在浏览器中连接API,它会提示我选择证书,但是在我选择了证书后,既没有触发OnCertificateValidated事件,也没有触发OnAuthenticationFailed事件。经过进一步测试,我意识到在Startup.csAddCertificate调用内的整个选项配置委托从未运行。这让我认为我缺少了某种针对Kestrel的配置,但我不知道是什么。请注意,我的Web API不使用IIS托管。还需要做什么来使用基于自签名证书的身份验证?
我目前所拥有的代码是基于此处文档中找到的指令: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-5.0

1
您正在使用HTTPS(安全)协议,该协议使用TLS进行身份验证。证书是身份验证的一部分。在TLS中,服务器发送一个包含可能用于身份验证的证书名称的证书块。然后,客户端将名称与存储中的证书(或应用程序中指定的证书)进行比较。如果名称不匹配,则TLS将失败。自Net 4.7.2或更高版本以来,TLS在操作系统中完成而非在Net中完成。在移动设备上,内核必须能够支持TLS的版本(1.2或1.3)。您是否正在使用HTTPS(而非HTTP)? - jdweng
@jdweng 我正在使用HTTPS。但我并不是在尝试证明服务器的真实性(那部分已经可以工作了),我正在尝试使用从客户端发送到服务器的证书来验证客户端,就像https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-5.0中所描述的那样。 - TheProgrammerNinja3.14
你无法这样做。证书必须加载到客户端和服务器上,不能随消息发送。证书是私钥,你不想将密钥与消息一起发送,这就像把你家的钥匙给小偷一样。TLS服务器只发送一个带有证书名称的证书块,然后客户端使用其中一个命名的证书来加密消息。 - jdweng
2个回答

20

好的,最终我能够解决自己的问题。解决问题有两个不同的部分,但最终只需要对我的项目代码进行一些小修改。

识别客户端证书

首先,服务器无法将自签名的客户端证书识别为有效证书。可以通过以下两种方法之一解决此问题:1.将所有客户端证书(或签署它们的根CA)添加到操作系统的受信任证书存储中;2.向Kestrel添加ClientCertificateValidation回调函数以确定是否接受或拒绝证书。

#2的示例(对Program.cs中的ConfigureHttpsDefaults lambda进行调整)如下:

webBuilder.ConfigureKestrel(o =>
{
    o.ConfigureHttpsDefaults(opts =>
    {
        opts.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
        opts.ClientCertificateValidation = (cert, chain, policyErrors) =>
        {
            // Certificate validation logic here
            // Return true if the certificate is valid or false if it is invalid
        };
    });
});

顺便提一下,调用opts.AllowAnyClientCertificate()是一个简写方式,它添加了一个ClientCertificateValidation回调函数,该函数始终返回true,使得所有自签名证书均有效。


所需授权

应用了上述方法之后,我的API将接受来自有效证书的查询,但在OnCertificateValidated事件中的额外证书验证逻辑仍未运行。因为根据关于ASP.NET Core问题#14033的评论,除非对访问的端点启用了授权,否则此事件的额外证书验证将永远不会运行。这是有道理的,因为根据ASP.NET Core文档中该主题的说明,此事件通常用于从证书生成ClaimsPrincipal。将ASP.NET设置为使用授权并要求对API调用进行授权(例如,将[Authorize]属性应用于所有控制器)会导致那些API调用运行附加身份验证检查。

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthentication();

// Adding this and adding the [Authorize] attribute
// to controllers fixes the problem.
app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
    // Endpoints
});

之后,每个连接都调用了我的OnCertificateValidated事件,我能够执行额外的身份验证逻辑并拒绝无效证书。


2
你很棒,你的帖子帮了我很多。 - IcyBrk
谢谢您的回复。我一直在遇到Postman套接字挂起问题,但没有任何解释。仅当我使用浏览器时,错误才更加明显。 - Cam Bruce

2

接受的答案对我非常有帮助(谢谢!),但它并没有完全解决我的问题。

我发现ClientCertificateValidation函数并不是验证(和拒绝)证书的唯一位置。使用AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme). AddCertificate...也会触发另一个验证,在处理ClientCertificateValidation之后。

为了解决我的问题,我必须在CertificateAuthenticationOptions:中添加CustomTrustStore配置行。

AddCertificate(options =>
{
    options.AllowedCertificateTypes = CertificateTypes.All;
    options.ChainTrustValidationMode = X509ChainTrustMode.CustomRootTrust;
    options.CustomTrustStore = new X509Certificate2Collection { rootCert };
    options.RevocationMode = X509RevocationMode.NoCheck;
    options.Events = new CertificateAuthenticationEvents
    {
        OnCertificateValidated = context =>
        {
            if (validationService.ValidateCertificate(context.ClientCertificate))
            {
                context.Success();
            }
            else
            {
                context.Fail("invalid cert");
            }

            return Task.CompletedTask;
        },
        OnAuthenticationFailed = context =>
        {
            context.Fail("invalid cert");
            return Task.CompletedTask;
        }
    };
});

其中 rootCert 是这样初始化的:

var rootCert = new X509Certificate2("RootCert.pfx", "1234");

同时将 RootCert.pfx 作为文件添加到项目中。

由于我们现在使用了内置的验证方式 AddAuthentication(.).AddCertificate,因此我们应该禁用早期的验证方式 AllowAnyCertificate()(这种验证方式不知道我们自定义的根信任):

builder.WebHost.ConfigureKestrel(kso =>
{
    kso.ConfigureHttpsDefaults(cao => {
        cao.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
        cao.AllowAnyClientCertificate();
        cao.CheckCertificateRevocation = false;
    });
});

当我部署到Azure应用服务时,这对我非常有用。由于安全原因,无法添加受信任的根证书,这是一个不错的解决方法! - benk
你如何使用dotnet为自签名证书创建根证书? - RADU

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接