C# / .NET - 如何允许我的应用程序(仅限)使用“自定义”根证书颁发机构进行HTTPS?

24

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

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

我该怎么办才能让我的应用程序(安全地)联系我们的服务器?我知道C#提供了一些类来安装我们的Root-CA证书到系统证书存储中作为“受信任的根”。这不是我们想做的!这是因为(a)它会向用户显示令人担忧(而且过于技术化)的警告,以及(b)它还会影响其他应用程序——我们不想或不需要这样。

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

最好的方法是什么?

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


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

2个回答

26
RemoteCertificateValidationCallback委托是解决您问题的正确方法。但是,我会在委托中使用与Olivier建议的不同的行为。原因是:执行了太多无关紧要的检查,而有关紧要的检查则没有执行。
首先,我们应该考虑当您的服务使用从商用CA购买的合法证书时的情况(现在可能不是这种情况,但将来可能会是)。这意味着如果sslPolicyErrors参数具有None标志,则立即返回True,证书有效,并且没有拒绝它的明显原因。仅在以下语句不是严格的情况下,此步骤才是必要的:

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

否则,请忽略第一步。
假设该服务仍使用私有和不受信任的CA的证书。在这种情况下,我们必须处理与证书链无关且仅特定于SSL会话的错误。因此,在调用RemoteCertificateValidationCallback委托时,我们应确保sslPolicyErrors 参数中不存在RemoteCertificateNameMismatchRemoteCertificateNotAvailable标志。如果存在任何一个标志,则应拒绝连接而无需进行其他检查。
假设没有这些标志。在此时,我们已经正确处理了SSL特定错误,仅证书链可能存在问题。
如果我们到达这一步,可以声称sslPolicyErrors参数包含RemoteCertificateChainErrors标志。这可能意味着任何事情,因此我们必须进行附加检查。您的根CA证书是常量。这意味着我们可以在chain参数中检查根证书并将其与我们的常量(例如Root CA证书的Thumbprint)进行比较。如果比较失败,则立即拒绝该证书,因为它不属于您,没有明显的理由去相信由未知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;
        }
    }
}

这个方法(命名为delegate)可以附加到ServicePointManager.ServerCertificateValidationCallback。代码可能会被压缩(例如将多个IF合并到一个IF语句中),我使用冗长版本来反映文本逻辑。


非常感谢您的回复!我认为您所描述的很有道理。使用带有AllowUnknownCertificateAuthority标志的“自定义”X509Chain实际上是我正在寻找的。它将大部分检查推迟到了“框架”。但我必须问一下:如果找不到CRL或CRL指示我们的“内置”根证书已被吊销,您绝对确定Build()会在这里失败吗?虽然我们的“内置”根证书现在是完全受信任的,但我们需要能够在将来撤销它 - 无论出于什么原因。 - dtr84
1
RFC 5280不支持根CA吊销,也没有符合要求的链接引擎支持此功能。 - Crypt32
1
如果您想要替换根CA证书,请在代码中更改“rootCAThumbprint”变量的值为新值。这将自动使您之前的根CA证书失效。 - Crypt32
2
如果您想拒绝根CA,则请在代码中清除指纹值(或更新检索指纹的源)。我没有其他建议。 - Crypt32
6
这句话的意思是:在System.dll v4.0.0.0中,SslPolicyErrors.None等于0,这是否意味着(sslPolicyErrors & (SslPolicyErrors.None))总是为0?请注意,这只是翻译,不包括任何解释或其他信息。 - dhakim
显示剩余9条评论

7
你可以使用该回调函数,无需重新发明轮子。
如果你仔细观察,回调函数具有多个参数
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签名(可能不是你的!):

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

如果需要(根据您的需求),您可以允许或禁止使用证书,即使服务器名称和证书名称不匹配。例如,如果证书是为本地主机制作的,而您从另一台计算机访问该服务器,则会发生这种情况。或者在局域网中,您只使用http://myserver而不是http://myserver.domain.com,但证书是在全限定名称上制作的(反之亦然):

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

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


2
谢谢回复!但是你所做的就是我所说的“必须手动验证证书”的意思。你必须自己实现各种检查,无法确定是否正确(和完整)。实际上,你的代码还没有检查CRL。如果没有其他办法,我们可能需要走这条路。但我更喜欢像“像通常一样验证服务器的证书,但也将此特定的X509证书视为受信任的根(就像它在系统的‘受信任的根’存储中一样)”这样的解决方案。我可以以某种方式使用X509Chain.ChainPolicy.ExtraStore吗?不知道在哪里设置它! - dtr84
@dtr84,“你必须自己实现各种检查,无法确定你是否做得正确(和完整)”。是的,这个解决方案几乎无法与RFC5280验证规则竞争。 - Crypt32

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接