Search code examples
c#sslx509certificate2

c# Validating an X509Certificate2: am I doing this right?


Using framework 4.5.1 and the following requirement, am I doing this right?

  1. the URL in the certificate must match the given URL
  2. the certificate must be valid and trusted
  3. the certificate must not be expired

The following passes, but is this sufficient?

In particular does the call to chain.Build(cert) satisfy #2 above?

    protected bool ValidateDigitalSignature(Uri uri)
    {
        bool isValid = false;
        X509Certificate2 cert = null;
        HttpWebRequest request = WebRequest.Create(uri) as HttpWebRequest;
        using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
        {
            response.Close();
        }

        isValid = (request.ServicePoint.Certificate != null);
        if(isValid)
            cert = new X509Certificate2(request.ServicePoint.Certificate);
        if (isValid)
        {
            X509Chain chain = new X509Chain();
            chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
            chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain;
            chain.Build(cert);
            isValid = (chain.ChainStatus.Length == 0);
        }
        if (isValid)
        {
            var dnsName = cert.GetNameInfo(X509NameType.DnsName, false);

            isValid = (Uri.CheckHostName(dnsName) == UriHostNameType.Dns
                && uri.Host.Equals(dnsName, StringComparison.InvariantCultureIgnoreCase));
        }
        if (isValid)
        {
            //The certificate must not be expired
            DateTimeOffset today = DateTimeOffset.Now;
            isValid = (today >= cert.NotBefore && today <= cert.NotAfter);
        }
        return isValid;
    }

Solution

  • If you're trying to validate that an HTTPS certificate is valid, HttpWebRequest can do that for you.

    To make HttpWebRequest check the revocation status you need to set the global ServicePointManager.CheckCertificateRevocationList = true before calling GetResponse() (I think it's GetResponse, as opposed to the call to Create()).

    That will then check:

    • The certificate chains to a trusted root
    • The certificate is not expired (and other such things)
    • The request hostname matches what it should

    Which is all three points that you asked about. The hardest one is getting the hostname matching correct, because

    1. There can be several SubjectAlternativeName DNS entries, and there's not a good way to ask about them in .NET.
    2. Any SubjectAlternativeName DNS entry is allowed to have a wildcard (*) in it. But subject CN values are not (and the .NET APIs don't indicate which type of name you got back).
    3. Name normalization for IDNA, et cetera.

    In fact, the only thing that HttpWebRequest doesn't automatically do for you (unless you set the global) is check revocation. And you can do that via

    HttpWebRequest request = WebRequest.Create(uri) as HttpWebRequest;
    request.ServerCertificateValidationCallback = ValidationCallback;
    
    private static bool ValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
    {
        // Since you want to be more strict than the default, reject it if anything went wrong.
        if (sslPolicyErrors != SslPolicyErrors.None)
        {
            return false;
        }
    
        // If the chain didn't suppress any type of error, and revocation
        // was checked, then it's okay.
        if (chain.ChainPolicy.VerificationFlags == X509VerificationFlags.None &&
            chain.ChainPolicy.RevocationMode == X509RevocationMode.Online)
        {
            return true;
        }
    
        X509Chain newChain = new X509Chain();
        // change any other ChainPolicy options you want.
        X509ChainElementCollection chainElements = chain.ChainElements;
    
        // Skip the leaf cert and stop short of the root cert.
        for (int i = 1; i < chainElements.Count - 1; i++)
        {
            newChain.ChainPolicy.ExtraStore.Add(chainElements[i].Certificate);
        }
    
        // Use chainElements[0].Certificate since it's the right cert already
        // in X509Certificate2 form, preventing a cast or the sometimes-dangerous
        // X509Certificate2(X509Certificate) constructor.
        // If the chain build successfully it matches all our policy requests,
        // if it fails, it either failed to build (which is unlikely, since we already had one)
        // or it failed policy (like it's revoked).        
        return newChain.Build(chainElements[0].Certificate);
    }
    

    And, of note, as I put in the sample code here, you only need to check the return value of chain.Build(), because that will be false if any cert is expired or whatnot. You also may want to check the root cert (or an intermediate, or whatever) out of the built chain for being an expected value (certificate pinning).

    If the ServerCertificateValidationCallback returns false an exception is thrown on GetResponse().

    You should try your validator out to make sure it works: