One of the nice things about .NET Core being open source is following along with some of the issues that people report. I tend to keep an eye on System.Security tagged issues, since those tend to be at the intersection of things that interest me and things I can maybe help with.

A user filed an issue where .NET Framework considered a CMS valid, and .NET Core did not. This didn’t entirely surprise me. In the .NET Framework, the SignedCms class is heavily backed by Windows’ handling of CMS/PKCS#7. In .NET Core, the implementation is managed (sans the cryptography). The managed implementation adheres somewhat strictly to the CMS specification. As other issues have noticed, Windows’, thus .NET Framework’s, implementation was a little more relaxed in some ways.

This turned out not to be one of those cases. The CMS part was actually working just fine. What was failing was RSA itself. The core of the issue was that different implementations of RSA disagreed on the RSA signature’s validity.

That seems pretty strange!

When I talk about different implementations on Windows, I am usually referring to CAPI vs CNG, or RSACryptoServiceProvider and RSACng, respectively. For now, I’m keeping this post to the .NET Framework. We’ll bring .NET Core in to the discussion later.

There are two implementations because, well, Windows has two of them. CNG, or “Cryptography API: Next Generation” is the newer of the two and is intended to be future of cryptographic primitives on Windows. It shipped in Windows Vista, and offers functionality that CAPI cannot do. An example of that is PSS RSA signatures.

.NET Framework exposes these implementations as RSACryptoServiceProvider and RSACng. They should be interchangable, and CNG implementations should be used going forward. However, there is one corner case where the old, CAPI implementation considers a signature valid while the CNG one does not.

The issue can be demonstrated like so:

byte[] n = new byte[] { ... };
byte[] e = new byte[] { ... };
byte[] signature = new byte[] { ... };
var digest = new byte[] {
    0x68, 0xB4, 0xF9, 0x26, 0x34, 0x31, 0x25, 0xDD,
    0x26, 0x50, 0x13, 0x68, 0xC1, 0x99, 0x26, 0x71,
    0x19, 0xA2, 0xDE, 0x81, 
};
using (var rsa = new RSACng())
{
    rsa.ImportParameters(new RSAParameters {
        Modulus = n,
        Exponent = e
    });
    var valid = rsa.VerifyHash(digest, signature, HashAlgorithmName.SHA1,
                               RSASignaturePadding.Pkcs1);
    Console.WriteLine(valid);
}
using (var rsa = new RSACryptoServiceProvider())
{
    rsa.ImportParameters(new RSAParameters {
        Modulus = n,
        Exponent = e
    });
    var valid = rsa.VerifyHash(digest, signature, HashAlgorithmName.SHA1,
                               RSASignaturePadding.Pkcs1);
    Console.WriteLine(valid);
}

When used with one of the curious signatures that exhibits this behavior, such as the one in the GitHub link, the first result will be false, and the second will be true.

Nothing jumped out at me as being problematic. The signature padding is PKCS, the public exponent is the very typical 67,537, and the RSA key is sensible in size.

To make it stranger, this signature came off the timestamp of Firefox’s own signed installer. So why are the results different?

Jeremy Barton from Microsoft on .NET Core made the observation that the padding in the RSA signature itself is incorrect, but in a way that CAPI tollerates and CNG does not, at least by default. Let’s look at the raw signature. To do that, we need the public key and signature on disk, and we can poke at them with OpenSSL.

Using the command:

openssl rsautl -verify -in sig.bin -inkey key.der \
    -pubin -hexdump -raw -keyform der

We get the following output:

0000 - 00 01 ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
0010 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
0020 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
0030 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
0040 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
0050 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
0060 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
0070 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
0080 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
0090 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
00a0 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
00b0 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
00c0 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
00d0 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
00e0 - ff ff ff ff ff ff ff ff-ff ff ff 00 68 b4 f9 26
00f0 - 34 31 25 dd 26 50 13 68-c1 99 26 71 19 a2 de 81

This is a PKCS#1 v1.5 padded signature, as indicated by by starting with 00 01. The digest at the end can be seen, 68 b4 f9 26 ... 19 a2 de 81 which matches the digest above, so we know that the signature is for the right digest.

What is not correct in this signature is how the digest is encoded. The signature contains the bare digest. It should be encoded as an ASN.1 sequence along with the AlgorithmIdentifer of the digest:

DigestInfo ::= SEQUENCE {
	digestAlgorithm AlgorithmIdentifier,
	digest OCTET STRING
}

This goes back all the way to a document (warning: link is to an ftp:// site) written in 1993 by RSA labratories explaining how PKCS#1 v1.5 works,and was standardized in to an RFC in 1998.

The RSA signature we have only contains the raw digest. It is not part of a DigestInfo. If the digest were properly encoded, it would look something like this:

0000 - 00 01 ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
0010 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
0020 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
0030 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
0040 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
0050 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
0060 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
0070 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
0080 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
0090 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
00a0 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
00b0 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
00c0 - ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff
00d0 - ff ff ff ff ff ff ff ff-ff ff ff ff 00 30 21 30
00e0 - 09 06 05 2b 0e 03 02 1a-05 00 04 14 68 b4 f9 26
00f0 - 34 31 25 dd 26 50 13 68-c1 99 26 71 19 a2 de 81

The signature now includes DigestInfo along with the OID 1.3.14.3.2.26 to indicate that the digest is SHA1.

At this point we know what the difference is, and the original specification in part 10.1.2 makes it fairly clear that the “data” should be a digest and should be encoded as DigestInfo, not a bare digest.

The source of this signature is from Verisign's timestamp authority at http://timestamp.verisign.com/​scripts/​timstamp.dll. After checking with someone at DigiCert (now running this TSA), it was launched in May 1995.

I suspect that the TSA is old enough that the implementation was made before the specification was complete or simply got the specification wrong and no one noticed. Bringing this back to CNG and CAPI, CNG can validate this signatures, but you must explicitly tell CNG that the signature does not have an object identifier. BCRYPT_PKCS1_PADDING_INFO’s documentation has the detail there, but gist of it is

If there is no OID in the signature, then verification fails unless this member is NULL.

This would be used with {B,N}CryptVerifySignature. To bring this back around to the .NET Framework, how do we use RSACng and give null in for the padding algorithm? The short answer is: you cannot. If you try, you will get an explicit ArgumentException saying that the hash algorithm name cannot be null.

For .NET Framework, this solution “keep using RSACryptoServiceProvider”. If you need to validate these signatures, chances are you do not need to use CNG’s newer capabilities like PSS since these malformed signatures appear to be coming from old systems. Higher level things like SignedCms and SignedXml use RSACryptoServiceProvider by default, so they will continue to work.

To bring in .NET Core, the situation is a little more difficult. If you are using SignedCms like so:

var signedCms = new SignedCms();
signedCms.Decode(File.ReadAllBytes("cms-with-sig.bin"));
signedCms.CheckSignature(true);

This will start throwing when you migrate to .NET Core. .NET Core will use CNG when run on Windows to validate RSA signatures for SignedCms and SignedXml. This is currently not configurable, either. When used with SignedCms, it ultimately calls the X509Certificate2.GetRSAPublicKey() extension method, and that will always return an implementation based on CNG.

If you are using SignedCms on .NET Core and need to validate a CMS that is signed with these problematic signatures, you are currently out of luck using in-the-box components. As far as other platforms go, both macOS and Linux environments for .NET Core will agree with CNG - that the signature is invalid.

The good news is, these signatures are not easy to come by. So far, only the old Verisign timestamp authority is known to have produced signatures like this.