C# X509 证书验证,带在线 CRL 检查,无需将根证书导入受信任的根 CA 证书存储

C# X509 certificate validation, with Online CRL check, without importing root certificate to trusted root CA certificate store

我试图在不将根 CA 证书导入受信任的根 CA 证书存储的情况下验证 X509 证书链(在生产中,此代码将 运行 在 Azure 函数中,并且 you can't add certificates to the trusted root CA certificate store on Azure App Services ).

我们还需要对此证书链执行在线 CRL 检查。

我对此进行了搜索,发现许多其他人都面临着同样的问题,但 none 的建议似乎有效。我遵循了 dotnet/runtime GitHub 上的 this SO post, which echoes the suggestions from issue #26449 中概述的方法。这是一个重现问题的小型控制台应用程序(针对 .NET Core 3.1):

static void Main(string[] args)
{
    var rootCaCertificate = new X509Certificate2("root-ca-cert.cer");
    var intermediateCaCertificate = new X509Certificate2("intermediate-ca-cert.cer");
    var endUserCertificate = new X509Certificate2("end-user-cert.cer");

    var chain = new X509Chain();
    chain.ChainPolicy.ExtraStore.Add(rootCaCertificate);
    chain.ChainPolicy.ExtraStore.Add(intermediateCaCertificate);
    chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
    chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
    chain.Build(endUserCertificate);
    chain.Build(new X509Certificate2(endUserCertificate));

    var errors = chain.ChainStatus.ToList();
    if (!errors.Any())
    {
        Console.WriteLine("Certificate is valid");
        return;
    }

    foreach (var error in errors)
    {
        Console.WriteLine($"{error.Status.ToString()}: {error.StatusInformation}");
    }
}

当运行这个returns三个错误:

UntrustedRoot: A certificate chain processed, but terminated in a root certificate which is not trusted by the trust provider.
RevocationStatusUnknown: The revocation function was unable to check revocation for the certificate.
OfflineRevocation: The revocation function was unable to check revocation because the revocation server was offline.

但是,如果我将根 CA 证书添加到受信任的根 CA 证书存储区,那么所有三个错误都会消失。

问题

  1. 这是我的实现有问题,还是我想做的不可能?
  2. 我有哪些选择可以尝试实现这一目标?一些谷歌搜索表明 .NET 5 中提供的 X509ChainPolicy.CustomTrustStore 可能会挽救局面。充气城堡是实现这一目标的另一种选择吗?

好吧,这不是一个完整的答案,但可能会让你继续下去。

我们之前构建的 Azure Web 应用程序(不是函数应用程序,但我想这无关紧要)完全可以满足您的需求。我们花了一个星期左右。完整的答案是发布整个应用程序的代码,我们显然做不到。不过有一些提示。

我们通过将证书上传到应用程序(TLS 设置)并通过 WEBSITE_LOAD_CERTIFICATES 设置在代码中访问它们来绕过证书问题(你把指纹放在那里,link 你也提到了已发布),而不是您可以在代码中获取它们并构建您自己的证书存储:

var certThumbprintsString = Environment.GetEnvironmentVariable("WEBSITE_LOAD_CERTIFICATES");
var certThumbprints = certThumbprintsString.Split(",").ToList();

var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
for (var i = 0; i < certThumbprints.Count; i++)
{
  store.Certificates.Find(X509FindType.FindByThumbprint, certThumbprint, false);
}

然后我们实施了一些验证器:主题、时间、证书链(您基本上以某种方式标记您的根,然后从您在商店中验证链的证书出发,看看您是否最终进入您的根)和 CRL。

对于 CRL Bouncy Castle 提供支持:

// Get CRL from certificate, fetch it, cache it
var crlParser = new X509CrlParser();
var crl = crlParser.ReadCrl(data);
var isRevoked = crl.IsRevoked(cert);

从证书中获取 CRL 很棘手,但可行(我出于这个目的,或多或少地遵循了这个 https://docs.microsoft.com/en-us/archive/blogs/joetalksmicrosoft/pki-authentication-as-a-azure-web-app)。

A bit of Googling suggests the X509ChainPolicy.CustomTrustStore offered in .NET 5 might save the day

是的。

不是将 rootCaCertificate 放入 ExtraStore,而是将其放入 CustomTrustStore,然后设置 chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;。现在你提供的根是唯一对链有效的根。您还可以删除 AllowUnknownCertificateAuthority 标志。

OfflineRevocation

这个错误有点误导。这意味着“已请求对该链进行撤销,但缺少一个或多个撤销响应”。在这种情况下,它丢失是因为构建器没有要求它,因为它不信任根。 (一旦您不信任根,您就无法信任 CRLs/OCSP 响应,那么为什么要询问它们呢?)

RevocationStatusUnknown

同样,未知是因为它没有要求它。此代码与 OfflineRevocation 不同,因为从技术上讲,有效的 OCSP 响应是(实际上)“我不知道”。那将是 online/unknown.

UntrustedRoot

已通过上面的自定义信任代码解决。


其他注意事项:确定证书有效的正确方法是从 chain.Build 中捕获布尔值 return。对于您当前的链,如果您禁用了撤销 (chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck),那么 Build 将 returned true... 但是 UntrustedRoot 错误仍会出现在 ChainStatus 中输出。 Build 中的布尔值 return 是 false 如果有任何错误 VerificationFlags 没有说要忽略 .

您也只需调用一次 Build :)。

static void Main(string[] args)
{
    var rootCaCertificate = new X509Certificate2("root-ca-cert.cer");
    var intermediateCaCertificate = new X509Certificate2("intermediate-ca-cert.cer");
    var endUserCertificate = new X509Certificate2("end-user-cert.cer");

    var chain = new X509Chain();
    chain.ChainPolicy.CustomTrustStore.Add(rootCaCertificate);
    chain.ChainPolicy.ExtraStore.Add(intermediateCaCertificate);
    chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
    chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
    bool success = chain.Build(endUserCertificate);

    if (success)
    {
        return;
    }

    foreach (X509ChainStatus error in chain.ChainStatus)
    {
        Console.WriteLine($"{error.Status.ToString()}: {error.StatusInformation}");
    }
}