Search code examples
.netsslclient-certificatesx509certificate2

Is this a fine way of validating a client certificate


I need to do a validation of a client certificate, and I am a bit uncertain if I am doing it the right way. My strategy is:

  1. see if I can build a chain
  2. check the built chain, to see if it matches what I know is true (leaf cert has the same issuer as the intermediate, there is three certs in the chain etc.)

I do an Issuer and a chain length check, because I did some unit tests, that did show that if I just sent in the public key part of either the

  • root certificate or
  • the intermediate certificate

the chain was built successfully, so I couldn't rely on the chain build alone, and that is why I am doing some additional checks, but now I am not certain if this is the right way. I am not checking for revocation, because I know that the CA does not provide that currently (It looks like their CRL is expired. More on that here).

Here is my code

public ChainValidatorStatus Validate(X509Certificate2 clientCertificateToBeValidated)
{
    var chain = new X509Chain();

    chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
    chain.ChainPolicy.CustomTrustStore.Add(_options.CA);
    chain.ChainPolicy.CustomTrustStore.Add(_options.Intermediate);

    chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;

    try
    {
        var buildValid = chain.Build(clientCertificateToBeValidated);
        var status = new ChainValidatorStatus { BuildValid = buildValid };

        if (buildValid)
        {
            var buildClientCert = chain.ChainElements.First();

            // there needs to be three elements in the ChainElements. We expect there to be: root, intermediate, client certificate
            var isThereThreeElementsInChainElements = chain.ChainElements.Count == 3;

            // we expect the first cert in the built chain to have an issuer that is the same as the intermediate
            var validIssuerInFirstCertificateInChain = buildClientCert.Certificate.Issuer == _options.Intermediate.Subject;

            // we expect the incoming to have the issuer as the intermediate
            var validIssuer = clientCertificateToBeValidated.Issuer == _options.Intermediate.Subject;

            X509ChainElementCollection elements = chain.ChainElements;
            bool isRootCertTheSame = elements[^1].Certificate.RawData.SequenceEqual(_options.CA.GetRawCertData());

            status.ValidIssuerInFirstCertificateInChain = validIssuerInFirstCertificateInChain;
            status.ValidIssuer = validIssuer;
            status.IsThereThreeElementsInChainElements = isThereThreeElementsInChainElements;
            status.IsRootCertTheSame = isRootCertTheSame;

        }

        // if we get here we know: the client cert is trusted, is signed by the intermediate, and is not the CA or the intermediate
        return status;
    }
    catch (Exception exception)
    {
        return ChainValidatorStatus.NotValid;
    }
}

Am I completely off here, or is this a valid way of making sure that:

  1. the client certificate is one I trust (by building the chain)
  2. the incoming certificate is not the root or the intermediate (by checking the issuer and the chain length)
  3. the incoming certificate is signed (or produced by?) the intermediate (by checking the Issuer)

Hope someone will be able to chip in.


Solution

  • X509Chain performs a complete certificate chain validation logic (as described in RFC 5280 §6 and a little-bit more), so your logic in if (buildValid) {} condition is redundant and potentially flawed. If you look into RFC, chain building and validation is very complex process and it is extremely unlikely you can replicate the same yourself with same degree of confidence. X509Chain implements this for you. I agree with comments, do not roll your own crypto.

    what you need to do in that condition block, is to perform actual authentication and authorization. That is, you have to examine leaf certificate, extract identify information and bind it to an account in your identity database. After that you can apply authorization rules to figure out what connected identity can do and what cannot.