Web中如何使用客户端证书进行身份验证和授权API
How to use a client certificate to authenticate and authorize in a Web API
我正在尝试使用客户端证书通过 Web API 对设备进行身份验证和授权,并开发了一个简单的概念证明来解决潜在解决方案的问题。我正在 运行 解决 Web 应用程序未收到客户端证书的问题。有很多人报告了这个问题,including in this Q&A,但其中 none 有答案。我希望提供更多细节来解决这个问题,并希望得到我的问题的答案。我对其他解决方案持开放态度。主要要求是用 C# 编写的独立进程可以调用 Web API 并使用客户端证书进行身份验证。
此 POC 中的 Web API 非常简单,只有 returns 一个值。它使用一个属性来验证是否使用了 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 设置。
这是发送带有客户端证书的请求的客户端代码。这是一个控制台应用程序。
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,原因短语为“需要客户端证书”,表明它正在进入我的 RequireHttpsAttribute 并且没有找到任何客户端证书. 运行 通过调试器,我已验证证书正在加载并添加到 WebRequestHandler。证书将导出到正在加载的 CER 文件中。带有私钥的完整证书位于 Web 应用程序服务器的本地计算机的个人和受信任根存储中。对于此测试,客户端和 Web 应用程序 运行 在同一台机器上。
我可以使用 Fiddler 调用此 Web API 方法,附加相同的客户端证书,它工作正常。使用 Fiddler 时,它通过了 RequireHttpsAttribute 中的测试和 returns 成功状态代码 200 和 returns 预期值。
有没有人运行遇到过 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;
}
您会注意到,当您从文件中获取证书时,它 returns 是一个 X509Certificate 类型的对象,当您从证书存储中检索它时,它是 X509Certificate2 类型的对象。 X509CertificateCollection.Add 方法需要一种 X509Certificate。
更新二:
我仍在努力解决这个问题,并尝试了许多不同的选择,但无济于事。
- 我将 Web 应用程序更改为 运行 主机名而不是本地主机。
- 我将 Web 应用程序设置为需要 SSL
- 我确认证书是为客户端身份验证设置的,并且它在受信任的根目录中
- 除了在 Fiddler 中测试客户端证书外,我还在 Chrome 中验证了它。
在尝试这些选项的过程中,有一次它开始工作了。然后我开始取消更改以查看是什么导致它起作用。它继续工作。然后我尝试从受信任的根中删除证书以验证这是必需的并且它停止工作,现在我无法让它恢复工作,即使我将证书放回受信任的根中。现在 Chrome 甚至不会像以前那样提示我输入证书,它在 Chrome 中失败,但在 Fiddler 中仍然有效。我一定缺少一些神奇的配置。
我也尝试在绑定中启用 "Negotiate Client Certificate",但 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
这是我使用的客户端证书:
我对问题出在哪里感到困惑。我正在为任何可以帮助我解决这个问题的人提供赏金。
确保 HttpClient 可以访问完整的客户端证书(包括私钥)。
您正在使用文件 "ClientCertificate.cer" 调用 GetCert,这导致假设不包含私钥 - 应该是 windows 中的 pfx 文件。
从 windows 证书库访问证书并使用指纹搜索它可能会更好。
复制指纹时注意:在证书管理中查看时有一些不可打印的字符(将字符串复制到notepad++并检查显示字符串的长度)。
看了源码我也觉得私钥肯定有问题
它实际上是在检查通过的证书是否为X509Certificate2类型以及是否具有私钥。
如果找不到私钥,它会尝试在 CurrentUser 存储中查找证书,然后在 LocalMachine 存储中查找。如果找到证书,它会检查私钥是否存在。
(参见 source code 来自 class SecureChannnel,方法 EnsurePrivateKey)
因此,根据您导入的文件(.cer - 不带私钥或 .pfx - 带私钥)以及在哪个商店可能找不到正确的文件,并且 Request.ClientCertificate 不会被填充。
您可以激活 Network Tracing 来尝试调试它。它会给你这样的输出:
- 正在尝试在证书存储中找到匹配的证书
- 在 LocalMachine 存储或 CurrentUser 存储中找不到证书。
跟踪帮助我找到了问题所在(感谢 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 solution for me was not the one marked as an answer since my certificate was in the trusted root. The answer is the one under it 是您更新注册表的地方。我刚刚将值添加到注册表项。
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL
值名称:SendTrustedIssuerList 值类型:REG_DWORD 值数据:0(假)
将此值添加到注册表后,它开始在我的 Windows 7 机器上运行。这似乎是一个 Windows 7 问题。
我实际上有一个类似的问题,我们必须有许多受信任的根证书。我们新安装的网络服务器有超过一百个。我们的根以字母 Z 开头,所以它在列表的末尾结束。
问题是 IIS 只向客户端发送了前二十多个受信任的根并截断了其余的,包括我们的。那是几年前的事了,记不起工具的名字了……它是 IIS 管理套件的一部分,但 Fiddler 应该也是如此。意识到错误后,我们删除了很多我们不需要的可信根。这是通过反复试验完成的,所以请小心删除。
清理后一切顺利。
更新:
来自微软的示例:
原创
这就是我如何让客户端认证正常工作并检查特定根 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();
}
}
}
我最近遇到了类似的问题,按照 Fabian 的建议,我找到了解决方案。
事实证明,对于客户端证书,您必须确保两件事:
私钥实际上是作为证书的一部分导出的。
应用程序池标识运行 应用可以访问所述私钥。
在我们的案例中,我必须:
- 将 pfx 文件导入本地服务器存储,同时选中导出复选框以确保私钥已发送出去。
- 使用 MMC 控制台,授予服务帐户访问证书私钥的权限。
其他答案中解释的受信任的根问题是有效的,只是不是我们的问题。
我正在尝试使用客户端证书通过 Web API 对设备进行身份验证和授权,并开发了一个简单的概念证明来解决潜在解决方案的问题。我正在 运行 解决 Web 应用程序未收到客户端证书的问题。有很多人报告了这个问题,including in this Q&A,但其中 none 有答案。我希望提供更多细节来解决这个问题,并希望得到我的问题的答案。我对其他解决方案持开放态度。主要要求是用 C# 编写的独立进程可以调用 Web API 并使用客户端证书进行身份验证。
此 POC 中的 Web API 非常简单,只有 returns 一个值。它使用一个属性来验证是否使用了 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 设置。
这是发送带有客户端证书的请求的客户端代码。这是一个控制台应用程序。
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,原因短语为“需要客户端证书”,表明它正在进入我的 RequireHttpsAttribute 并且没有找到任何客户端证书. 运行 通过调试器,我已验证证书正在加载并添加到 WebRequestHandler。证书将导出到正在加载的 CER 文件中。带有私钥的完整证书位于 Web 应用程序服务器的本地计算机的个人和受信任根存储中。对于此测试,客户端和 Web 应用程序 运行 在同一台机器上。
我可以使用 Fiddler 调用此 Web API 方法,附加相同的客户端证书,它工作正常。使用 Fiddler 时,它通过了 RequireHttpsAttribute 中的测试和 returns 成功状态代码 200 和 returns 预期值。
有没有人运行遇到过 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;
}
您会注意到,当您从文件中获取证书时,它 returns 是一个 X509Certificate 类型的对象,当您从证书存储中检索它时,它是 X509Certificate2 类型的对象。 X509CertificateCollection.Add 方法需要一种 X509Certificate。
更新二: 我仍在努力解决这个问题,并尝试了许多不同的选择,但无济于事。
- 我将 Web 应用程序更改为 运行 主机名而不是本地主机。
- 我将 Web 应用程序设置为需要 SSL
- 我确认证书是为客户端身份验证设置的,并且它在受信任的根目录中
- 除了在 Fiddler 中测试客户端证书外,我还在 Chrome 中验证了它。
在尝试这些选项的过程中,有一次它开始工作了。然后我开始取消更改以查看是什么导致它起作用。它继续工作。然后我尝试从受信任的根中删除证书以验证这是必需的并且它停止工作,现在我无法让它恢复工作,即使我将证书放回受信任的根中。现在 Chrome 甚至不会像以前那样提示我输入证书,它在 Chrome 中失败,但在 Fiddler 中仍然有效。我一定缺少一些神奇的配置。
我也尝试在绑定中启用 "Negotiate Client Certificate",但 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
这是我使用的客户端证书:
我对问题出在哪里感到困惑。我正在为任何可以帮助我解决这个问题的人提供赏金。
确保 HttpClient 可以访问完整的客户端证书(包括私钥)。
您正在使用文件 "ClientCertificate.cer" 调用 GetCert,这导致假设不包含私钥 - 应该是 windows 中的 pfx 文件。 从 windows 证书库访问证书并使用指纹搜索它可能会更好。
复制指纹时注意:在证书管理中查看时有一些不可打印的字符(将字符串复制到notepad++并检查显示字符串的长度)。
看了源码我也觉得私钥肯定有问题
它实际上是在检查通过的证书是否为X509Certificate2类型以及是否具有私钥。
如果找不到私钥,它会尝试在 CurrentUser 存储中查找证书,然后在 LocalMachine 存储中查找。如果找到证书,它会检查私钥是否存在。
(参见 source code 来自 class SecureChannnel,方法 EnsurePrivateKey)
因此,根据您导入的文件(.cer - 不带私钥或 .pfx - 带私钥)以及在哪个商店可能找不到正确的文件,并且 Request.ClientCertificate 不会被填充。
您可以激活 Network Tracing 来尝试调试它。它会给你这样的输出:
- 正在尝试在证书存储中找到匹配的证书
- 在 LocalMachine 存储或 CurrentUser 存储中找不到证书。
跟踪帮助我找到了问题所在(感谢 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 solution for me was not the one marked as an answer since my certificate was in the trusted root. The answer is the one under it 是您更新注册表的地方。我刚刚将值添加到注册表项。
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL
值名称:SendTrustedIssuerList 值类型:REG_DWORD 值数据:0(假)
将此值添加到注册表后,它开始在我的 Windows 7 机器上运行。这似乎是一个 Windows 7 问题。
我实际上有一个类似的问题,我们必须有许多受信任的根证书。我们新安装的网络服务器有超过一百个。我们的根以字母 Z 开头,所以它在列表的末尾结束。
问题是 IIS 只向客户端发送了前二十多个受信任的根并截断了其余的,包括我们的。那是几年前的事了,记不起工具的名字了……它是 IIS 管理套件的一部分,但 Fiddler 应该也是如此。意识到错误后,我们删除了很多我们不需要的可信根。这是通过反复试验完成的,所以请小心删除。
清理后一切顺利。
更新:
来自微软的示例:
原创
这就是我如何让客户端认证正常工作并检查特定根 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();
}
}
}
我最近遇到了类似的问题,按照 Fabian 的建议,我找到了解决方案。 事实证明,对于客户端证书,您必须确保两件事:
私钥实际上是作为证书的一部分导出的。
应用程序池标识运行 应用可以访问所述私钥。
在我们的案例中,我必须:
- 将 pfx 文件导入本地服务器存储,同时选中导出复选框以确保私钥已发送出去。
- 使用 MMC 控制台,授予服务帐户访问证书私钥的权限。
其他答案中解释的受信任的根问题是有效的,只是不是我们的问题。