C# / .NET - How to allow a "custom" Root-CA for HTTPS in my application (only)?

dtr84 picture dtr84 · Nov 10, 2015 · Viewed 7.8k times · Source

Okay, here is what I need to do:

My application, written in C# (.NET Framework 4.5), needs to communicate with our server via HTTPS. Our server uses a TLS/SSL certificate issued by our own Root-CA. That Root-CA, while perfectly trusted by my application, is not installed in the system's "trusted root" certificate store. So, without further work, C# refuses to contact the server, because the server's certificate cannot be validated - as expected. Note: We cannot use a Root-CA already installed in the system.

What can I do to allow my application to (securely) contact our server? I know that C# provides classes to install our Root-CA certificate into the system's certificate store as a "trusted root". That's not what we want to do! That's because (a) it shows an alarming (and way too technical) warning to the user, and because (b) it would effect other applications too - which we don't want or need.

So what I need is something that tells C#/.NET to use a "custom" (i.e. application-specific) set of certificates - instead of the system-wide certificate store - to validate the chain of the server certificate. The whole certificate chain still needs to be validated properly (including revocation lists!). Only our Root-CA needs to be accepted as a "trusted" root for my application.

What would be the best way to do this?

Any help would be much appreciated. Thanks in advance!


BTW: I found out that I can use ServicePointManager.ServerCertificateValidationCallback to install my own certificate validation function. This does work. But that method isn't good, because now I need to do the whole certificate validation manually in my own code. However, I do not want to re-implement the whole certificate verification process (e.g. downloading and checking CRL's etc), which is already implemented (and tested) in .NET Framework. It's like re-inventing the wheel and can never be tested as thoroughly as the .NET implementation that already exists.

Answer

Crypt32 picture Crypt32 · Nov 10, 2015

The RemoteCertificateValidationCallback delegate is the right way to your solution. However, I would use a different behavior in the delegate, than suggested by Olivier. That's why: too many irrelevant checks are performed and relevant are not.

So, look at the issue in details:

At first, we shall consider the scenario when your service uses legitimate certificate purchased from commercial CA (this may not be the case right now, but may be in some future). This means that if sslPolicyErrors parameter has None flag presented, immediately return True, the certificate is valid and there are no obvious reasons to reject it. This step is necessary only if the following your statement is NOT strict:

Only our Root-CA needs to be accepted as a "trusted" root for my application.

otherwise, ignore first step.

Let's assume, the service still uses certificate from private and untrusted CA. In this case we have to handle errors which are not related to certificate chain and are specific only to SSL session. Thus, when the RemoteCertificateValidationCallback delegate is called, we shall ensure that RemoteCertificateNameMismatch and RemoteCertificateNotAvailable flags are not presented in the sslPolicyErrors parameter. If any of them presented, we shall reject connection without additional checks.

Let's assume that none of these flags presented. At this point we correctly handled SSL-specific errors and only certificate chain may have issues.

If we reach this far, we can claim that sslPolicyErrors parameter contains RemoteCertificateChainErrors flag. This can mean everything and we have to make additional checks. Your root CA certificate is a constant. This means that we can examine root certificate in the chain parameter and compare it with our constant (Root CA certificate's thumbprint, for example). If comparison fails, we immediately reject the certificate, because it is not your's and there are no obvious reasons to trust certificate issued by an unknown CA and which may have other chain issues.

If comparison succeeds, then we reached the case we have to handle carefully and properly. We have to execute another instance of certificate chaining engine and instruct it to collect any chain issues, except UntrustedRoot error only. This means that if SSL certificate has other issues (RevocationOffline, validity, policy errors for example) we will know about that and will reject this certificate.

The code below is a programmatical implementation of many words above:

using System;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

namespace MyNamespace {
    class MyClass {
        Boolean ServerCertificateValidationCallback(Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) {
            String rootCAThumbprint = ""; // write your code to get your CA's thumbprint

            // remove this line if commercial CAs are not allowed to issue certificate for your service.
            if ((sslPolicyErrors & (SslPolicyErrors.None)) > 0) { return true; }

            if (
                (sslPolicyErrors & (SslPolicyErrors.RemoteCertificateNameMismatch)) > 0 ||
                (sslPolicyErrors & (SslPolicyErrors.RemoteCertificateNotAvailable)) > 0
            ) { return false; }
            // get last chain element that should contain root CA certificate
            // but this may not be the case in partial chains
            X509Certificate2 projectedRootCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
            if (projectedRootCert.Thumbprint != rootCAThumbprint) {
                return false;
            }
            // execute certificate chaining engine and ignore only "UntrustedRoot" error
            X509Chain customChain = new X509Chain {
                ChainPolicy = {
                    VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority
                }
            };
            Boolean retValue = customChain.Build(chain.ChainElements[0].Certificate);
            // RELEASE unmanaged resources behind X509Chain class.
            customChain.Reset();
            return retValue;
        }
    }
}

This method (named delegate) can be attached to ServicePointManager.ServerCertificateValidationCallback. The code might be compacted (combine multiple IF's in one IF statement, for example), I used verbose version to reflect textual logic.