我正在尝试使用客户端证书通过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中,我只是检查客户端证书的可用性。一旦这起作用,我就可以在证书中添加对信息的检查,以根据证书列表进行验证。
以下是此web应用程序的SSL的IIS中的设置。
以下是发送带有客户端证书的请求的客户端的代码。这是一个控制台应用程序。
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禁止的状态代码,以及一个“客户端证书需要”的原因短语,这表明它正在进入我的RequireHttpsAttribute,并且它没有找到任何客户端证书。通过调试器运行它,我已经验证了证书正在加载并添加到WebRequestHandler中。证书将导出到正在加载的CER文件中。带有私钥的完整证书位于web应用程序服务器的本地计算机的Personal和Trusted Root存储区中。在此测试中,客户端和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:我仍在尝试解决这个问题,并尝试了许多不同的选择,但都无济于事。
<代码>F220
在尝试这些选项的过程中,它一度开始工作。然后,我开始撤销更改,看看是什么原因导致了它的工作。它继续工作。然后,我尝试从受信任的根中删除证书,以验证这是必需的,但它停止了工作,现在我无法让它恢复工作,即使我将证书放回了受信任的根中。现在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
下面是我使用的客户端证书:
我对问题是什么感到困惑。我正在为任何可以帮助我解决这个问题的人添加一个赏金。
发布于 2016-03-09 00:19:35
跟踪帮助我找到了问题所在(谢谢Fabian的建议)。通过进一步的测试,我发现我可以让客户端证书在另一台服务器(Windows server 2012)上工作。我在我的开发机器(Windows7)上进行了测试,这样我就可以调试这个过程了。因此,通过将跟踪与工作的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\Control\SecurityProviders\SCHANNEL
False值名称: SendTrustedIssuerList值类型: REG_DWORD值数据:0 (False)
在将这个值添加到注册表之后,它开始在我的Windows7机器上工作。这似乎是Windows 7的问题。
发布于 2017-11-18 08:30:20
更新:
来自Microsoft的示例:
原始
这就是我如何让客户端证书工作并检查特定的根CA是否颁发了它,以及它是不是一个特定的证书。
首先,我编辑了<src>\.vs\config\applicationhost.config
并进行了以下更改:<section name="access" overrideModeDefault="Allow" />
这允许我在web.config
中编辑<system.webServer>
,并在IIS Express中添加以下需要客户端认证的行。注意:我出于开发目的对此进行了编辑,不允许在生产环境中进行覆盖。
对于生产环境,请按照如下指南设置IIS:
https://medium.com/@hafizmohammedg/configuring-client-certificates-on-iis-95aef4174ddb
web.config:
<security>
<access sslFlags="Ssl,SslNegotiateCert,SslRequireCert" />
</security>
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<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;
}
}
然后,可以调用具有客户端认证的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<X509Certificate>().FirstOrDefault(x => x.GetSerialNumberString().Equals(certificateSerialNumber, StringComparison.InvariantCultureIgnoreCase));
return cert;
}
finally
{
store?.Close();
}
}
}
发布于 2016-02-24 00:15:40
确保HttpClient有权访问完整的客户端证书(包括私钥)。
您使用文件"ClientCertificate.cer“调用GetCert,这会导致假设没有包含私钥-而应该是windows中的pfx文件。从windows cert存储中访问证书并使用指纹进行搜索可能会更好。
复制指纹时要小心:在证书管理中查看时有一些不可打印的字符(将字符串复制到notepad++并检查显示字符串的长度)。
https://stackoverflow.com/questions/35582396
复制相似问题