Search code examples
.netsslssl-certificatex509certificate2

How to ensure we're getting the topmost Root CA certificate from a chain?


I'm using this code to evaluate each certificate in a chain for Root CA status:

Function GetRootCaCertificates(Chain As X509Chain) As IEnumerable(Of X509Certificate2)
  With Chain.ChainElements.Cast(Of X509ChainElement)
    With .Select(Function(Element As X509ChainElement) Element.Certificate)
      Return .Where(Function(Certificate As X509Certificate2)
                      With Certificate.Extensions.Cast(Of X509Extension)
                        With .Where(Function(Extension As X509Extension) TypeOf Extension Is X509BasicConstraintsExtension)
                          With .Cast(Of X509BasicConstraintsExtension)
                            Return .Where(Function(Extension As X509BasicConstraintsExtension) Extension.CertificateAuthority = True).Count > 0
                          End With
                        End With
                      End With
                    End Function).ToList
    End With
  End With
End Function

It works well, but in the current case it's returning the two StartCom certificates:

Certificate Chain

(Yes, I know that StartCom has been delisted—I'll be dealing with that later this week.)

The ServerCertificateValidationCallback delegate receives the X509Chain, which in turn contains an array of the certificates that make up the chain. I'm not confident that we can rely on the elements' order in the array to determine the top-level Root CA.

I found this and this, but the first relies on Magic Strings™ and the second relies on an optional field. And the Extension.CertificateAuthority property above doesn't narrow it down enough, either. As we can see, two certificates in the chain have this property set to True.

The public properties of an X509Certificate2 don't seem to include anything that we can use to reliably identify its issuer in the chain. Most handy would be something like Certificate.IssuerThumbprint or similar.

Obviously this information is available somehow, since the chain was built in the first place. And I'm having a hard time considering the possibility that this simple capability has been overlooked in the API.

I must be missing it somewhere.

--EDIT--

I found the X509ChainPolicy.ExtraStore property, which seems to contain all certificates except the one we're after. From here it's a simple matter of exclusion:

Dim oCertificates As List(Of X509Certificate2)
Dim oThumbprints As IEnumerable(Of String)

oThumbprints = Chain.ChainPolicy.ExtraStore.Cast(Of X509Certificate2).Select(Function(Certificate As X509Certificate2) Certificate.Thumbprint)
oCertificates = Chain.ChainElements.Cast(Of X509ChainElement).Select(Function(Element As X509ChainElement) Element.Certificate).ToList
oCertificates.RemoveAll(Function(Certificate As X509Certificate2) oThumbprints.Contains(Certificate.Thumbprint))

Is this a reliable means of finding the top-level Root CA in a chain?


Solution

  • The ChainElements are in order from leaf-most to root-most. So as long as you don't get a PartialChain error, it's the last ChainElement's Certificate.

    private static X509Certificate2 GetRootCertificate(X509Chain chain)
    {
        // Assumes that chain.Build was already called
    
        foreach (X509ChainStatus status in chain.ChainStatus)
        {
            if (status.Status == X509ChainStatusFlags.PartialChain)
            {
                return null;
            }
        }
        
        X509ChainElement chainElement = chain.ChainElements[chain.ChainElements.Count - 1];
        return chainElement.Certificate;
    }