Client certificate with HttpClient in c# - c#

Want to send data with client certificate (.p12 or .pfx) from Windows application to Server machine, Windows application developed in .Net Framework 4.6, OS is windows 10.
When hit from postman with client certificate (.p12 or .pfx) [Loaded in setting tab -> Add client certificate - > put hostname, select pfx file, put password], all working properly (client certificate send to server machine), but issue from below c# code,
X509Certificate2 certificate = new X509Certificate2(certificateFilePath, "password");
WebRequestHandler handler = new WebRequestHandler();
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.ServerCertificateValidationCallback = (a, b, c, d) => { return true; };
handler.ClientCertificates.Add(certificate);
HttpClient request = new HttpClient(handler);
request.DefaultRequestHeaders.Add("User-Agent", UserAgent);
// added other headers and data
var result = request.PostAsync(url, byteContent).Result;
string resultContent = result.Content.ReadAsStringAsync().Result;
Also cross check with fiddler for Postman hit and c# hit.
When server not receive client certificate, it return 403 error.

The HttpStatus code 403 can caused by TLS issues due to not invoking API with the expected server TLS version. You can check the outcome of resultContent from the code line string resultContent = result.Content.ReadAsStringAsync().Result;
To set the SslProtocol, either you can set at Handler like (if you are targeting .Net 4.7 onwards or .Net core)
WebRequestHandler handler = new WebRequestHandler();
handler.SslProtocols = SslProtocols.Tls13 | SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls;
or at application level using ServicePointManager in Startup method (or .Net framework version before 4.7)
System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls13 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
A side note suggestion - I would suggest you to use pure async/await pattern. Don't use the sync call on the IO request by invoking .Result.

I assume that your handler has no access to the private key for authentication.
This may be caused due to line 1 in your example, in which you import the certificate with the default key storage flags. Of course this is just a guess and I don't have your certificate to check this, but you can verify that by calling
// Sample for RSA, use DSA if required
var privateKeyParams = ((RSA)certificate.PrivateKey).ExportParameters(true);
which will cause a CryptographicException ("Not supported" or similar) if the key parameters cannot be accessed.
Please try the following instead for loading the certificate:
X509Certificate2 certificate = new X509Certificate2(
certificateFilePath, "password",
X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.UserKeySet);
Just a further sidenote; If you use this in production code, be sure to extend your server certificate validation callback (your 4th line) to actually validate the server certificate. See X509Chain.Build, which also allows you to modify the validation options to your needs (what the path validation actually does can be read in RFC5280).

Related

In the VaultSharp library, what's the equivalent of setting the VAULT_CACERT environment variable?

I'm getting the error An error occurred while sending the request when using the VaultSharp library in C# to request secrets from a Vault service. I can get the access token I need from the command line, so I know the Vault address and my personal Vault token work.
The CLI relies on the environment variables VAULT_ADDR, VAULT_TOKEN and VAULT_CACERT. I see VaultSharp creates the VaultClientSettings object using the first two: address and token information--but where in VaultSharp can I specify the CA certificate path?
Here's the code I'm using, copied from https://github.com/rajanadar/VaultSharp/blob/master/README.md:
string vaultToken = Environment.GetEnvironmentVariable("VAULT_TOKEN");
VaultSharp.V1.AuthMethods.IAuthMethodInfo authMethod = new VaultSharp.V1.AuthMethods.Token.TokenAuthMethodInfo(vaultToken);
string vaultAddress = Environment.GetEnvironmentVariable("VAULT_ADDR");
var vaultClientSettings = new VaultSharp.VaultClientSettings(vaultAddress, authMethod);
VaultSharp.VaultClient vaultClient = new VaultSharp.VaultClient(vaultClientSettings);
string vaultRoute = Properties.Settings.Default.VaultRoute;
VaultSharp.V1.Commons.Secret<VaultSharp.V1.Commons.SecretData> kv2Secret = await vaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync(vaultRoute);
It's this last statement ReadSecretAsync that throws the error.
Many thanks for your help!
There is no equivalent of VAULT_CACERT in VaultSharp. VaultSharp expects your Vault URL to have a trusted SSL Cert. If not, you will get TLS errors while establishing the handshake. And in non-prod environments, folks typically use the following snippet to solve for it.
ServicePointManager.ServerCertificateValidationCallback +=
(sender, cert, chain, sslPolicyErrors) => true; // or do specific checks

Could not create SSL/TLS secure channel while calling pages within the site

I am working on a web call that is supposed to do some checking on WSDL pages within the same site. I thought this would be simple. It works fine until we add in the certificate call and then the SSL breaks
var httpClientHandler = new HttpClientHandler
{
SslProtocols = SslProtocols.Tls12,
ServerCertificateCustomValidationCallback = (message, certificate2, chain, errors) => { return true; }
};
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection collection = store.Certificates.Find(X509FindType.FindBySubjectName, "fakename.com", false);
httpClientHandler.ClientCertificates.AddRange(collection);
HttpClient client = new HttpClient(httpClientHandler);
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
HttpResponseMessage resp = client.GetAsync(uri, HttpCompletionOption.ResponseContentRead, token).Result;
the user clicks the healthcheck link
I construct a full uri for the WSDL.
the code above runs and I read the respnse.
Unit testing shows this working fine, the difference is that I am making the call outside of the site, from that I can deduce that the certificate does get added, and a call is possible.
Has anyone else seen this happen? Is there some rule I need to observe in order to call a page from within my own site behind SSL?
I am answering my own question...
when I was running the unit tests the call was coming from outside the site and simply used the public .cer file for the client certificate.
when creating the same call from within the site I had to grab the private key file and apply the password.
var clientCertificate = new X509Certificate2(_509FilePath, "my password");
I think I would have expected a 403 error rather than the SSL issue, but hey... at least it's working now... thanks #CodeCaster for the hint in the comments

SOAP SSL connection in C# with certificate chain (CA Bundle)

I'm having difficulties trying to connect to a 3rd-party SOAP server that requires two-way SSL. On the client side I have our certificate and private key, and I also have the self-signed certificate chain provided by the service provider. What I'm essentially trying to do is the C# equivalent of this Python code:
r = requests.post(
url,
verify=(ca_path),
cert=(client_public_path, client_private_path),
headers=headers,
data=body)
Back in C# land, I've subscribed to the service and made it as far as this:
var endpoint = new EndpointAddress("https://server.endpoint.com/");
var binding = new BasicHttpBinding();
binding.Security.Mode = BasicHttpSecurityMode.Transport;
binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;
using (var service = new MyServiceReference.ServerClient(binding, endpoint))
{
X509Certificate certificate = LoadCertificate(client_public_path, client_private_path);
service.ClientCredentials.ClientCertificate.Certificate = certificate;
// how to set server CA Bundle?
var response = service.SomeMethod(new Request{...});
At a bit of a loss though trying to figure out how to set the server-side chain? I have the two certs that are in the chain file, and I can also separate them out and load them into a X509Certificate2Collection if need be, just can't seem to figure out what to do with it after that?
The only other thing I've tried is assigning a custom validator on the server certificate:
service.ClientCredentials.ServiceCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.Custom;
service.ClientCredentials.ServiceCertificate.Authentication.CustomCertificateValidator = new MyValidator();
The custom validator's Validate method never gets called thoough, which if I'm not mistaken would suggest it's a either a problem with the client cert or the binding config?

Can a .p12 file with CA certificates be used in C# without importing them to certificate store

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?
Wireshark curl:
Wireshark .net core:
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.

C# RestSharp Client Certificate getting exception "The SSL connection could not be established"

I programming in C# .Net Core 3.1 and .Net Standard library where this code is put.
I am not getting my http requests with RestSharp with a client certificate that are NOT installed on the server/computer i am not going to have access to the server/computer where this is going to be running later on..
My code:
// Create a RestSharp RestClient objhect with the base URL
var client = new RestClient(_baseAPIUrl);
// Create a request object with the path to the payment requests
var request = new RestRequest("swish-cpcapi/api/v1/paymentrequests");
// Create up a client certificate collection and import the certificate to it
X509Certificate2Collection clientCertificates = new X509Certificate2Collection();
clientCertificates.Import(_certDataBytes, _certificatePassword, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
// Add client certificate collection to the RestClient
client.ClientCertificates = clientCertificates;
//X509Certificate2 certificates = new X509Certificate2(_certDataBytes, _certificatePassword);
//client.ClientCertificates = new X509CertificateCollection() { certificates };
// Add payment request data
request.AddJsonBody(requestData);
var response = client.Post(request);
var content = response.Content;
I have done what ever i can find on internet and i have used the service test environment with there own generated certificate and generated my own in there production environment with the same result..
I have tested set TLS 1.1 or TLS 1.2 to test, they specified TLS 1.1 on there own curl examples.
Anyone got any idea?
Update 2020-01-08 - I have got information from the service tech support that they only see me sending one certificate but when i trying to debug and checking the X509Certificate2Collection i finding 3 certificate. But they saying they only see one?
IMHO, if the service you are trying to access is protected via SSL cert with expecting public key/private key, then without the cert, you cant access it. If thats the intent of the service to be protected, i think you may only do to check the service health check or at max, check if the service is accessible (without having the client cert)
But just as HTTPS, then you can try to download the cert from the browser if you can access the service URL from your browser, say, to access their meta or a simple GET or something. In chrome, you may see a lock symbol (security icon) just before to the URL. click that, and you may possibly download the public cert. You can install that on your machine and try that.
In case, if that service has a pub cert and the access doesnt need the client cert, then above option may work. and i hope you are installing the CERT and access that via your code from the account that has access to the cert. Lets say, you install the cert under the LocalSystem, then you may need administrator access from code/solution to access that cert path. Or install the cert under the current_user path.
I would Check if am able to access the service from browser as the first step.
This is, just from what i have understood from your question. again, if you can explain whether the service is a protected service based on public key/private key, you need to have the access/cert to use that service.
UPDATE:
I have tried few options on my end, by creating a demo api and a demo client to use client certs. With what i have understood from your query, i assume below may be some of your options to try out.
// --- using the cert path if you have the path (instead of the byte[])
var myCert = new X509Certificate2("<path to the client cert.cer/.pfx>", "secure-password", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
X509CertificateCollection clientCerts = new X509CertificateCollection();
clientCerts.Add(myCert);
OR
var certStore = new X509Store(StoreName.Root, StoreLocation.LocalMachine); //replace with appropriate values based on your cert configuration
certStore.Open(OpenFlags.ReadOnly);
//option 1 (ideally a client must have the cert thumbprint instead of a password
var cert = certStore.Certificates.Find(X509FindType.FindByThumbprint, "<cert Thumbprint>", false);
//option 2 (explore other options based on X509NameTypes
var cert = certStore.Certificates.OfType<X509Certificate2>()
.FirstOrDefault(cert => cert.GetNameInfo(X509NameType.DnsName, false) == "mycompany.dns.name.given in the cert");
client.ClientCertificates = new X509CertificateCollection(cert);
I recommend generate your own certificate
public static void MakeCert()
{
var ecdsa = ECDsa.Create(); // generate asymmetric key pair
var req = new CertificateRequest("cn=foobar", ecdsa, HashAlgorithmName.SHA256);
var cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(10));
// Create PFX (PKCS #12) with private key
File.WriteAllBytes("c:\\temp\\mycert.pfx", cert.Export(X509ContentType.Pfx, "P#55w0rd"));
// Create Base 64 encoded CER (public key only)
File.WriteAllText("c:\\temp\\mycert.cer",
"-----BEGIN CERTIFICATE-----\r\n"
+ Convert.ToBase64String(cert.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks)
+ "\r\n-----END CERTIFICATE-----");
}
and then add to request
var client = new RestClient(url);
client.ClientCertificates = new X509CertificateCollection();
client.ClientCertificates.Add(new X509Certificate2("c:\\temp\\mycert.cer"));

Categories

Resources