此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 Forbidden的返回结果,并附带着“需要客户端证书”的原因短语,表明它正在进入我的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中仍然有效。我肯定是遗漏了一些神奇的配置。
我也尝试在绑定中启用“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
这是我正在使用的客户端证书:
我对问题感到困惑。我正在为任何能帮助我解决这个问题的人提供赏金。