1

I'm trying to validate an X509 certificate chain without importing the root CA certificate into the trusted root CA certificate store (in production this code will run in an Azure Function, and you can't add certificates to the trusted root CA certificate store on Azure App Services).

We also need to perform an online CRL check on this certificate chain.

I've searched on this and I see many others are facing the same problem, but none of the suggestions seem to work. I've followed the approach outlined in this SO post, which echoes the suggestions from issue #26449 on the dotnet/runtime GitHub. Here's a small console application (targetting .NET Core 3.1) reproducing the problem:

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

When ran this returns three errors:

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.

However, if I add the root CA certificate to the trusted root CA certificate store then all three errors disappear.

Questions

  1. Is this something wrong with my implementation, or is what I'm trying to do not possible?
  2. What are my options to try to achieve this? A bit of Googling suggests the X509ChainPolicy.CustomTrustStore offered in .NET 5 might save the day. Is Bouncy Castle another option for achieving this?
simon-pearson
  • 1,601
  • 8
  • 10
  • The important part of the error message is "revocation server was offline". One way of checking certificate is to use the certificate to make a connection. So if you create a server in your application and make a virtual connection to the local host and i connection completes then you know the certificate was good. The error says server is offline so you need to make a real server by creating a socket. – jdweng May 14 '21 at 09:32
  • Hi @jdweng, thanks for replying. The CRL is definitely online because if I add the root CA certificate to my trusted root store all three errors disapper. Furthermore, I can browse to the CRL and download it. – simon-pearson May 14 '21 at 09:37
  • I think you mazy be failing because the TLS version is wrong. Try adding this static method to beginning of the code which sometimes helps : ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; If TLS fails then the connection will fail. – jdweng May 14 '21 at 09:53
  • @jdweng sorry, but all your assumptions and recommendations are irrelevant and unrelated to OP problem. – Crypt32 May 14 '21 at 11:45

2 Answers2

2

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

Yep.

Instead of putting rootCaCertificate into ExtraStore, put it into CustomTrustStore, then set chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;. Now your provided root is the only root valid for the chain. You can also remove the AllowUnknownCertificateAuthority flag.

OfflineRevocation

This error is slightly misleading. It means "revocation was requested for the chain, but one or more revocation responses is missing". In this case it's missing because the builder didn't ask for it, because it didn't trust the root. (Once you don't trust the root you can't trust the CRLs/OCSP responses, so why ask for them at all?)

RevocationStatusUnknown

Again, the unknown is because it didn't ask for it. This code is different than OfflineRevocation because technically a valid OCSP response is (effectively) "I don't know". That'd be an online/unknown.

UntrustedRoot

Solved by the custom trust code above.


Other things of note: The correct way to determine the certificate is valid is to capture the boolean return value from chain.Build. For your current chain, if you had disabled revocation (chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck) then Build would have returned true... but the UntrustedRoot error would still be present in the ChainStatus output. The boolean return from Build is false if there are any errors that the VerificationFlags didn't say to ignore.

You also only need to call Build once :).

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}");
    }
}
bartonjs
  • 30,352
  • 2
  • 71
  • 111
  • Many thanks for the detailed response - it all makes sense. My only outstanding question is what's the point of the `X509VerificationFlags.AllowUnknownCertificateAuthority` option if it doesn't allow untrusted root CAs? – simon-pearson May 14 '21 at 17:36
  • 1
    @simon-pearson it sort of does and sort of doesn’t allow them... it means Build won’t return false just because the root was unknown. If you had also said it was OK for revocation status unknown then build would have passed (nothing is expired or poorly chained). But, yeah, it’s a sort of weird half-thing. – bartonjs May 14 '21 at 18:36
  • I just re- read your reply. So you're saying that the `X509Chain` class only performs the CRL check if the chain could be built to a trusted root? Not that I don't believe you (I do!), but can you link me to some reading on this? I still feel a bit shaky on the fundamentals here with the validation checks that the `X509Chain` class. – simon-pearson May 15 '21 at 14:38
  • re-phrasing above question: so you're saying that the X509Chain class only performs the CRL check if the chain could be built to a trusted root? Is this defined in a standard and common to all CRL check implementations, or is this an implementation decision made by Microsoft when writing the X509Chain class? Can you link me to some reading on this? Also can you provide any links to reading around this weird behavior of the AllowUnknownCertificateAuthority flag causing X509Chain.Build to return true but still return an UntrustedRoot error? – simon-pearson May 15 '21 at 14:48
  • Can't stress enough I'm not questioning what you're saying - my understanding of the checks that the X509Chain class is performing is a little patchy and I was hoping to do some background reading to fill in the blanks. – simon-pearson May 15 '21 at 14:51
  • @simon-pearson Mainly I know the answers because I wrote the code (.NET X509Chain for Linux to behave like Windows). If you want a good explanation of why I trusted roots wouldn’t have revocation checked I suggest asking on InfoSec.SE. If you want to know why the status codes are suppressed with the ignore flags that’s really be a question about Windows CertBuildCertificateChain/CertVerifyCertificateChainPolicy. Not sure who would be able to answer “why”, since the code is 20 years old at this point. Don’t know of any existing places for these. – bartonjs May 17 '21 at 04:24
1

Well, not a full answer, but probably it will get you going.

We've built before an Azure Web App (not a function app, but I guess wouldn't matter) which did exactly what you want. Took us a week or so. Full answer would be posting the code of the whole app, we I obviously cannot do. Some hints though.

We walked around the certificate problem by uploading certificates to the app (TLS settings) and accessing them in code through WEBSITE_LOAD_CERTIFICATES setting (you put thumbprints there, it's also mentioned in the link you posted), than you can get them in code and build your own certificate store:

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

Than we implemented a number of validators: for subject, time, certificate chain (you basically mark your root somehow and go from the certificate you validate up the chain in your store and see if you end up in your root) and CRL.

For CRL Bouncy Castle offers support:

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

Getting CRL from the certificate is tricky, but doable (I followed this for the purpose, more or less https://learn.microsoft.com/en-us/archive/blogs/joetalksmicrosoft/pki-authentication-as-a-azure-web-app).

Maxim Zabolotskikh
  • 3,091
  • 20
  • 21
  • hi Maxim, thanks for your reply. If I'm understanding you right you're saying we'll have to hand-roll our certificate verification code - I'd really hoped it wouldn't come to that! What I don't understand is what the `X509VerificationFlags.AllowUnknownCertificateAuthority` flag is for if not exactly this? – simon-pearson May 14 '21 at 12:29
  • As I understand this flag is for verifying a chain that does not end in a trusted root, i.e. Thawte and the like, but in your own self-signed certificate. – Maxim Zabolotskikh May 17 '21 at 06:21