C#/.NET-如何允许;自定义“;我的应用程序中HTTPS的根CA(仅限)

本文关键字:的根 HTTPS CA 仅限 应用程序 我的 NET- 何允许 自定义 | 更新日期: 2023-09-27 18:21:30

好的,这是我需要做的:

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

我可以做些什么来允许我的应用程序(安全地)联系我们的服务器?我知道C#提供了一些类,可以将我们的根CA证书作为"受信任的根"安装到系统的证书存储中。这不是我们想做的事!这是因为(a)它向用户显示了一个警告(而且技术性太强),而且因为(b)它也会影响其他应用程序——这是我们不想要或不需要的。

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

最好的方法是什么?

任何帮助都将不胜感激。提前感谢!


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

C#/.NET-如何允许;自定义“;我的应用程序中HTTPS的根CA(仅限)

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

因此,请详细查看问题:

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

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

否则,忽略第一步。

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

让我们假设这些标志都没有出现。在这一点上,我们正确地处理了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),我使用详细版本来反映文本逻辑。

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

如果仔细观察,回调有多个参数:

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;

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

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

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

// 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(可能不是您的!)签名,那么在功能开始时添加一个快速出口可能是明智的:

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;

就是这样。你仍然依赖默认的实现,只需要额外检查你的部分。