WebApi HttpClient没有发送客户端证书。

36

我正在尝试使用客户端证书保护我的RESTful WebApi服务的SSL和客户端身份验证。

为了测试,我生成了一个自签名证书并将其放置在本地计算机的受信任根证书颁发机构文件夹中,并生成了“服务器”和“客户端”证书。与服务器的标准https连接正常工作。

但是,当我使用测试客户端连接并提供客户端证书时,服务器中用于验证证书的代码从未被调用,并返回403禁止状态。

这意味着服务器在到达我的验证代码之前就已经拒绝了我的证书。然而,如果我启动Fiddler,它知道需要客户端证书,并要求我提供到“My Documents\Fiddler2”的客户端证书。我给它与我在测试客户端中使用的相同客户端证书,我的服务器现在可以工作并接收到我期望的客户端证书!这意味着WebApi客户端没有正确发送证书,我的客户端代码与我找到的其他示例基本相同。

    static async Task RunAsync()
    {
        try
        {
            var handler = new WebRequestHandler();
            handler.ClientCertificateOptions = ClientCertificateOption.Manual;
            handler.ClientCertificates.Add(GetClientCert());
            handler.ServerCertificateValidationCallback += Validate;
            handler.UseProxy = false;

            using (var client = new HttpClient(handler))
            {
                client.BaseAddress = new Uri("https://hostname:10001/");

                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));

                var response = await client.GetAsync("api/system/");
                var str = await response.Content.ReadAsStringAsync();

                Console.WriteLine(str);
            }
        } catch(Exception ex)
        {
            Console.Write(ex.Message);
        }
    }

有什么想法为什么Fiddler可以工作而我的测试客户端却不行?

编辑:这是获取客户端证书的代码:

private static X509Certificate GetClientCert()
    {            
        X509Store store = null;
        try
        {
            store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
            store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);

            var certs = store.Certificates.Find(X509FindType.FindBySubjectName, "Integration Client Certificate", true);

            if (certs.Count == 1)
            {
                var cert = certs[0];
                return cert;
            }
        }
        finally
        {
            if (store != null) 
                store.Close();
        }

        return null;
    }

虽然测试代码没有处理 null 证书,但我正在调试以确保找到正确的证书。


你检查过 GetClientCert 返回的证书是否符合你的预期吗? - csgero
1
GetClientCert的功能是从本地个人存储中查找客户端证书,这正是我提供给Fiddler的同一证书。 但即使它是不同的证书,我也希望服务器验证代码得到调用。 - Tronneh
取决于服务的托管方式。例如,IIS会进行证书验证,并在不调用您的代码的情况下返回403。 - csgero
我正在自托管服务,因为它嵌入在Windows服务中。 然而,什么标准会导致IIS返回403呢?我认为这个标准应该是普遍适用的,但是当Fiddler参与时,同样的证书可以使用,所以我认为HttpClient没有正确地发送证书。 - Tronneh
2
从此链接:https://dev59.com/YGIk5IYBdhLWcg3wl_PE : 仅当它与服务器信任的根之一具有信任链时,客户端才会使用客户端证书(来自WebRequestHandler.ClientCertificates集合)。 我猜测您的证书没有受到信任(自签名)。 - Khanh TO
显示剩余7条评论
3个回答

3

有两种类型的证书。第一种是公共的.cer文件,由服务器所有者发送给您。这个文件只是一长串字符。第二种是密钥库证书,这是您创建的自签名证书,并将cer文件发送给您调用的服务器,他们会安装它。根据您的安全级别,您可能需要将其中一个或两个添加到客户端(在您的情况下是处理程序)。我只看到过一个服务器使用密钥库证书,那里的安全非常高。以下代码从bin/deployed文件夹获取这两个证书:

#region certificate Add
                // KeyStore is our self signed cert
                // TrustStore is cer file sent to you.

                // Get the path where the cert files are stored (this should handle running in debug mode in Visual Studio and deployed code) -- Not tested with deployed code
                string executableLocation = Path.GetDirectoryName(AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory);

                #region Add the TrustStore certificate

                // Get the cer file location
                string pfxLocation = executableLocation + "\\Certificates\\TheirCertificate.cer";

                // Add the certificate
                X509Certificate2 theirCert = new X509Certificate2();
                theirCert.Import(pfxLocation, "Password", X509KeyStorageFlags.DefaultKeySet);
                handler.ClientCertificates.Add(theirCert);
                #endregion

                #region Add the KeyStore 
                // Get the location
                pfxLocation = executableLocation + "\\Certificates\\YourCert.pfx";

                // Add the Certificate
                X509Certificate2 YourCert = new X509Certificate2();
                YourCert.Import(pfxLocation, "PASSWORD", X509KeyStorageFlags.DefaultKeySet);
                handler.ClientCertificates.Add(YourCert);
                #endregion

                #endregion

此外,您需要处理证书错误(注意:这很糟糕 - 它表示所有证书问题都可以)。您应该更改此代码以处理特定的证书问题,如名称不匹配。我将在我的列表上进行处理。有许多例子可以参考。

在您的方法顶部放置此代码:

// Ignore Certificate errors  need to fix to only handle 
ServicePointManager.ServerCertificateValidationCallback = MyCertHandler;

在您的类中的某个方法

private bool MyCertHandler(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors error)
    {
        // Ignore errors
        return true;
    }

2
在你的代码中,你使用了store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
客户端证书不会从LocalMachine中获取,你应该使用StoreLocation.CurrentUser
检查MMC -> 文件 -> 添加或删除插件 -> 证书 -> 我的用户帐户,你将看到Fiddler使用的证书。如果你将它从我的用户帐户中删除,并且只在计算机帐户中导入它,你将发现Fiddler也无法找到它。
另外需要注意的是,在查找证书时,你还必须考虑文化因素。
示例:
var certificateSerialNumber= "‎83 c6 62 0a 73 c7 b1 aa 41 06 a3 ce 62 83 ae 25".ToUpper().Replace(" ", string.Empty);

//0 certs
var certs = store.Certificates.Find(X509FindType.FindBySerialNumber, certificateSerialNumber, true);

//null
var cert = store.Certificates.Cast<X509Certificate>().FirstOrDefault(x => x.GetSerialNumberString() == certificateSerialNumber);

//1 cert
var cert1 = store.Certificates.Cast<X509Certificate>().FirstOrDefault(x =>
                x.GetSerialNumberString().Equals(certificateSerialNumber, StringComparison.InvariantCultureIgnoreCase));

9
如果您投反对票,请说明原因。否则就没有改进答案的机会了。 - Ogglas
为什么证书无法从本地计算机存储中获取?这是非用户特定证书应存储的位置。 - Suncat2000

1
 try this.

证书应该在当前用户存储中。 或者给予完全权限并从文件中读取,因为它是一个控制台应用程序。
// Load the client certificate from a file.
X509Certificate x509 = X509Certificate.CreateFromCertFile(@"c:\user.cer");

从用户存储中读取。
 private static X509Certificate2 GetClientCertificate()
    {
        X509Store userCaStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
        try
        {
            userCaStore.Open(OpenFlags.ReadOnly);
            X509Certificate2Collection certificatesInStore = userCaStore.Certificates;
            X509Certificate2Collection findResult = certificatesInStore.Find(X509FindType.FindBySubjectName, "localtestclientcert", true);
            X509Certificate2 clientCertificate = null;
            if (findResult.Count == 1)
            {
                clientCertificate = findResult[0];
            }
            else
            {
                throw new Exception("Unable to locate the correct client certificate.");
            }
            return clientCertificate;
        }
        catch
        {
            throw;
        }
        finally
        {
            userCaStore.Close();
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接