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

后端 未结 2 861
囚心锁ツ
囚心锁ツ 2021-02-04 04:08

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 certificat

相关标签:
2条回答
  • 2021-02-04 04:56

    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.

    0 讨论(0)
  • 2021-02-04 05:05

    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.

    0 讨论(0)
提交回复
热议问题