Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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.

like image 706
dtr84 Avatar asked Nov 10 '15 10:11

dtr84


People also ask

How old is the letter C?

The letter c was applied by French orthographists in the 12th century to represent the sound ts in English, and this sound developed into the simpler sibilant s.

What is C full form?

Originally Answered: What is the full form of C ? C - Compiler . C is a general-purpose, high-level language that was originally developed by Dennis M. Ritchie to develop the UNIX operating system at Bell Labs. C was originally first implemented on the DEC PDP-11 computer in 1972.

What C is used for?

C programming language is a machine-independent programming language that is mainly used to create many types of applications and operating systems such as Windows, and other complicated programs such as the Oracle database, Git, Python interpreter, and games and is considered a programming foundation in the process of ...

What is C in C language?

What is C? C is a general-purpose programming language created by Dennis Ritchie at the Bell Laboratories in 1972. It is a very popular language, despite being old. C is strongly associated with UNIX, as it was developed to write the UNIX operating system.


2 Answers

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.

like image 165
Crypt32 Avatar answered Sep 22 '22 12:09

Crypt32


You can use that callback and don't have to reinvent the wheel.

If you take a closer look, the callback has multiple parameters:

public delegate bool RemoteCertificateValidationCallback(
    object sender,
    X509Certificate certificate,
    X509Chain chain,
    SslPolicyErrors sslPolicyErrors
)

The parameters are the result of the check through the .Net implementation. You can e.g. check if the only problem is the missing root CA by using the following code:

// Check if the only error of the chain is the missing root CA,
// otherwise reject the given certificate.
if (chain.ChainStatus.Any(statusFlags => statusFlags.Status != X509ChainStatusFlags.UntrustedRoot))
    return false;

It will iterate through the whole chain and checks all states. If any is anything else then untrusted root (beware: the enum has a flags attribute) we fail. But if the only bad thing is the not trusted root, you can take it.

But another problem you get now, is that you know that this certificate has an untrusted root, but you don't know if that is really your certificate (or any other).

To ensure this, you have to read the public part of your server certificate that you have stored with your application and compare it to the given chain:

// Read CA certificate from file.
var now = DateTime.UtcNow;
var certificateAuthority = new X509Certificate(_ServerCertificateLocation);
var caEffectiveDate = DateTime.Parse(certificateAuthority.GetEffectiveDateString());
var caExpirationDate = DateTime.Parse(certificateAuthority.GetExpirationDateString());

// Check if CA certificate is valid.
if (now <= caEffectiveDate
    || now > caExpirationDate)
    return false;

// Check if CA certificate is available in the chain.
return chain.ChainElements.Cast<X509ChainElement>()
                          .Select(element => element.Certificate)
                          .Where(chainCertificate => chainCertificate.Subject == certificateAuthority.Subject)
                          .Where(chainCertificate => chainCertificate.GetRawCertData().SequenceEqual(certificateAuthority.GetRawCertData()))
                          .Any();

Maybe it would be wise to add a fast exit at the beginning of the function, if the server delivers a certificate that is signed by an installed root CA (which is maybe not yours!):

if (sslPolicyErrors == SslPolicyErrors.None
    && chain.ChainStatus.All(statusFlags => statusFlags.Status == X509ChainStatusFlags.NoError))
    return true;

Also if desired (depending on your needs) you can allow or disallow to use a certificate, where the server name and certificate name doesn't match. This happens, e.g. if the certificate was made for localhost and you access the server from a different machine. Or within a intranet you only use http://myserver instead of http://myserver.domain.com, but the certificate was made on the full qualified name (or vice versa):

if (sslPolicyErrors == SslPolicyErrors.RemoteCertificateNameMismatch)
    return false;

And that's it. You still rely on the default implementation and only additionally check your parts.

like image 45
Oliver Avatar answered Sep 22 '22 12:09

Oliver