Custom certificate validation in .NET

  • .NET

Many companies run their own certificate authority (CA). While this is very convenient for a company as they can generate certificates on demand, it means that every software that make a call to a https internal service must be aware of the internal root certificate. On Windows, you can add the root certificate to the Certificate Store using a GPO so it's very easy. Other mechanism could be used on other operating systems. Thus, all applications can validate the root certificate using the certificate store. However, there are some case where you cannot add the certificate to the store and benefits of the automatic validation. By default, .NET will throw an exception when a certificate cannot be validated:

The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.

In this case you need to implement your own certificate validation. .NET allows you to override the default certificate validation, but many people use it to basically disable the validation. This is very bad as it reduces the security of your application. To do it right you need to know the reasons why a certificate can be invalid and handle only the case you are interested in. Here're some of the reasons a certificate is invalid:

  • It can be expired
  • It can be revoked (check the Certificate Revocation List (CRL))
  • It can have a weak signature (it uses unsecured algorithm for the signature)
  • it can be tampered (the signature is invalid)
  • It can be used for a usage it was not allowed to
  • One of the certificate of the chain is invalid
  • The root certificate is not trusted
  • etc.

In our case, we would like .NET to do all the common checks and just handle the case where the root certificate is not trusted. In this specific case and only this one, we'll check if the root certificate matches the internal CA's root certificate.

First, you need to get the root certificate and export it to an easy to use format. If the certificate is in the certificate store, you can export it as base64 encoded X.509.

  1. Open the certificate manager certmgr.msc

  2. Select the root certificate and select export

    Certificate Manager - Export Certificate

  3. Select the base-64 encoded X.509 format

    Certificate Export Wizard - Format CER

  4. At the end, you should have a file in the following form

    Certificate in CER format

Now let's write the validation method. The signature match the signature of the event we'll use later. In our case the only interesting parameter is the certificate chain. The chain contains all the certificates from the website's certificate to the CA's certificate (root certificate). All the certificates of the chain must be valid. if the root certificate is not valid because it is untrusted, we'll compare it with the internal CA's certificate.

private static bool ValidateCertificate(HttpRequestMessage request, X509Certificate2 certificate, X509Chain certificateChain, SslPolicyErrors policy)
{
    var validRootCertificates = new[]
    {
        Convert.FromBase64String(@"MIIDdzCCAl+gAwIBAgI...9OhgQ="), // Set your own root certificates (format CER)
    };

    foreach (var element in certificateChain.ChainElements)
    {
        foreach (var status in element.ChainElementStatus)
        {
            if (status.Status == X509ChainStatusFlags.UntrustedRoot)
            {
                // improvement: we could validate that the request matches an internal domain by using request.RequestUri in addition to the certicate validation

                // Check that the root certificate matches one of the valid root certificates
                if (validRootCertificates.Any(cert => cert.SequenceEqual(element.Certificate.RawData)))
                    continue; // Process the next status
            }

            return false;
        }
    }

    // Return true only if all certificates of the chain are valid
    return true;
}

You can now use an HttpClientHandler and set the custom certificate handler. Then, every time an invalid certificate is found it should call the custom handler which will have the last word.

using (var handler = new HttpClientHandler())
{
    handler.ServerCertificateCustomValidationCallback = ValidateCertificate;
    using (var httpClient = new HttpClient(handler))
    {
        // This should success
        var result = await httpClient.GetAsync("https://server.internal/");
    }
}

The call should now succeed thanks to the custom certificate handler!

Do you have a question or a suggestion about this post? Contact me on Twitter or by email!

Follow me:
Enjoy this blog?Buy Me A CoffeeDonate with PayPal