Best practices for using ServerCertificateValidationCallback - c#

I am working on a project that uses some HTTP communication between two back-end servers. Servers are using X509 certificates for authentication. Needless to say, when server A (client) establishes connection to server B (server), there is a SSL/TLS validation error, since certificates used are not from trusted 3rd party authority.
Normally, the way to handle it is using ServicePointManager.ServerCertificateValidationCallback, such as:
ServicePointManager.ServerCertificateValidationCallback +=
(sender, cert, chain, error) =>
{
return cert.GetCertHashString() == "xxxxxxxxxxxxxxxx";
};
That approach works, except it's not ideal. What it essentially does is override validation procedure for EVERY http request done by the application. So, if another class will try to run HTTP request, it will fail. Also, if another class overrides ServicePointManager.ServerCertificateValidationCallback for its own purposes, then my communication starts failing out of sudden.
The only solution which comes to mind, is creating a separate AppDomain to perform client HTTP requests. That would work, but really - it's silly to have to do that only so that one can perform HTTP requests. Overhead will be staggering.
With that in mind, have anyone researched if there is a better practice in .NET, which would allow accessing web services, while handling client SSL/TLS validation without affecting other web clients?

An acceptable (safe) methodology working in .NET 4.5+ is to use HttpWebRequest.ServerCertificateValidationCallback. Assigning that callback on a specific instance of request will change the validation logic just for the request, not influencing other requests.
var request = (HttpWebRequest)WebRequest.Create("https://...");
request.ServerCertificateValidationCallback +=
(sender, cert, chain, error) =>
{
return cert.GetCertHashString() == "xxxxxxxxxxxxxxxx";
};

An alternative for code that does not use HttpWebRequest, and for environments where you can't install trusted certificates in the certificate store: Check the callback's error parameter, which will contain any error that were detected prior to the callback. This way, you can ignore errors for specific hash strings, but still accept other certificates that pass validation.
ServicePointManager.ServerCertificateValidationCallback +=
(sender, cert, chain, error) =>
{
if (cert.GetCertHashString() == "xxxxxxxxxxxxxxxx")
{
return true;
}
else
{
return error == SslPolicyErrors.None;
}
};
Reference:
https://msdn.microsoft.com/en-us/library/system.net.security.remotecertificatevalidationcallback(v=vs.110).aspx
Note that this will still affect other web client instances in the same appdomain (they will all accept the specified hash string), but at least it won't block other certificates.

The straight-forward approach for this scenario should be to install the two self-generated certificates in the trusted root stores on the client machines. You will get a security warning when you do this because the certificates can't be authenticated with Thawte or similar but after that regular secure communication should work. IIRC, you need to install the full (both public and private key) version in trusted root for this to work.

Bit late to the party, I know, but another option is to use a class inheriting IDisposable that can be put into a using(){} block around your code:
public class ServicePointManagerX509Helper : IDisposable
{
private readonly SecurityProtocolType _originalProtocol;
public ServicePointManagerX509Helper()
{
_originalProtocol = ServicePointManager.SecurityProtocol;
ServicePointManager.ServerCertificateValidationCallback += TrustingCallBack;
}
public void Dispose()
{
ServicePointManager.SecurityProtocol = _originalProtocol;
ServicePointManager.ServerCertificateValidationCallback -= TrustingCallBack;
}
private bool TrustingCallBack(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
// The logic for acceptance of your certificates here
return true;
}
}
Used in this fashion:
using (new ServicePointManagerX509Helper())
{
// Your code here
}

Related

The remote certificate was rejected by the provided RemoteCertificateValidationCallback - how to get more details?

I have prepared a test case for my problem - a very simple .NET 6 Console app:
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
namespace CheckCert
{
internal class Program
{
public const string jsonUrl = "https://wordsbyfarber.com/de/top-5";
static void Main(string[] args)
{
Console.WriteLine($"jsonUrl = {jsonUrl}");
HttpClientHandler handler = new()
{
ServerCertificateCustomValidationCallback = BackendCaValidation,
CheckCertificateRevocationList = true,
};
HttpClient httpClient = new(handler);
string jsonStr = httpClient.GetStringAsync(new Uri(jsonUrl)).Result;
Console.WriteLine($"jsonStr = {jsonStr}");
}
private static bool BackendCaValidation(HttpRequestMessage message,
X509Certificate2? certificate,
X509Chain? chain,
SslPolicyErrors sslPolicyErrors)
{
Console.WriteLine($"sslPolicyErrors = {sslPolicyErrors}");
return SslPolicyErrors.None == sslPolicyErrors;
}
}
}
When I run it, it works as expected, will print SslPolicyErrors.None and the JSON content from my private website, which uses a Let's Encrypt certificate.
However when I change the jsonUrl to the URL of my work server, which I am better not sharing in public, then I end up with SslPolicyErrors.RemoteCertificateChainErrors and a System.AggregateException.
The exception says "look at the inner exception".
So inspect the inner exception and it says:
The remote certificate was rejected by the provided RemoteCertificateValidationCallback
So I keep looking at the certificate and the chain displayed by the Microsoft Edge browser - both for my private website and for the work server.
The work server uses a certificate issued by a self-signed work CA (and there is an intermediate certificate inbetween). All 3 certificates are not expired yet.
My question is: how to get more information here?
Why exactly do I get a SslPolicyErrors.RemoteCertificateChainErrors? is it because of that self-signed corporate CA or maybe because of some signing algorithm?
And also - similar code works for us in another project (an Azure Service Fabric application) without failing. I wonder, what could be the difference?
UPDATE:
I have followed the suggestion by Mr. Spiller (thank you!) and have added the code:
Console.WriteLine("-----------------------------------");
foreach (X509ChainStatus status in chain.ChainStatus)
{
Console.WriteLine($"status = {status.Status}");
}
Now my private Let's Encrypt secured URL looks like this (why is there no chain printed? I can see the chain in the web browser):
And the "faulty" corporate URL looks like this:
My main question is: how to make my app work against the corporate URL, without making it insecure?
I.e. I would probably have to accept the returned SslPolicyErrors.RemoteCertificateChainErrors in my app, but can I still perform some checks?
The parameter X509Chain? chain has a property ChainStatus which you can use to get the status for each element of the certification chain.
Each element in turn has a property Status of type System.Security.Cryptography.X509Certificates.X509ChainStatusFlags (cf. documentation) that should give you the status of each particular element of the certification chain.
In your case one (the only?) element most likely has the status UntrustedRoot.
If you want to connect to your corporate server even though the certificate is not trusted, you can simply return true from the callback. I.e. in BackendCaValidation check whether you are talking to the corporate server and return true even though sslPolicyErrors is not None.
The other (preferred?) way is to trust your corporate CA system-wide. I.e. add the CA to the cert store of your operating system and mark it as trusted.

Finding a way to avoid IP spoofing on application authorization

I've developed C# application, which uses authorization via TCP. It connects to server application (located on amazon VPS), sends authorization string and gets response. The TCP information exchange flow is encrypted via TLS1.2 and also additionally with Rijndael symmetric encryption. Also inside the client application there is a check for SSLStream on RemoteCertificateValidationCallback where thumbprint of the ssl certificate obtained from serer is compared with the hardcoded one. The client applcation is obfuscated with .NetReactor, also with option to produce native .exe, and this exe additionally is compressed with Themida. But the hacker has somehow cracked the protocol and created his own fake authorization server and selling the illegal copy of program to his "clients". Each time I change the encryption secret key, he immediately goes out with new update, so it is infinite struggle. He doesn't need to crack the client .exe, he just updates the server on his side. I know he uses Windows standart postproxy to redirect the traffic, I've made addtional checks inside the client exe for existance of windows postproxy records in registry, but it didn't do any effect.
So my question is: is there any way in C# to check if application connects to the "correct" server and not the "fake" one? I'm using standard TcpClient class for authorization, but can migrate to something else if it can help.
As #00110001 mentions, you probably have more issues than this, but maybe public key pinning can help you.
Assuming that your binary is signed (i.e. cannot easily be altered after downloading directly from your website, without incurring warnings during start):
ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) =>
{
if (sender is HttpWebRequest httpWebRequest
&& httpWebRequest.RequestUri.Host.Contains("<your pinned domain>", StringComparison.OrdinalIgnoreCase))
{
return _allowedPublicKeys.Contains(certificate?.GetPublicKeyString());
}
else if (sslPolicyErrors == SslPolicyErrors.None)
{
return true;
}
return false;
};
But if the "hacker" is re-publishing your binaries with their changes, which it seems like the person does as they point to another authorization server, you're out of luck.
Edit: If you're using HttpClient and need only to pin some outbound requests, you can do something like:
internal HttpClient GetSecureHttpClient()
{
var handler = new HttpClientHandler();
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.ServerCertificateCustomValidationCallback = (httpRequestMessage, certificate, chain, sslPolicyErrors) =>
{
if (httpRequestMessage.RequestUri.Host.Contains("<your pinned domain>", StringComparison.OrdinalIgnoreCase))
{
return _allowedPublicKeys.Contains(certificate?.GetPublicKeyString());
}
else if (sslPolicyErrors == SslPolicyErrors.None)
{
return true;
}
return false;
};
var client = new HttpClient(handler);
return client;
}
// use:
var _ = await GetSecureHttpClient().GetAsync("https://google.com");
See dotnetfiddle example: https://dotnetfiddle.net/q1ggDS

ASP.NET Web API: verify if client has specific certifcate installed

We want to create a self signed certificate and manually install it on client PC's, with the private key marked as non-exportable.
The client calls a ASP.NET Web API, and there we want to check if that specific certificate is installed.
We want to do this as an additional method of authentication, to make sure that only valid clients can call the Web API. We know this is'nt waterproof because it is still possible to export the private key, but we have other additional authentication mechanisms such as a user password.
How can we achieve this? Or are there better ways to achieve our goal?
For Me After I created the self signed certificate in the Server , I added he Certificate Thumbprint to my Client App
private void setAppLocalSSL()
{
ServicePointManager.ServerCertificateValidationCallback += delegate (
object sender,
X509Certificate cert,
X509Chain chain,
SslPolicyErrors sslPolicyErrors)
{
if (sslPolicyErrors == SslPolicyErrors.None)
{
return true; //Is valid
}
// this is the Thumbprint of my local cert
// your Cert Thumbprint instead of "0000000000000000"
if (cert.GetCertHashString().ToLower() == "0000000000000000")
{
return true;
}
return false;
};
}
call setAppLocalSSL onload
Note : this is a WPF App.

Accept only self signed SSL-Certificate C#

I have a selfprogrammed API running on a Windows Server 2012 with a self signed SSL-Certificate. Now I want to communicate with the webservice via HTTPS.
The comunication is only at a local network, but i still want the conection to be secure.
Is there a way to accept only my self signed certificate? I found a lot of solutions to accept all certificates, but I only want mine to be accepted.
I already thought about adding it to the windows accepted certificates, but since the program consuming the webservice is user by serveral users on diffrent PC's and I do not have administration rights on all off them.
Is it even possible to have a secure connection the way I want it to be?
Yes, as Gusman suggests, you should implement your own method for the ServerCertificateValidationCallback. You can compare the thumbprint of the cert to validate whether it is the one you want to trust. Something like this should work:
public static class CertificateValidator
{
public static string TrustedThumbprint { get; set; }
public static bool ValidateSslCertificate(
object sender,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors errors)
{
// Wrap certificate to access thumbprint.
var certificate2 = new X509Certificate2(certificate);
// Only accept certificate with trusted thumbprint.
if (certificate2.Thumbprint.Equals(
TrustedThumbprint, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// In all other cases, don't trust the certificate.
return false;
}
}
In your startup code, include the following to wire this up:
// Read your trusted thumbprint from some secure storage.
// Don't check it into source control! ;-)
CertificateValidator.TrustedThumbprint = SomeSecureConfig.Get["TrustedApiThumbprint"];
// Set the callback used to validate certificates.
ServicePointManager.ServerCertificateValidationCallback += CertificateValidator.ValidateSslCertificate;

Validating a CAcert certificate in C#

I'm currently creating a C# program which will be fetching some data over https from my server. The server in question is using a CAcert certificate (http://www.cacert.org/), and I need a way of validating the servers certificate (checking the Subject and that it is signed by the cacert root certificate).
I'd like to do this without having to import the CAcert root as a trusted CA into the windows certificate store, some people might not like that, and AFAIK that requires admin.
I'm currently using a TcpClient and SslStream and not the WebRequest/WebResponse classes because I might move from using HTTP to using my own protocol some day, but if the task is easier using the *request classes I'll consider using them.
First you want to use the overloaded SslStream constructor:
SslStream(Stream innerStream, bool leaveInnerStreamOpen, RemoteCertificateValidationCallback userCertificateValidationCallback);
Then the RemoteCertificateValidationCallback method looks something like this:
public bool IsValid(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
... you logic here ...
}
You just need to simply walk the chain and look at the certificates until you find one you are willing to accept by verifying the public key:
foreach(X509ChainElement e in chain.ChainElements)
if( e.Certificate.Subject == "CN=XXX.xx" && e.Certificate.GetPublicKeyString() == "expected public key" )
return true;

Categories