3

This is my scenario:

A CA signs a certificate with its private key and sends it to a client via https response. From this response the client creates a X509Certificate2 class, that represents the certificate in .NET

X509Certificate2 myCert = new X509Certificate2(certBuffer);

I also have the CA's public key to verify the signature

RootCertificate.PublicKey.Key

I am now looking for a way to check if the signature is valid. How can I do that? The X509Certificate2 class doesn't seem to provide a way to access the signature field.

2 Answers 2

5

X.509 certificate validation is a complex process. With .NET, you are supposed to use the X509Chain class to perform such a validation, which entails path building, verifying signatures, revocation status, and a gazillion of other things. See this answer for an example.

X509Certificate2 also has a Verify() method, but that one checks the certificate with regards to the machine defaults: the user's trust store, the user's policies... which may or may not map well to your specific case. To have full control on the process, use X509Chain.

(Extracting the signature value, and the exact byte sequence which is signed, is not overly complex if you know a bit of ASN.1/DER, but the point is that there is much more to certificate validation than mere signatures. Doing things manually is a path which I have personally walked several times, and it entails a lot of suffering. Using the implementation already provided by .NET will save you a lot of time; and that implementation is X509Chain.)

2
  • So did I understand correctly, that chain.Build() will among others perform a signature verification? I added the CA certificate to the chain and called Build(myCert). This method returned true and calling it with an invalid certificate returned false. Can that be prove enough?
    – PogoMips
    Oct 2, 2013 at 9:42
  • Yes, that's how this class is supposed to work.
    – Tom Leek
    Oct 2, 2013 at 10:45
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.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .