i am developing very simple desktop (win forms) application, that comunicates with server via WCF service on HTTPS protocol.
Every client that use this application, can upload client certificate (unique for each client), wich shall be used to to communication.
What exactly i am trying to achieve:
1. user will "upload" certificate to my desktop application, application take the certificate and programmatically save the certificate to windows cert store, here is my code:
[SecurityCritical]
public CertificateInfoDto GetCertificateInfoAndImportToStore(string fullPath, SecureString password)
{
if (string.IsNullOrEmpty(fullPath))
{
throw new ArgumentNullException("fullPath");
}
if (!File.Exists(fullPath))
{
throw new ArgumentException(string.Concat("No file present on ", fullPath));
}
try
{
byte[] rawBytes = this.GetCertificateContent(fullPath);
var certificate = new X509Certificate2(rawBytes, password, X509KeyStorageFlags.Exportable);
this.EnsureImport(certificate);
}
// some error handling etc, and end of method.
[SecurityCritical]
private void EnsureImport(X509Certificate2 certificate)
{
X509Store store = null;
try
{
store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite);
store.Add(certificate);
}
In my application settings file, i store only certificate serial number.
2. Now, when i have uploaded certificate to windows store, i want to use it for wcf communication, so my code looks like:
var client = new SomeWcfServiceProxy();
client.ClientCredentials.ClientCertificate.SetCertificate(
StoreLocation.CurrentUser,
StoreName.My,
X509FindType.FindBySerialNumber,
certificateSerialNumber);
And finally, here is my problem, when i try to call some method on proxy, ill get an exception:
System.ServiceModel.Security.SecurityNegotiationException: Could not establish secure channel for SSL/TLS with authority 'url (i changed this)'. ---> System.Net.WebException: The request was aborted: Could not create SSL/TLS secure channel.
But what confuses me, is that when i create proxy client like this:
var client = new SomeWcfServiceProxy();
var clientCertificate = new X509Certificate2(#"C:\U\BB\certificate.pfx", "password");
client.ClientCredentials.ClientCertificate.Certificate = clientCertificate;
Everything working like a charm!
So there is my question: I do not want to store clients certificate password anywhere, i want to once upload it via windows certificate store and then use it only from windows certificate store. Is this possible? Or i must store certificate password somewhere ( i do not know where, becouse i think it is not very secure and this certificates are very very confidential).
Thanks for any help :)
After all, i needed this:
var certificate = new X509Certificate2(rawBytes, password, X509KeyStorageFlags.UserKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
Now certificate is imported just right and can be used for communication without any problems
Related
No mather what i try, in an new .net 5 API project i cannot connect to RavenDB. It is given me an error:
This server requires client certificate for authentication, but none
was provided by the client.
The way I connect to the database:
byte[] certificateBytes = _certificateProvider.GetCertificate("CERT-NAME").Result;
string passphrase = _secretProvider.GetSecret("CERT-PASSPHRASE").Result;
X509Certificate2 certificate = new X509Certificate2(certificateBytes, passphrase);
var documentStore = new DocumentStore()
{
Urls = new[] { databaseOptions.DbUrl },
Conventions =
{
UseOptimisticConcurrency = true,
FindCollectionName = findCollectionName
},
Database = "DBNAME",
Certificate = certificate,
}.Initialize();
The certificate provider and the secret provider gets the data out of an Azure Key Vault. I validated the X509Certificate and it has the same thumbprint as I get in the admin panel of RavenDB. So that is loaded correctly. Also the certificate has the read/write rights on the requested database
But when i then do the following:
using (IAsyncDocumentSession session = documentStore.OpenAsyncSession())
{
var entity = await session.Query<EntityDTO>()
.SingleOrDefaultAsync();
}
Then i get the following error:
This server requires client certificate for authentication, but none
was provided by the client.
This while the certificate is given when initializing the document store. Anyone an idea how to continue with this as RavenCloud doesn't give more information then this?
In another project (.net core 3.1) the same code with the same certificate works. But could not find anything in the release notes of .net 5 what can cause this.
You need to load the certificate with the MachineKeySet flag.
X509Certificate2 certificate = new X509Certificate2(certificateBytes, passphrase, X509KeyStorageFlags.MachineKeySet);
This will solve the problem in Azure but your local dev machine might have a problem after this change. You need to allow full access for the user running RavenDB to the MachineKeySet folder on your local machine.
"C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys"
Please also note that if you don't dispose the certificate afterwards, the private keys pile up in that folder (a key is written to that folder every time the X509Certificate2 constructor opens the certificate).
In the RavenDB forum #iftah responded that you should set the storage flag to MachineKeySet because of the private key.
While that was not the case for me, it give me an direction to check. I was loading the certificate from Azure Key Vault using the CertificateClient and GetCertificateAsync. This only gives you the public key. So there never was an private key.
With the help of this post: KeyVault generated certificate with exportable private key. I've found out that i could use the SecretClient with GetSecretAsync. Then use Convert.FromBase64String() and that gives back also the private key.
So for the ones that have similar problems. Please verify when loading the certificate that you have the private key. My new code:
byte[] certificateBytes = Convert.FromBase64String(_secretProvider.GetSecret("CERT-NAME").Result);
X509Certificate2 certificate = new X509Certificate2(certificateBytes, string.Empty);
var documentStore = new DocumentStore()
{
Urls = new[] { databaseOptions.DbUrl },
Conventions =
{
UseOptimisticConcurrency = true,
FindCollectionName = findCollectionName
},
Database = "DBNAME",
Certificate = certificate,
}.Initialize();
And don't forget to load you certificate the following way if you want to have it working on Azure as #iftah suggested above:
X509Certificate2 certificate = new X509Certificate2(certificateBytes, string.Empty, X509KeyStorageFlags.MachineKeySet);
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"));
I have a C# RestFull client trying to connect to a Go server. Once I reach the TLS handshake stage it fails because client didn't provide a certificate.
I have verified that client certificate was associated with RestClient Object before the execution of the request.
// Initializing the client
RestClient client = new RestClient(Config.SERVER_DOMAIN);
// Adding client certificate (exported from a smart card with no private key attributes)
client.ClientCertificates = Global.cpf.getCertCollection();
// defining our request then executing it
var request = new RestRequest("users", Method.GET);
var response = await client.ExecuteTaskAsync(request);
It works only if the certificate was read from a .PFX file where the private component is there. But when I switch to smart card certificate (which has no private key attributes because the smart card doesn't want you to have them) the server doesn't receive any certificate from the client.
I understand that TLS needs a private key for the handshake stage, yet the client obj doesn't see any associated private key with the given certificate and therefore doesn't recognize the certificate as a valid one for TLS establishment.
I know that private keys can't be exported from the smart card, and I know that there has to be a way to tell RestClient object that in order to pass handshake stage, you should communicate with the smart card, however, I gave up!
Can someone point me to the right direction?
For most of the smartcards there should be minidriver or standalone CSP (Cryptographic Service Provider) available. These components act as a drivers that integrate the card with Windows cryptographic subsystem. They are usually provided by the device vendor and you should be able to get your certificate with reference to correct private key from your Windows Certificate Store once they're set up correctly.
Try to use the following method:
private static X509Certificate2 GetCertificate()
{
X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
try
{
// Note: X509Certificate2UI requires reference to System.Security
X509Certificate2Collection certs = X509Certificate2UI.SelectFromCollection(store.Certificates, null, null, X509SelectionFlag.SingleSelection);
if (certs != null && certs.Count > 0)
return certs[0];
}
finally
{
store.Close();
}
return null;
}
I have a non-exportable client certificate in the "client" computer's Local Computer / Personal certificates store, and I want to use this certificate for client authentication when making web requests using System.Net.Http.HttpClient.
I have tried using HttpClientHandler thus:
var subject = /* Distinctive subject name of the certificate known to exist in the store */;
var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
var certificates = store.Certificates.Find(X509FindType.FindBySubjectName, subject, true);
// Known/verifiable at this point that certificates[0] is the correct client cert
var clientHandler = new HttpClientHandler();
clientHandler.ClientCertificates.Add(certificates[0]);
using (var client = new HttpClient(clientHandler))
{
var response = await client.GetAsync(URL.Text);
var result = await response.Content.ReadAsStringAsync();
// ... etc...
}
... but the GetAsync call throws an exception with the message:
The request was aborted: Could not create SSL/TLS secure channel.
I have been able to verify that this is a client-side issue and that the correct certificate is being returned from the store.
I have tested this code against known working endpoints (which require a certificate); other clients work.
I know the certificate is valid because it is successfully used for client authentication by other client code (sources I don't currently have at my disposal)
As I understand it, I am only attaching the public portion (because it is non-exportable), so the authentication is failing. There must surely be a way to use a private, non-exportable key for such a purpose, correct? How do I do it?
As I understand if someone doesn't want to use a custom domain name and instead plans on using *.azurewebsite.net domain assigned to the website by Azure, then HTTPS is already enabled with a certificate from Microsoft(I know this is not as secure as using a custom domain name). How would be I able to load this certificate programmatically. Currently I use the following method to load a certificate from local machine or Azure :
public static X509Certificate2 LoadFromStore(string certificateThumbprint,bool hostedOnAzure)
{
var s = certificateThumbprint;
var thumbprint = Regex.Replace(s, #"[^\da-zA-z]", string.Empty).ToUpper();
var store = hostedOnAzure ? new X509Store(StoreName.My, StoreLocation.CurrentUser) : new X509Store(StoreName.Root, StoreLocation.LocalMachine);
try
{
store.Open(OpenFlags.ReadOnly);
var certCollection = store.Certificates;
var signingCert = certCollection.Find(X509FindType.FindByThumbprint, thumbprint, false);
if (signingCert.Count == 0)
{
throw new FileNotFoundException(string.Format("Cert with thumbprint: '{0}' not found in certificate store. Also number of certificates in the sotre was {1}", thumbprint, store.Certificates.Count));
}
return signingCert[0];
}
finally
{
store.Close();
}
}
I assume the culprit is the following line of code :
new X509Store(StoreName.My, StoreLocation.CurrentUser)
because when I get an exception it tells me there is no certificate in the store although I pass the correct certificate Thumbprint(I grab the thumbprint from Chrome manually).
You will not be able to access this certificate programmatically in your WebApp as this certificate is not really installed on the Azure WebApp. Azure WebApps have a front-end server which does a "kind of" SSL Offloading so the WebApp actually never has access to this particular certificate. Why exactly you want to read this certificate though ?
Typically if there is a need for certificates in WebApps, you would install client certificates and pass them to services for Authentication as mentioned in https://azure.microsoft.com/en-us/blog/using-certificates-in-azure-websites-applications/ and those certificates you can access programmatically (code snippet mentioned in the same article)
But I am not sure what exactly you want to achieve by reading the server certificate