C# How can I validate a Root-CA-Cert certificate (x509) chain?
Asked Answered
Z

2

26

Let's say I have three certificates (in Base64 format)

Root
 |
 --- CA
     |
     --- Cert (client/signing/whatever)

How can I validate the certs and certificate path/chain in C#? (All those three certs may not be in my computer cert store)

Edit: BouncyCastle has the function to verify. But I'm trying not to use any third-party library.

    byte[] b1 = Convert.FromBase64String(x509Str1);
    byte[] b2 = Convert.FromBase64String(x509Str2);
    X509Certificate cer1 = 
        new X509CertificateParser().ReadCertificate(b1);
    X509Certificate cer2 =
        new X509CertificateParser().ReadCertificate(b2);
    cer1.Verify(cer2.GetPublicKey());

If the cer1 is not signed by cert2 (CA or root), there will be exception. This is exactly what I want.

Zhao answered 7/9, 2011 at 9:37 Comment(0)
M
42

The X509Chain class was designed to do this, you can even customize how it performs the chain building process.

static bool VerifyCertificate(byte[] primaryCertificate, IEnumerable<byte[]> additionalCertificates)
{
    var chain = new X509Chain();
    foreach (var cert in additionalCertificates.Select(x => new X509Certificate2(x)))
    {
        chain.ChainPolicy.ExtraStore.Add(cert);
    }

    // You can alter how the chain is built/validated.
    chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
    chain.ChainPolicy.VerificationFlags = X509VerificationFlags.IgnoreWrongUsage;

    // Do the validation.
    var primaryCert = new X509Certificate2(primaryCertificate);
    return chain.Build(primaryCert);
}

The X509Chain will contain additional information about the validation failure after Build() == false if you need it.

Edit: This will merely ensure that your CA's are valid. If you want to ensure that the chain is identical you can check the thumbprints manually. You can use the following method to ensure that the certification chain is correct, it expects the chain in the order: ..., INTERMEDIATE2, INTERMEDIATE1 (Signer of INTERMEDIATE2), CA (Signer of INTERMEDIATE1)

static bool VerifyCertificate(byte[] primaryCertificate, IEnumerable<byte[]> additionalCertificates)
{
    var chain = new X509Chain();
    foreach (var cert in additionalCertificates.Select(x => new X509Certificate2(x)))
    {
        chain.ChainPolicy.ExtraStore.Add(cert);
    }

    // You can alter how the chain is built/validated.
    chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
    chain.ChainPolicy.VerificationFlags = X509VerificationFlags.IgnoreWrongUsage;

    // Do the preliminary validation.
    var primaryCert = new X509Certificate2(primaryCertificate);
    if (!chain.Build(primaryCert))
        return false;

    // Make sure we have the same number of elements.
    if (chain.ChainElements.Count != chain.ChainPolicy.ExtraStore.Count + 1)
        return false;

    // Make sure all the thumbprints of the CAs match up.
    // The first one should be 'primaryCert', leading up to the root CA.
    for (var i = 1; i < chain.ChainElements.Count; i++)
    {
        if (chain.ChainElements[i].Certificate.Thumbprint != chain.ChainPolicy.ExtraStore[i - 1].Thumbprint)
            return false;
    }

    return true;
}

I am unable to test this because I don't have a full CA chain with me, so it would be best to debug and step through the code.

Magnetoelectricity answered 7/9, 2011 at 10:15 Comment(6)
Thanks. But I purposely put different issuer cert in "additionalCertificates" and the result is "true" :(Zhao
Thanks Jonathan. But not working. It looks like chain.Build is verifying the validity of the certs. Not the cert path. As long as certs are valid (tho the cert chain/path is wrong) the result is true. thumbprint method is not working because all thumbprints are different (even in the correct path/chain)Zhao
@Jacob, that's why I validate the thumbprints manually, as I said, step through the new portion of the code. Also I made a typo in the if (chain.Build) bit. Update it.Magnetoelectricity
primaryCertificate is end certificate and additional certs are CA to rootIdeogram
The chain.Build() never returns false and thus not very useful. Even if I take a wrong root CA - it still returns true.Assoil
@AlexanderFarber That's probably because one of the certificates is trusted by the system anyway. See learn.microsoft.com/en-us/dotnet/api/… you can now restrict which root certificates get usedMacao
B
1

The X509Chain does not work reliably for scenarios where you do not have the root certificate in the trusted CA store on the machine.

Others will advocate using bouncy castle. I wanted to avoid bringing in another library just for this task, so I wrote my own.

As see in RFC3280 Section 4.1 the certificate is a ASN1 encoded structure, and at it's base level is comprised of only 3 elements.

  1. The "TBS" (to be signed) certificate
  2. The signature algorithm
  3. and the signature value
Certificate  ::=  SEQUENCE  {
     tbsCertificate TBSCertificate,
     signatureAlgorithm   AlgorithmIdentifier,
     signatureValue BIT STRING
}

C# actually has a handy tool for parsing ASN1, the System.Formats.Asn1.AsnDecoder.

Using this, we can extract these 3 elements from the certificate to verify the chain.

The first step was extracting the certificate signature, since the X509Certificate2 class does not expose this information and it is necessary for the purpose of certificate validation.

Example code to extract the signature value part:

public static byte[] Signature(
    this X509Certificate2 certificate,
    AsnEncodingRules encodingRules = AsnEncodingRules.BER)
{
    var signedData = certificate.RawDataMemory;
    AsnDecoder.ReadSequence(
        signedData.Span,
        encodingRules,
        out var offset,
        out var length,
        out _
    );

    var certificateSpan = signedData.Span[offset..(offset + length)];
    AsnDecoder.ReadSequence(
        certificateSpan,
        encodingRules,
        out var tbsOffset,
        out var tbsLength,
        out _
    );

    var offsetSpan = certificateSpan[(tbsOffset + tbsLength)..];
    AsnDecoder.ReadSequence(
        offsetSpan,
        encodingRules,
        out var algOffset,
        out var algLength,
        out _
    );

    return AsnDecoder.ReadBitString(
        offsetSpan[(algOffset + algLength)..],
        encodingRules,
        out _,
        out _
    );
}

The next step is to extract the TBS certificate. This is the original data which was signed.

example code to extract the TBS certificate data:

public static ReadOnlySpan<byte> TbsCertificate(
    this X509Certificate2 certificate,
    AsnEncodingRules encodingRules = AsnEncodingRules.BER)
{
    var signedData = certificate.RawDataMemory;
    AsnDecoder.ReadSequence(
        signedData.Span,
        encodingRules,
        out var offset,
        out var length,
        out _
    );

    var certificateSpan = signedData.Span[offset..(offset + length)];
    AsnDecoder.ReadSequence(
        certificateSpan,
        encodingRules,
        out var tbsOffset,
        out var tbsLength,
        out _
    );

    // include ASN1 4 byte header to get WHOLE TBS Cert
    return certificateSpan.Slice(tbsOffset - 4, tbsLength + 4);
}

You may notice that when extracting the TBS certiifcate I needed to include the ASN1 header in the data, this is because the signature of the TBS Certificate INCLUDES this data (this annoyed me for a while).

For the first time in history, the Microsoft does not impede us with their API design, and we are able to obtain the Signature Algorithm directly from the X509Certificate2 object. Then we just need to decide to what extend we are going to implement different hash algorithms.

var signature = signed.Signature();
var tbs = signed.TbsCertificate();
var alg = signed.SignatureAlgorithm;

// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpnap/a48b02b2-2a10-4eb0-bed4-1807a6d2f5ad
switch (alg)
{
    case { Value: var value } when value?.StartsWith("1.2.840.113549.1.1.") ?? false:
        return signedBy.GetRSAPublicKey()?.VerifyData(
            tbs,
            signature,
            value switch {
                "1.2.840.113549.1.1.11" => HashAlgorithmName.SHA256,
                "1.2.840.113549.1.1.12" => HashAlgorithmName.SHA384,
                "1.2.840.113549.1.1.13" => HashAlgorithmName.SHA512,
                _ => throw new UnsupportedSignatureAlgorithm(alg)
            },
            RSASignaturePadding.Pkcs1
        ) ?? false;
    case { Value: var value } when value?.StartsWith("1.2.840.10045.4.3.") ?? false:
        return signedBy.GetECDsaPublicKey()?.VerifyData(
            tbs,
            signature,
            value switch
            {
                "1.2.840.10045.4.3.2" => HashAlgorithmName.SHA256,
                "1.2.840.10045.4.3.3" => HashAlgorithmName.SHA384,
                "1.2.840.10045.4.3.4" => HashAlgorithmName.SHA512,
                _ => throw new UnsupportedSignatureAlgorithm(alg)
            },
            DSASignatureFormat.Rfc3279DerSequence
        ) ?? false;
    default: throw new UnsupportedSignatureAlgorithm(alg);
}

As shown in the code above, https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpnap/a48b02b2-2a10-4eb0-bed4-1807a6d2f5ad is a good resource to see the mapping of algorithms and OIDs.

Another thing you should be aware of is that there are some articles out there that claim that for elliptical curve algorithms, microsoft expects a R,S formatted key instead of a DER formatted key. I tried to convert the key to this format but it ultimately didn't work. What I discovered was that it was necessary to use the DSASignatureFormat.Rfc3279DerSequence parameter.

Additional certificate checks, like "not before" and "not after", or CRL and OCSP checks can be done in addition to the chain verification.

Biologist answered 22/12, 2022 at 6:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.