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

时间:2015-11-10 10:14:03

标签: c# .net ssl x509certificate

好的,这就是我需要做的事情:

我的应用程序是用C#(.NET Framework 4.5)编写的,需要通过HTTPS与我们的服务器通信。我们的服务器使用由我们自己的Root-CA颁发的TLS / SSL证书。 Root CA虽然完全受我的应用程序信任,但在系统的“受信任的根”证书存储区中安装。因此,如果没有进一步的工作,C#拒绝联系服务器,因为无法验证服务器的证书 - 正如预期的那样。注意:我们无法使用已安装在系统中的Root-CA.

如何让我的应用程序(安全地)联系我们的服务器?我知道C#提供了将Root-CA证书作为“受信任的根”安装到系统的证书存储中的类。那是我们想要做的事情!这是因为(a)它向用户显示警告(并且过于技术性)警告,并且因为(b)它也会影响其他应用程序 - 我们不会想要或需要。

所以我需要的是告诉C#/ .NET使用“自定义”(即特定于应用程序的)证书集 - 而不是系统范围的证书存储 - 来验证服务器证书链。整个证书链仍然需要正确验证(包括撤销列表!)。只有我们的Root-CA需要被接受为我的应用程序的“可信”根。

最好的方法是什么?

非常感谢任何帮助。提前谢谢!

BTW:我发现我可以使用ServicePointManager.ServerCertificateValidationCallback来安装我自己的证书验证功能。这个确实有效。但是这种方法并不好,因为现在我需要在我自己的代码中手动执行整个证书验证 。但是,我想要重新实现整个证书验证过程(例如,下载和检查CRL等),这已经在.NET Framework中实现(并经过测试)。这就像重新发明轮子一样,永远不能像已经存在的.NET实现那样彻底地进行测试。

2 个答案:

答案 0 :(得分:14)

RemoteCertificateValidationCallback代表是您解决方案的正确方法。但是,我会在委托中使用不同的行为,而不是Olivier建议的行为。这就是为什么:执行了太多不相关的检查,相关的检查没有。

所以,详细看一下这个问题:

首先,我们将考虑您的服务使用从商业CA购买的合法证书的情况(现在可能不是这种情况,但可能在将来)。这意味着如果sslPolicyErrors参数显示None标记,则立即返回True,证书有效,并且没有明显的理由拒绝它。仅当以下语句不严格时,才需要执行此步骤:

  

只有我们的Root-CA需要被接受为我的应用程序的“受信任”根。

否则,忽略第一步。

我们假设,该服务仍然使用来自私有和不受信任的CA的证书。在这种情况下,我们必须处理与证书链无关的错误,并且仅针对SSL会话。因此,当调用RemoteCertificateValidationCallback委托时,我们将确保RemoteCertificateNameMismatch参数中未显示RemoteCertificateNotAvailablesslPolicyErrors标记。如果其中任何一个出现,我们将拒绝连接而无需额外的检查。

我们假设这些标志都没有出现。此时,我们正确处理了SSL特定的错误,只有证书链可能存在问题。

如果我们到目前为止,我们可以声明sslPolicyErrors参数包含RemoteCertificateChainErrors标记。这可能意味着一切,我们必须进行额外的检查。您的根CA证书是常量。这意味着我们可以检查chain参数中的根证书,并将其与我们的常量(例如,根CA证书的指纹)进行比较。如果比较失败,我们会立即拒绝该证书,因为它不是您的证书,并且没有明显的理由信任由未知CA颁发的证书,并且可能存在其他链问题。

如果比较成功,那么我们就必须小心谨慎地处理。我们必须执行另一个证书链引擎实例,并指示它收集任何链问题,只有UntrustedRoot错误。这意味着如果SSL证书存在其他问题(例如RevocationOffline,有效性,策略错误),我们将了解这一点并拒绝此证书。

以下代码是上述许多单词的程序化实现:

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;
        }
    }
}

此方法(命名委托)可以附加到ServicePointManager.ServerCertificateValidationCallback。代码可能会被压缩(例如,在一个IF语句中组合多个IF),我使用了详细版本来反映文本逻辑。

答案 1 :(得分:2)

您可以使用该回调,而不必重新发明轮子。

如果仔细观察,请callback has multiple parameters

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

参数是通过.Net实现检查的结果。你可以,例如使用以下代码检查唯一的问题是否缺少根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;

它将遍历整个链并检查所有状态。如果有任何其他东西,那么不信任的根(注意:枚举有一个标志属性)我们失败了。但是,如果唯一的坏事是不受信任的根,你可以接受它。

但是你现在遇到的另一个问题是,你知道这个证书有一个不受信任的根,但你不知道这是不是你的证书(或任何其他证书)。

为确保这一点,您必须阅读与应用程序一起存储的服务器证书的公共部分,并将其与给定链进行比较:

// 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();

如果服务器提供由已安装的根CA(可能不是您的CA)签名的证书,那么在函数开头添加快速退出也许是明智之举:

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

如果需要(根据您的需要),您可以允许或禁止使用证书,其中服务器名称和证书名称不匹配。这发生了,例如,如果证书是为 localhost 制作的,则从其他计算机访问服务器。或者在Intranet中,您只使用http://myserver而不是http://myserver.domain.com,但证书是使用完全限定名称(反之亦然):

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

就是这样。您仍然依赖于默认实现,只需另外检查您的部件。