Issue
I got a .p12 certificate file with 3 certificates in it. 2 of them are CA certificates.
If I use curl (7.70 on Win10) I can do: curl -s -S -i --cert Swish_Merchant_TestCertificate_1234679304.p12:swish --cert-type p12 --tlsv1.2 --header "Content-Type:application/json" https://mss.cpc.getswish.net/swish-cpcapi/api/v1/paymentrequests --data-binary @jsondata.json
Curl will use the CA certificates in the p12 file when connecting to the server.
On the other hand, if I try to do something similar in .net core (3.1) it fails with the error message "The message received was unexpected or badly formatted."
var handler = new HttpClientHandler();
var certs = new X509Certificate2Collection();
certs.Import(@"Swish_Merchant_TestCertificate_1234679304.p12", "swish", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
foreach (var cert in certs)
{
handler.ClientCertificates.Add(cert);
}
var client = new HttpClient(handler);
var url = "https://mss.cpc.getswish.net/swish-cpcapi/api/v1/paymentrequests";
var request = new HttpRequestMessage()
{
RequestUri = new Uri(url),
Method = HttpMethod.Post,
};
request.Content = new StringContent(System.IO.File.ReadAllText(@"jsondata.json"), Encoding.UTF8, "application/json");
request.Headers.Add("accept", "*/*");
var response = await client.SendAsync(request);
Using Wireshark I saw that curl sends all three certificates from the p12 file whereas .net core only sends one. See images below.
If I install the CA certificates into "Personal certificate" for "Current User" then .net core also sends all three certificates and it works.
Question: Do I have to install the CA certificates (into the certificate store) when using .net core or is there a way to make it behave just like curl which uses the certificates from the p12 file?
Solution
Short answer: no*.
Wordier intro: SslStream
picks one certificate out of the ClientCertificates
collection, using data that (was historically, but no longer generally) is sent by the TLS server about appropriate roots (and if none is applicable then it picks the first thing where HasPrivateKey
is true). During the selection process each candidate certificate is checked in isolation, and it asks the system to resolve the chain. On Windows, the selected certificate is then sent down to the system libraries for "we're doing TLS now", which (IIRC) is where the limitations come from. (macOS and Linux builds of .NET Core just try to maintain behavioral parity)
Once the certificate is selected, there's one last chain-walk to determine what certificates to include in the handshake, it's done without the context of anything else from the ClientCertificates
collection.
If you know that your collection represents one chain, your best answer is to import the CA elements into your user CertificateAuthority store. That store does not impart any trust to the CA certificates, it's really just a cache that's used when building chains.
Also, you don't want PersistKeySet, and probably don't want MachineKeySet: What is the rationale for all the different X509KeyStorageFlags?
var handler = new HttpClientHandler();
using (X509Store store = new X509Store(StoreName.CertificateAuthority, StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadWrite);
var certs = new X509Certificate2Collection();
certs.Import(@"Swish_Merchant_TestCertificate_1234679304.p12", "swish", X509KeyStorageFlags.DefaultKeySet);
foreach (X509Certificate2 cert in certs)
{
if (cert.HasPrivateKey)
{
handler.ClientCertificates.Add(cert);
}
else
{
store.Add(cert);
}
}
}
var client = new HttpClient(handler);
...
* If your system already has the CA chain imported, it'll work. Alternatively, if the CA chain uses the Authority Information Access extension to publish a downloadable copy of the CA cert, the chain engine will find it, and everything will work.
Answered By - bartonjs Answer Checked By - Mary Flores (WPSolving Volunteer)