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

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

此POC中的Web API非常简单,只返回单个值。 它使用一个属性来validation是否使用了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中,我只是检查客户端证书的可用性。 一旦这个工作,我可以添加证书中的信息检查,以validation证书列表。

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

在此处输入图像描述

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

  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并且它没有找到任何客户端证书。 通过调试器运行此程序我已validation证书已加载并添加到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().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
  • 我确认证书已设置为客户端身份validation,并且它位于受信任的根目录中
  • 除了在Fiddler中测试客户端证书外,我还在Chrome中对其进行了validation。

在尝试这些选项的过程中,它开始工作。 然后我开始退出更改以查看导致其工作的原因。 它继续工作。 然后我尝试从受信任的根目录中删除证书以validation这是否是必需的并且它已停止工作,现在即使我将证书放回受信任的根目录中,我也无法恢复工作。 现在,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 

这是我正在使用的客户端证书:

在此处输入图像描述

在此处输入图像描述

在此处输入图像描述

我很困惑这个问题是什么。 我正在为任何可以帮助我解决这个问题的人添加赏金。

跟踪帮助我找到问题所在(感谢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似乎与我的问题类似 。 我的解决方案不是标记为答案的解决方案,因为我的证书位于受信任的根目录中。 答案是你更新注册表的地方。 我刚刚将值添加到注册表项。

HKEY_LOCAL_MACHINE \系统\ CurrentControlSet \控制\ SecurityProviders \ SCHANNEL

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

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

更新:

来自Microsoft的示例:

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

原版的

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

首先,我编辑了\.vs\config\applicationhost.config并进行了此更改:

这允许我在web.config编辑并添加以下行,这些行需要在IIS Express中进行客户端认证。 注意:我为了开发目的编辑了这个,不允许在生产中覆盖。

对于生产,请按照这样的指南来设置IIS:

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

web.config中:

    

API控制器:

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

属性:

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

然后可以使用这样的客户端认证来调用API,从另一个Web项目进行测试。

 [RoutePrefix("api/certificatetest")] public class CertificateTestController : ApiController { public IHttpActionResult Get() { var handler = new WebRequestHandler(); handler.ClientCertificateOptions = ClientCertificateOption.Manual; handler.ClientCertificates.Add(GetClientCert()); handler.UseProxy = false; var client = new HttpClient(handler); var result = client.GetAsync("https://localhost:44331/api/values").GetAwaiter().GetResult(); var resultString = result.Content.ReadAsStringAsync().GetAwaiter().GetResult(); return Ok(resultString); } private static X509Certificate GetClientCert() { X509Store store = null; try { store = new X509Store(StoreName.My, StoreLocation.CurrentUser); store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly); var certificateSerialNumber= "‎81 c6 62 0a 73 c7 b1 aa 41 06 a3 ce 62 83 ae 25".ToUpper().Replace(" ", string.Empty); //Does not work for some reason, could be culture related //var certs = store.Certificates.Find(X509FindType.FindBySerialNumber, certificateSerialNumber, true); //if (certs.Count == 1) //{ // var cert = certs[0]; // return cert; //} var cert = store.Certificates.Cast().FirstOrDefault(x => x.GetSerialNumberString().Equals(certificateSerialNumber, StringComparison.InvariantCultureIgnoreCase)); return cert; } finally { store?.Close(); } } } 

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

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

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

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

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

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

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

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

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

(参见SecureChannnel类的源代码 ,方法EnsurePrivateKey)

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

您可以激活网络跟踪以尝试对其进行调试。 它会给你这样的输出:

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