简体   繁体   English

C#/ .NET - 如何在我的应用程序中仅允许HTTPS的“自定义”Root-CA?

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

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. 我的应用程序是用C#(.NET Framework 4.5)编写的,需要通过HTTPS与我们的服务器通信。 Our server uses a TLS/SSL certificate issued by our own Root-CA. 我们的服务器使用由我们自己的Root-CA颁发的TLS / SSL证书。 That Root-CA, while perfectly trusted by my application, is not installed in the system's "trusted root" certificate store. Root CA虽然完全受我的应用程序信任, 但未安装在系统的“受信任的根”证书存储中。 So, without further work, C# refuses to contact the server, because the server's certificate cannot be validated - as expected. 因此,如果没有进一步的工作,C#拒绝联系服务器,因为无法验证服务器的证书 - 正如预期的那样。 Note: We cannot use a Root-CA already installed in the system. 注意:我们无法使用已安装在系统中的Root-CA。

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". 我知道C#提供了将Root-CA证书作为“受信任的根”安装到系统证书存储中的类。 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. 这是因为(a)它向用户显示警告(并且过于技术性)警告,并且因为(b)它也会影响其他应用程序 - 这是我们不想要或不需要的。

So what I need is something that tells C#/.NET to use a "custom" (ie application-specific ) set of certificates - instead of the system-wide certificate store - to validate the chain of the server certificate. 所以我需要的是告诉C#/ .NET使用“自定义”(即特定于应用程序 )的证书集 - 而不是系统范围的证书存储 - 来验证服务器证书的链。 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. 只有我们的Root-CA需要被接受为我的应用程序的“受信任”根。

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. BTW:我发现我可以使用ServicePointManager.ServerCertificateValidationCallback来安装我自己的证书验证功能。 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 (eg downloading and checking CRL's etc), which is already implemented (and tested) in .NET Framework. 不过,我不想重新实现整个证书验证过程(例如,下载和检查CRL的等),在.NET框架中,这已经实现(和测试)。 It's like re-inventing the wheel and can never be tested as thoroughly as the .NET implementation that already exists. 这就像重新发明轮子一样,永远不能像已经存在的.NET实现那样彻底地进行测试。

The RemoteCertificateValidationCallback delegate is the right way to your solution. RemoteCertificateValidationCallback委托是解决方案的正确方法。 However, I would use a different behavior in the delegate, than suggested by Olivier. 但是,我会在委托中使用不同的行为,而不是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). 首先,我们将考虑您的服务使用从商业CA购买的合法证书的情况(现在可能不是这种情况,但可能在将来)。 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. 这意味着如果sslPolicyErrors参数显示None标志,则立即返回True ,证书有效且没有明显的理由拒绝它。 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. 只有我们的Root-CA需要被接受为我的应用程序的“受信任”根。

otherwise, ignore first step. 否则,忽略第一步。

Let's assume, the service still uses certificate from private and untrusted CA. 我们假设,该服务仍使用来自私有和不受信任的CA的证书。 In this case we have to handle errors which are not related to certificate chain and are specific only to SSL session. 在这种情况下,我们必须处理与证书链无关的错误,并且仅针对SSL会话。 Thus, when the RemoteCertificateValidationCallback delegate is called, we shall ensure that RemoteCertificateNameMismatch and RemoteCertificateNotAvailable flags are not presented in the sslPolicyErrors parameter. 因此,当调用RemoteCertificateValidationCallback委托时,我们将确保在sslPolicyErrors参数中不显示RemoteCertificateNameMismatchRemoteCertificateNotAvailable标志。 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. 此时,我们正确处理了特定于SSL的错误,并且只有证书链可能存在问题。

If we reach this far, we can claim that sslPolicyErrors parameter contains RemoteCertificateChainErrors flag. 如果我们到目前为止,我们可以声称sslPolicyErrors参数包含RemoteCertificateChainErrors标志。 This can mean everything and we have to make additional checks. 这可能意味着一切,我们必须进行额外的检查。 Your root CA certificate is a constant. 您的根CA证书是常量。 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). 这意味着我们可以检查chain参数中的根证书,并将其与我们的常量(例如,根CA证书的指纹)进行比较。 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. 如果比较失败,我们会立即拒绝该证书,因为它不是您的证书,并且没有明显的理由信任由未知CA颁发的证书,并且可能存在其他链问题。

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. 我们必须执行另一个证书链引擎实例,并指示它收集任何链问题,只有UntrustedRoot错误。 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. 这意味着如果SSL证书有其他问题(例如RevocationOffline,有效性,策略错误),我们将了解这一点,并将拒绝此证书。

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 . 此方法(名为委托)可以附加到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. 代码可能会被压缩(例如,在一个IF语句中组合多个IF),我使用详细版本来反映文本逻辑。

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. 参数是通过.Net实现检查的结果。 You can eg check if the only problem is the missing root CA by using the following code: 您可以使用以下代码检查唯一的问题是否是缺少的根CA:

// 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!): 如果服务器提供由已安装的根CA(可能不是您的CA)签名的证书,那么在函数开头添加快速退出可能是明智的:

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, eg if the certificate was made for localhost and you access the server from a different machine. 发生这种情况,例如,如果证书是为localhost制作的,并且您从另一台计算机访问服务器。 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): 或者在Intranet中,您只使用http://myserver而不是http://myserver.domain.com ,但证书是使用完全限定名称(反之亦然):

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

And that's it. 就是这样。 You still rely on the default implementation and only additionally check your parts. 您仍然依赖默认实现,只需另外检查您的部件。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM