48

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?

galets
  • 17,802
  • 19
  • 72
  • 101

4 Answers4

53

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";
};
Palec
  • 12,743
  • 8
  • 69
  • 138
galets
  • 17,802
  • 19
  • 72
  • 101
  • 4
    Just for completeness, the ServerCertificateValidationCallback property is not available for [FtpWebRequest](https://msdn.microsoft.com/en-us/library/system.net.ftpwebrequest(v=vs.110).aspx) in .Net Framework 4.5 – Storm May 27 '15 at 07:19
  • 11
    Also, for those of you using `HttpClient` instead of `HttpWebRequest`, the `ServerCertificateValidationCallback` property is over on the `WebRequestHandler` object you're passing into the `HttpClient` constructor. (This is the object you're already using to setup client-certificates, so it's an easy addition.) – Granger Feb 24 '17 at 17:12
  • 3
    Can I use this approach on a generated wcf Service Reference? I'm sitting here with a generated subclass of System.ServiceModel.ClientBase, but I can't find where to plugin this behaviour except by doing the "global" way presented in the question. – Mattias Nordqvist Feb 14 '18 at 09:55
22

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.

ckarras
  • 4,946
  • 2
  • 33
  • 37
  • 2
    I prefer your answer since it allows you to maintain a whitelist and allow normal implementation for others. – Osa E Oct 15 '18 at 19:44
7

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
}
Matt Sach
  • 1,162
  • 16
  • 37
6

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.

  • 1
    I don't see how is it straightforward at all. Installing anything in third party root is highly security sensitive operation. I'm trying to avoid affecting other classes here, and what your advise affects every application on the system. – galets Jan 04 '14 at 00:27
  • 1
    In the sense that other applications can also access your server without a certificate warning, correct. – 500 - Internal Server Error Jan 04 '14 at 00:59
  • 1
    This is by far the better solution. It will allow only the certificates that you choose and you do not disable a standard SSL certificate check. – Soeren L. Nielsen Jul 06 '15 at 10:54