I'm trying to connect to a server that uses TLS with client certificate authentication. Below is a code snippet:
async Task TestClientCertAuth()
{
int iWinInetError = 0;
Uri theUri = new Uri("http://xxx-xxx");
try
{
using (HttpBaseProtocolFilter baseProtocolFilter = new HttpBaseProtocolFilter())
{
// Task<Certificate> GetClientCertificate() displays a UI with all available
// certificates with and returns the user selecter certificate. An
// oversimplified implementation is included for completeness.
baseProtocolFilter.ClientCertificate = await GetClientCertificate();
baseProtocolFilter.AllowAutoRedirect = false;
baseProtocolFilter.AllowUI = false;
using (HttpClient httpClient = new HttpClient(baseProtocolFilter))
using (HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Get, theUri))
using (HttpResponseMessage httpResponse = await httpClient.SendRequestAsync(httpRequest))
{
httpResponse.EnsureSuccessStatusCode();
// Further HTTP calls using httpClient based on app logic.
}
}
}
catch (Exception ex)
{
iWinInetError = ex.HResult & 0xFFFF;
LogMessage(ex.ToString() + " Error code: " + iWinInetError);
throw;
}
}
// Task<Certificate> GetClientCertificate() displays a UI with all available
// certificates with and returns the user selecter certificate. An
// oversimplified implementation is included for completeness.
private async Task<Certificate> GetClientCertificate()
{
IReadOnlyList<Certificate> certList = await CertificateStores.FindAllAsync();
Certificate clientCert = null;
// Always choose first enumerated certificate. Works so long as there is only one cert
// installed and it's the right one.
if ((null != certList) && (certList.Count > 0))
{
clientCert = certList.First();
}
return clientCert;
}
The SendRequestAsync call throws an exception with HRESULT 0x80072F7D - I believe that means ERROR_INTERNET_SECURITY_CHANNEL_ERROR. There are no problems with the server certificate trust. The client certificate is installed in the app local store and I am abe to retrieve it using CertificateStores.FindAllAsync. Looking at the SSL traces and I can see the client certificate is not being sent.
The the above issue does not occur if HttpBaseProtocolFilter.AllowUI is set to true. In this case, the SendRequestAsync call causes a UI to be displayed asking for consent to use the client certificate. Once 'Allow' is selected on this dialog, I can see the client cert and cert verify messages being sent in the traces and the connection is established successfully.
Question: The app code already handles certificate selection by the user. I would like to know whether there is any way to specify consent to use the client certificate programmatically. Because enabling AllowUI causes other side effects - say for example if the server retruns a 401 HTTP code with a WWW-Authenticate: Basic header, the base protoctol filter pops up it's own UI to accept the user credentials without giving a chance for the caller to handle it. Would like to avoid both of the above UIs since I have already selected the client certificate and obtained credentials from the user with own UIs. Thanks
This Microsoft Blog Entry provides information on why this error occurs when AllowUI is false and provides a workaround. The certificate consent UI cannot be bypassed in any case, so that is something the EU has to go through. It also appears that the behavior on Windows Phone is different. Tried this solution and it seems to work on desktop and surface. The general idea is to 'prime' the certificate for use by the lower level APIs in the current app session by attempting to access the private key. In this case we just try to sign some dummy data. Once the EU grants access to the certificate, TLS session establishment goes through successfully. Need to check how this behaves on Windows Phone though.
private async Task<bool> RequestCertificateAccess(Certificate cert)
{
bool signOK = false;
try
{
IBuffer data = CryptographicBuffer.ConvertStringToBinary("ABCDEFGHIJKLMNOPQRSTUVWXYZ012345656789",
BinaryStringEncoding.Utf8);
CryptographicKey key = await PersistedKeyProvider.OpenKeyPairFromCertificateAsync(cert,
HashAlgorithmNames.Sha1, CryptographicPadding.RsaPkcs1V15);
IBuffer sign = await CryptographicEngine.SignAsync(key, data);
signOK = CryptographicEngine.VerifySignature(key, data, sign);
}
catch (Exception ex)
{
LogMessage(ex.ToString(), "Certificate access denied or sign/verify failure.");
signOK = false;
}
return signOK;
}
RequestClientCertificateAccess can be called just before setting the client certificate on the base protocol filter.
@Tomas Karban, thanks for the response. I have not used sharedUserCertificates, so any certificate that I can enumerate has to be in the apps' certificate store if I understand correctly. The link I shared might be of some help for your case if you've not seen it already.
I would argue that you don't have the certificate stored in the app's own certificate store. If you set HttpBaseProtocolFilter.AllowUI = true
and confirm the dialog, the app gets permission to use the private key from user store. Without the UI confirmation the app can only use private keys from its own certificate store.
The situation on Windows 10 Mobile is even worse -- as far as I can tell you cannot set HttpBaseProtocolFilter.AllowUI
to true (see my question Cannot set HttpBaseProtocolFilter.AllowUI to true on Windows 10 Mobile). That leaves the only option to use app's own certificate store.
来源:https://stackoverflow.com/questions/37019697/tls-client-certificate-authentication-on-uwp-windows-store-app