UWP 应用程序 HttpClient HTTPS 客户端证书问题
UWP app HttpClient HTTPS client certificate problems
我正在用 C# 编写一个 UWP 应用程序,它最终用于物联网,但现在我只在本地调试。我正在使用 Windows.Web.Http.HttpClient
连接到我也编写的自托管 WCF REST Web 服务,并在同一台机器上将 运行 作为控制台应用程序进行测试。该服务需要使用证书进行相互身份验证,因此我有 CA 证书、服务证书和客户端证书。
我的 UWP 代码是这样工作的:
- 检查应用程序证书商店以安装客户端证书和 CA 证书。
- 如果没有,请分别从 PFX 文件和 CER 文件安装。
- 将
Certificate
附加到 HttpBaseProtocolFilter
并将过滤器添加到 HttpClient
- 调用
HttpClient.PostAsync
调用 PostAsync
后出现以下错误:An Error Occurred in the Secure Channel Support
。经过大量在线搜索,根据常识,我很确定 HttpClient
是因为建立相互验证的 SSL 连接时出现问题而呕吐。但是根据我的故障排除,我不知道为什么。
为了进一步排除故障,我使用 System.Net.Http.HttpClient
编写了一个普通的旧控制台应用程序,将客户端证书附加到请求中,一切正常。遗憾的是,UWP 不完全支持 System.Net
。我也试过不将证书附加到 UWP HttpClient
并且应用程序提示我 UI 到 select 已安装的证书。我 select 正确的证书并且仍然得到相同的异常(这至少让我知道证书已正确安装并从应用程序的角度正确地使用 CA 进行验证)。此外,我从浏览器点击网络服务上的 GET,select 出现提示时客户端证书,并且能够下载文件。
我已经尝试过使用 Fiddler,并且我认为由于它代理流量的方式,它似乎可以进一步工作,除了我的 Web 服务拒绝请求为禁止访问(大概是因为 Fiddler 不包括正确的请求中的客户端证书)。我还没有使用 Wireshark,因为很难让 Wireshark 在 Windows.
上使用本地主机工作
我的下一步是开始将 Web 服务更改为不需要客户端身份验证,看看是否是问题所在。
两个问题:为什么 Windows.Web.Http.HttClient
在这种情况下不起作用?而且,不太重要的是,关于好的 HTTP 监控工具有什么建议可以帮助我进一步调试吗?
这个 MSDN post 证明有答案。似乎是对 MS 部分的疏忽,需要事先对 API 进行单独的、无意义的调用。嗯。
文章摘录:
但是,安全子系统在允许访问存储在共享用户证书库中的证书私钥之前需要用户确认。使事情复杂化的是,如果在代码中指定了客户端证书,则较低级别的网络功能会假定应用程序已经处理了这一点,并且不会提示用户进行确认。
如果您查看与证书相关的 Windows 运行时 类,您将找不到任何方法来明确请求访问证书私钥,那么应用程序开发人员该怎么办?
解决方案是将选定的证书用于 'Sign' 一些小数据。当应用程序调用 CryptographicEngine.SignAsync 时,底层代码请求访问私钥以进行签名,此时会询问用户是否允许应用程序访问证书私钥。请注意,您必须调用此函数的 'Async' 版本,因为该函数的同步版本:Sign 使用阻止显示确认对话框的选项。
例如:
public static async Task<bool> VerifyCertificateKeyAccess(Certificate selectedCertificate)
{
bool VerifyResult = false; // default to access failure
CryptographicKey keyPair = await PersistedKeyProvider.OpenKeyPairFromCertificateAsync(
selectedCertificate, HashAlgorithmNames.Sha1,
CryptographicPadding.RsaPkcs1V15);
String buffer = "Data to sign";
IBuffer Data = CryptographicBuffer.ConvertStringToBinary(buffer, BinaryStringEncoding.Utf16BE);
try
{
//sign the data by using the key
IBuffer Signed = await CryptographicEngine.SignAsync(keyPair, Data);
VerifyResult = CryptographicEngine.VerifySignature(keyPair, Data, Signed);
}
catch (Exception exp)
{
System.Diagnostics.Debug.WriteLine("Verification Failed. Exception Occurred : {0}", exp.Message);
// default result is false so drop through to exit.
}
return VerifyResult;
}
然后您可以修改前面的代码示例以在使用客户端证书之前调用此函数,以确保应用程序可以访问证书私钥。
- 将证书文件添加到您的项目中
- 将证书添加到清单文件(在附件中提供文件路径)
- Ur 项目中的 Frist Service Call 用于忽略证书验证以下代码最适合登录功能。
尝试
{
var filter = new HttpBaseProtocolFilter();
filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.Expired);
filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.Untrusted);
filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.InvalidName);
filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.RevocationFailure);
filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.RevocationInformationMissing);
filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.WrongUsage);
filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.IncompleteChain);
Windows.Web.Http.HttpClient client = new Windows.Web.Http.HttpClient(filter);
TimeSpan span = new TimeSpan(0, 0, 60);
var cts = new CancellationTokenSource();
cts.CancelAfter(span);
var request = new Windows.Web.Http.HttpRequestMessage()
{
RequestUri = new Uri(App.URL + "/oauth/token"),
Method = Windows.Web.Http.HttpMethod.Post,
};
//request.Properties. = span;
string encoded = System.Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(Server_Username + ":" + Server_Password));
var values = new Dictionary<string, string>
{ { "grant_type", "password" },{ "username", Uname}, { "password", Pwd }};
var content = new HttpFormUrlEncodedContent(values);
request.Headers.Add("Authorization", "Basic " + encoded);
request.Content = content;
User root = new User();
using (Windows.Web.Http.HttpResponseMessage response = await client.SendRequestAsync(request).AsTask(cts.Token))
{
HttpStatusCode = (int)response.StatusCode;
if (HttpStatusCode == (int)HttpCode.OK)
{
using (IHttpContent content1 = response.Content)
{
var jsonString = await content1.ReadAsStringAsync();
root = JsonConvert.DeserializeObject<User>(jsonString);
App.localSettings.Values["access_token"] = root.Access_token;
App.localSettings.Values["refresh_token"] = root.Refresh_token;
App.localSettings.Values["expires_in"] = root.Expires_in;
var json = JsonConvert.SerializeObject(root.Locations);
App.localSettings.Values["LocationList"] = json;
App.localSettings.Values["LoginUser"] = Uname;
}
}
}
}
catch (Exception ex)
{
ex.ToString();
}
我正在用 C# 编写一个 UWP 应用程序,它最终用于物联网,但现在我只在本地调试。我正在使用 Windows.Web.Http.HttpClient
连接到我也编写的自托管 WCF REST Web 服务,并在同一台机器上将 运行 作为控制台应用程序进行测试。该服务需要使用证书进行相互身份验证,因此我有 CA 证书、服务证书和客户端证书。
我的 UWP 代码是这样工作的:
- 检查应用程序证书商店以安装客户端证书和 CA 证书。
- 如果没有,请分别从 PFX 文件和 CER 文件安装。
- 将
Certificate
附加到HttpBaseProtocolFilter
并将过滤器添加到HttpClient
- 调用
HttpClient.PostAsync
调用 PostAsync
后出现以下错误:An Error Occurred in the Secure Channel Support
。经过大量在线搜索,根据常识,我很确定 HttpClient
是因为建立相互验证的 SSL 连接时出现问题而呕吐。但是根据我的故障排除,我不知道为什么。
为了进一步排除故障,我使用 System.Net.Http.HttpClient
编写了一个普通的旧控制台应用程序,将客户端证书附加到请求中,一切正常。遗憾的是,UWP 不完全支持 System.Net
。我也试过不将证书附加到 UWP HttpClient
并且应用程序提示我 UI 到 select 已安装的证书。我 select 正确的证书并且仍然得到相同的异常(这至少让我知道证书已正确安装并从应用程序的角度正确地使用 CA 进行验证)。此外,我从浏览器点击网络服务上的 GET,select 出现提示时客户端证书,并且能够下载文件。
我已经尝试过使用 Fiddler,并且我认为由于它代理流量的方式,它似乎可以进一步工作,除了我的 Web 服务拒绝请求为禁止访问(大概是因为 Fiddler 不包括正确的请求中的客户端证书)。我还没有使用 Wireshark,因为很难让 Wireshark 在 Windows.
上使用本地主机工作我的下一步是开始将 Web 服务更改为不需要客户端身份验证,看看是否是问题所在。
两个问题:为什么 Windows.Web.Http.HttClient
在这种情况下不起作用?而且,不太重要的是,关于好的 HTTP 监控工具有什么建议可以帮助我进一步调试吗?
这个 MSDN post 证明有答案。似乎是对 MS 部分的疏忽,需要事先对 API 进行单独的、无意义的调用。嗯。
文章摘录:
但是,安全子系统在允许访问存储在共享用户证书库中的证书私钥之前需要用户确认。使事情复杂化的是,如果在代码中指定了客户端证书,则较低级别的网络功能会假定应用程序已经处理了这一点,并且不会提示用户进行确认。
如果您查看与证书相关的 Windows 运行时 类,您将找不到任何方法来明确请求访问证书私钥,那么应用程序开发人员该怎么办?
解决方案是将选定的证书用于 'Sign' 一些小数据。当应用程序调用 CryptographicEngine.SignAsync 时,底层代码请求访问私钥以进行签名,此时会询问用户是否允许应用程序访问证书私钥。请注意,您必须调用此函数的 'Async' 版本,因为该函数的同步版本:Sign 使用阻止显示确认对话框的选项。
例如:
public static async Task<bool> VerifyCertificateKeyAccess(Certificate selectedCertificate)
{
bool VerifyResult = false; // default to access failure
CryptographicKey keyPair = await PersistedKeyProvider.OpenKeyPairFromCertificateAsync(
selectedCertificate, HashAlgorithmNames.Sha1,
CryptographicPadding.RsaPkcs1V15);
String buffer = "Data to sign";
IBuffer Data = CryptographicBuffer.ConvertStringToBinary(buffer, BinaryStringEncoding.Utf16BE);
try
{
//sign the data by using the key
IBuffer Signed = await CryptographicEngine.SignAsync(keyPair, Data);
VerifyResult = CryptographicEngine.VerifySignature(keyPair, Data, Signed);
}
catch (Exception exp)
{
System.Diagnostics.Debug.WriteLine("Verification Failed. Exception Occurred : {0}", exp.Message);
// default result is false so drop through to exit.
}
return VerifyResult;
}
然后您可以修改前面的代码示例以在使用客户端证书之前调用此函数,以确保应用程序可以访问证书私钥。
- 将证书文件添加到您的项目中
- 将证书添加到清单文件(在附件中提供文件路径)
- Ur 项目中的 Frist Service Call 用于忽略证书验证以下代码最适合登录功能。
尝试 {
var filter = new HttpBaseProtocolFilter();
filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.Expired);
filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.Untrusted);
filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.InvalidName);
filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.RevocationFailure);
filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.RevocationInformationMissing);
filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.WrongUsage);
filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.IncompleteChain);
Windows.Web.Http.HttpClient client = new Windows.Web.Http.HttpClient(filter);
TimeSpan span = new TimeSpan(0, 0, 60);
var cts = new CancellationTokenSource();
cts.CancelAfter(span);
var request = new Windows.Web.Http.HttpRequestMessage()
{
RequestUri = new Uri(App.URL + "/oauth/token"),
Method = Windows.Web.Http.HttpMethod.Post,
};
//request.Properties. = span;
string encoded = System.Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(Server_Username + ":" + Server_Password));
var values = new Dictionary<string, string>
{ { "grant_type", "password" },{ "username", Uname}, { "password", Pwd }};
var content = new HttpFormUrlEncodedContent(values);
request.Headers.Add("Authorization", "Basic " + encoded);
request.Content = content;
User root = new User();
using (Windows.Web.Http.HttpResponseMessage response = await client.SendRequestAsync(request).AsTask(cts.Token))
{
HttpStatusCode = (int)response.StatusCode;
if (HttpStatusCode == (int)HttpCode.OK)
{
using (IHttpContent content1 = response.Content)
{
var jsonString = await content1.ReadAsStringAsync();
root = JsonConvert.DeserializeObject<User>(jsonString);
App.localSettings.Values["access_token"] = root.Access_token;
App.localSettings.Values["refresh_token"] = root.Refresh_token;
App.localSettings.Values["expires_in"] = root.Expires_in;
var json = JsonConvert.SerializeObject(root.Locations);
App.localSettings.Values["LocationList"] = json;
App.localSettings.Values["LoginUser"] = Uname;
}
}
}
}
catch (Exception ex)
{
ex.ToString();
}