如何使用客户端证书在Web API中进行身份验证和授权

时间:2016-02-23 16:06:57

标签: c# security asp.net-web-api ssl-certificate client-certificates

我正在尝试使用客户端证书来使用Web API对设备进行身份验证和授权,并开发了一个简单的概念证明来解决潜在解决方案的问题。我遇到的问题是Web应用程序没有收到客户端证书。许多人报告了这个问题,including in this Q&A,但他们都没有答案。我希望提供更多细节来重振这个问题,并希望得到我的问题的答案。我对其他解决方案持开放态度。主要要求是用C#编写的独立进程可以调用Web API并使用客户端证书进行身份验证。

此POC中的Web API非常简单,只返回单个值。它使用一个属性来验证是否使用了HTTPS以及是否存在客户端证书。

public class SecureController : ApiController
{
    [RequireHttps]
    public string Get(int id)
    {
        return "value";
    }

}

以下是RequireHttpsAttribute的代码:

public class RequireHttpsAttribute : AuthorizationFilterAttribute 
{ 
    public override void OnAuthorization(HttpActionContext actionContext) 
    { 
        if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps) 
        { 
            actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden) 
            { 
                ReasonPhrase = "HTTPS Required" 
            }; 
        } 
        else 
        {
            var cert = actionContext.Request.GetClientCertificate();
            if (cert == null)
            {
                actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                {
                    ReasonPhrase = "Client Certificate Required"
                }; 

            }
            base.OnAuthorization(actionContext); 
        } 
    } 
}

在这个POC中,我只是检查客户端证书的可用性。一旦这个工作,我可以添加证书中的信息检查,以根据证书列表进行验证。

以下是IIS中针对此Web应用程序的SSL设置。

enter image description here

以下是使用客户端证书发送请求的客户端的代码。这是一个控制台应用程序。

    private static async Task SendRequestUsingHttpClient()
    {
        WebRequestHandler handler = new WebRequestHandler();
        X509Certificate certificate = GetCert("ClientCertificate.cer");
        handler.ClientCertificates.Add(certificate);
        handler.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(ValidateServerCertificate);
        handler.ClientCertificateOptions = ClientCertificateOption.Manual;
        using (var client = new HttpClient(handler))
        {
            client.BaseAddress = new Uri("https://localhost:44398/");
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            HttpResponseMessage response = await client.GetAsync("api/Secure/1");
            if (response.IsSuccessStatusCode)
            {
                string content = await response.Content.ReadAsStringAsync();
                Console.WriteLine("Received response: {0}",content);
            }
            else
            {
                Console.WriteLine("Error, received status code {0}: {1}", response.StatusCode, response.ReasonPhrase);
            }
        }
    }

    public static bool ValidateServerCertificate(
      object sender,
      X509Certificate certificate,
      X509Chain chain,
      SslPolicyErrors sslPolicyErrors)
    {
        Console.WriteLine("Validating certificate {0}", certificate.Issuer);
        if (sslPolicyErrors == SslPolicyErrors.None)
            return true;

        Console.WriteLine("Certificate error: {0}", sslPolicyErrors);

        // Do not allow this client to communicate with unauthenticated servers.
        return false;
    }

当我运行此测试应用程序时,我返回状态代码403 Forbidden,其中包含“Client Certificate Required”的原因短语,表示它已进入我的RequireHttpsAttribute并且未找到任何客户端证书。通过调试器运行我已经验证证书已加载并添加到WebRequestHandler。证书将导出到正在加载的CER文件中。具有私钥的完整证书位于本地计算机的Web应用程序服务器的个人和受信任根存储中。对于此测试,客户端和Web应用程序正在同一台计算机上运行。

我可以使用Fiddler调用此Web API方法,附加相同的客户端证书,并且它可以正常工作。使用Fiddler时,它会通过RequireHttpsAttribute中的测试并返回成功的状态代码200并返回预期值。

是否有人遇到HttpClient未在请求中发送客户端证书并找到解决方案的问题?

更新1:

我还尝试从证书存储区获取包含私钥的证书。以下是我检索它的方法:

    private static X509Certificate2 GetCert2(string hostname)
    {
        X509Store myX509Store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
        myX509Store.Open(OpenFlags.ReadWrite);
        X509Certificate2 myCertificate = myX509Store.Certificates.OfType<X509Certificate2>().FirstOrDefault(cert => cert.GetNameInfo(X509NameType.SimpleName, false) == hostname);
        return myCertificate;
    }

我确认此证书已正确检索,并且已添加到客户端证书集合中。但是我得到了相同的结果,其中服务器代码没有检索任何客户端证书。

为了完整性,这里是用于从文件中检索证书的代码:

    private static X509Certificate GetCert(string filename)
    {
        X509Certificate Cert = X509Certificate.CreateFromCertFile(filename);
        return Cert;

    }

您会注意到,当您从文件中获取证书时,它会返回X509Certificate类型的对象,当您从证书存储区检索它时,它的类型为X509Certificate2。 X509CertificateCollection.Add方法需要一种X509Certificate。

更新2: 我仍然试图解决这个问题并尝试了许多不同的选择,但无济于事。

  • 我将Web应用程序更改为在主机名而不是本地主机上运行。
  • 我将Web应用程序设置为需要SSL
  • 我验证了证书是为客户端身份验证设置的,并且证书是在受信任的根目录中
  • 除了在Fiddler中测试客户端证书外,我还在Chrome中验证了它。

在尝试这些选项的过程中,它开始工作。然后我开始退出更改以查看导致其工作的原因。它继续工作。然后我尝试从受信任的根目录中删除证书以验证这是否是必需的并且它已停止工作,现在即使我将证书放回受信任的根目录中,我也无法恢复工作。现在,Chrome甚至不会提示我提供类似于使用的证书,但它在Chrome中失败,但仍可在Fiddler中使用。必须有一些我缺少的神奇配置。

我还尝试在绑定中启用“协商客户端证书”,但Chrome仍然不会提示我提供客户端证书。以下是使用“netsh http show sslcert”

的设置
 IP:port                 : 0.0.0.0:44398
 Certificate Hash        : 429e090db21e14344aa5d75d25074712f120f65f
 Application ID          : {4dc3e181-e14b-4a21-b022-59fc669b0914}
 Certificate Store Name  : MY
 Verify Client Certificate Revocation    : Disabled
 Verify Revocation Using Cached Client Certificate Only    : Disabled
 Usage Check    : Enabled
 Revocation Freshness Time : 0
 URL Retrieval Timeout   : 0
 Ctl Identifier          : (null)
 Ctl Store Name          : (null)
 DS Mapper Usage    : Disabled
 Negotiate Client Certificate    : Enabled

以下是我使用的客户端证书:

enter image description here

enter image description here

enter image description here

我对这个问题感到困惑。我正在为任何可以帮助我解决问题的人添加赏金。

6 个答案:

答案 0 :(得分:7)

追踪帮助我找到了问题所在(感谢Fabian的建议)。我发现进一步测试我可以获得客户端证书在另一台服务器上工作(Windows Server 2012)。我在我的开发机器(Window 7)上测试了这个,所以我可以调试这个过程。因此,通过将跟踪与一个工作的IIS服务器进行比较,将一个没有工作的IIS服务器进行比较,我能够确定跟踪日志中的相关行。以下是客户端证书工作日志的一部分。这是发送之前的设置

System.Net Information: 0 : [17444] InitializeSecurityContext(In-Buffers count=2, Out-Buffer length=0, returned code=CredentialsNeeded).
System.Net Information: 0 : [17444] SecureChannel#54718731 - We have user-provided certificates. The server has not specified any issuers, so try all the certificates.
System.Net Information: 0 : [17444] SecureChannel#54718731 - Selected certificate:

以下是客户端证书失败的计算机上的跟踪日志。

System.Net Information: 0 : [19616] InitializeSecurityContext(In-Buffers count=2, Out-Buffer length=0, returned code=CredentialsNeeded).
System.Net Information: 0 : [19616] SecureChannel#54718731 - We have user-provided certificates. The server has specified 137 issuer(s). Looking for certificates that match any of the issuers.
System.Net Information: 0 : [19616] SecureChannel#54718731 - Left with 0 client certificates to choose from.
System.Net Information: 0 : [19616] Using the cached credential handle.

关注指示服务器指定137个发布者的行我发现了Q&A that seemed similar to my issue。我的解决方案不是标记为答案的解决方案,因为我的证书位于受信任的根目录中。答案是the one under it,您可以在其中更新注册表。我刚刚将值添加到注册表项中。

<强> HKEY_LOCAL_MACHINE \ SYSTEM \ CURRENTCONTROLSET \控制\ SecurityProviders \ SCHANNEL

值名称:SendTrustedIssuerList值类型:REG_DWORD值数据:0(错误)

将此值添加到注册表后,它开始在我的Windows 7计算机上运行。这似乎是一个Windows 7问题。

答案 1 :(得分:6)

<强>更新

Microsoft的示例:

https://docs.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth#special-considerations-for-certificate-validation

<强>原始

这就是我如何使客户端认证工作并检查特定的根CA是否已发布它以及它是否为特定证书。

首先,我修改了foldLeft()并进行了此更改:<src>\.vs\config\applicationhost.config

这允许我编辑<section name="access" overrideModeDefault="Allow" />中的<system.webServer>并添加以下行,这些行需要在IIS Express中进行客户端认证。 注意:我为了开发目的而对其进行了编辑,不允许在生产中进行覆盖。

对于制作,请按照此类指南设置IIS:

https://medium.com/@hafizmohammedg/configuring-client-certificates-on-iis-95aef4174ddb

的web.config:

web.config

API控制器:

<security>
  <access sslFlags="Ssl,SslNegotiateCert,SslRequireCert" />
</security>

属性:

[RequireSpecificCert]
public class ValuesController : ApiController
{
    // GET api/values
    public IHttpActionResult Get()
    {
        return Ok("It works!");
    }
}

然后可以使用此类客户端认证调用API,并从另一个Web项目进行测试。

public class RequireSpecificCertAttribute : AuthorizationFilterAttribute
{
    public override void OnAuthorization(HttpActionContext actionContext)
    {
        if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps)
        {
            actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
            {
                ReasonPhrase = "HTTPS Required"
            };
        }
        else
        {
            X509Certificate2 cert = actionContext.Request.GetClientCertificate();
            if (cert == null)
            {
                actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                {
                    ReasonPhrase = "Client Certificate Required"
                };

            }
            else
            {
                X509Chain chain = new X509Chain();

                //Needed because the error "The revocation function was unable to check revocation for the certificate" happened to me otherwise
                chain.ChainPolicy = new X509ChainPolicy()
                {
                    RevocationMode = X509RevocationMode.NoCheck,
                };
                try
                {
                    var chainBuilt = chain.Build(cert);
                    Debug.WriteLine(string.Format("Chain building status: {0}", chainBuilt));

                    var validCert = CheckCertificate(chain, cert);

                    if (chainBuilt == false || validCert == false)
                    {
                        actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                        {
                            ReasonPhrase = "Client Certificate not valid"
                        };
                        foreach (X509ChainStatus chainStatus in chain.ChainStatus)
                        {
                            Debug.WriteLine(string.Format("Chain error: {0} {1}", chainStatus.Status, chainStatus.StatusInformation));
                        }
                    }
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex.ToString());
                }
            }

            base.OnAuthorization(actionContext);
        }
    }

    private bool CheckCertificate(X509Chain chain, X509Certificate2 cert)
    {
        var rootThumbprint = WebConfigurationManager.AppSettings["rootThumbprint"].ToUpper().Replace(" ", string.Empty);

        var clientThumbprint = WebConfigurationManager.AppSettings["clientThumbprint"].ToUpper().Replace(" ", string.Empty);

        //Check that the certificate have been issued by a specific Root Certificate
        var validRoot = chain.ChainElements.Cast<X509ChainElement>().Any(x => x.Certificate.Thumbprint.Equals(rootThumbprint, StringComparison.InvariantCultureIgnoreCase));

        //Check that the certificate thumbprint matches our expected thumbprint
        var validCert = cert.Thumbprint.Equals(clientThumbprint, StringComparison.InvariantCultureIgnoreCase);

        return validRoot && validCert;
    }
}

答案 2 :(得分:1)

确保HttpClient可以访问完整的客户端证书(包括私钥)。

您正在使用文件“ClientCertificate.cer”调用GetCert,这导致假设没有包含私钥 - 应该是Windows中的pfx文件。 从Windows证书库访问证书并使用指纹搜索它可能更好。

复制指纹时要小心:在证书管理中查看时有一些不可打印的字符(将字符串复制到记事本++并检查显示字符串的长度)。

答案 3 :(得分:1)

我实际上遇到了类似的问题,我们需要许多受信任的根证书。我们全新安装的网络服务器已经过时了。我们的根以字母Z开头,所以它最终在列表的末尾。

问题在于, IIS仅向客户端发送了前20个可信根,并截断了其余,包括我们的。几年前,它不记得该工具的名称......它是IIS管理套件的一部分,但Fiddler也应该这样做。在意识到错误之后,我们删除了许多我们不需要的可信根。这是试验和错误,所以要小心删除。

清理之后,一切都像魅力一样。

答案 4 :(得分:0)

查看源代码我也认为私钥一定存在问题。

它所做的实际上是检查传递的证书是否为X509Certificate2类型,以及它是否具有私钥。

如果找不到私钥,它会尝试在CurrentUser商店中找到证书,然后在LocalMachine商店中找到。如果找到证书,则检查是否存在私钥。

(参见SecureChannnel类的source code,方法EnsurePrivateKey)

因此,根据您导入的文件(.cer - 没有私钥或.pfx - 使用私钥)以及在哪个商店中找不到正确的文件,并且不会填充Request.ClientCertificate。

您可以激活Network Tracing尝试对此进行调试。它将为您提供如下输出:

  • 尝试在证书库中找到匹配的证书
  • 无法在LocalMachine商店或CurrentUser商店中找到证书。

答案 5 :(得分:0)

我最近遇到了一个类似的问题,根据Fabian的建议,我实际上找到了解决方案。 事实证明,使用客户证书必须确保两件事:

  1. 私钥实际上是作为证书的一部分导出的。

  2. 运行该应用程序的应用程序池标识可以访问所述私钥。

在我们的情况下,我必须:

  1. 在检查导出复选框以确保私钥已发送出去的同时,将pfx文件导入本地服务器存储中。
  2. 使用MMC控制台,将已使用的服务帐户授予对证书私钥的访问权限。

在其他答案中解释的受信任的根问题是有效的,对我们而言不是问题。