Converting Openssl signing to .NET6

1.4k Views Asked by At

Application invokes openssl for signing using

openssl rsautl -sign -in rasi.bin -inkey riktest.key -out allkiri.bin

How to convert this to .NET 6 so that invoking openssl is not required?

riktest.key is text file containing

-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAfddAQEArYjDH7msMFifeYc1AG/TkKpcz2LITI73sC0eqnlgmWi3F7PD
Bo8lWrCw32h3v/FFMrK8KuktlnBtsSLaCCz1DWuXORzHaW7EqG8O8QNzFSmhIoqp
...

This is ASP.NET 6 MVC application. Does .NET 6 System.Security.Cryptography namespace contain this OpenSsl functionality ?

1

There are 1 best solutions below

7
On BEST ANSWER

Why generally the native .NET methods cannot be used

For RSASignaturePadding.Pkcs1, the native .NET implementations SignData() and SignHash() follow the RSASSA-PKCS1-v1_5 signature scheme described in RFC8017, which applies EMSA-PKCS1-v1_5 as encoding operation: The message is hashed and the following value is signed (i.e. encrypted with the private key):

EM = 0x00 || 0x01 || PS || 0x00 || T

Here PS consists of so many 0xff values that the size of EM is equal to the size of the modulus of the key. T is the DER encoding of the DigestInfo value, which contains the digest OID and the hash, e.g. for SHA256:

3031300d060960864801650304020105000420 || H 

where H is the 32 bytes SHA56 hash of the message M to be signed.

In contrast, openssl rsautl uses the RSA algorithm directly, as mentioned in the NOTES section, i.e. the following data is signed:

EM' = 0x00 || 0x01 || PS || 0x00 || M

This cannot be achieved with the native .NET methods in general (except for a special use case, see below): SignData() hashes and therefore fails, SignHash() does not hash but internally (like SignData()) generates the DER encoding of the DigestInfo value.

An alternative is BouncyCastle, which signs with the algorithm NoneWithRSA just like openssl rsautl.

One disadvantage of this algorithm is that only short messages can be signed due to the missing hashing, since the length criterion cannot be fulfilled for longer messages (according to which the size of EM' must correspond to the size of the modulus of the key).

Key Import

The posted key is a PEM encoded private key in PKCS#1 format.

.NET supports the import of PEM encoded keys (private/public, PKCS#8/PKCS#1 format) with ImportFromPem() since .NET 5, but the import of DER encoded keys has been supported since .NET Core 3.0. A private DER encoded key in PKCS#1 format can be imported with ImportRSAPrivateKey() (the conversion between PEM and DER encoding is trivial and consists of removing header, footer and line breaks and Base64 decoding of the remaining body).

BouncyCastle supports the import of a PEM encoded key with the PemReader class.

A possible implementation of the posted OpenSSL functionality with BouncyCastle

The following code generates the same signature as the OpenSSL statement when rasi.bin holds the data from dataToSign and riktest.key holds the key from privatePkcs1Pem:

using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using System;
using System.IO;

...

// For testing purposes a 512 bits key is used.
// In practice, keys >= 2048 bits must be used for security reasons!
string privatePkcs1Pem = @"-----BEGIN RSA PRIVATE KEY-----
                            MIIBOwIBAAJBANoHbFSEZoOSB9Kxt7t8PoBwmauaODjECHqJgtTU3h4MW5K3857+
                            04Flc6x6a9xxyvCKS5RtOP2gaOlOVtrph0ECAwEAAQJBALu8LpRr2RWrdV7/tfQT
                            HIJd8oQnbAe9DIvuwh/fF08IwApOE/iGL+Ded49eoHHu1OXycZhpHavN/sQMnssP
                            FNECIQDyDIW7V5UUu16ZAeupeQ7zdV6ykVngd0bb3FEn99EchQIhAOaYe3ll211q
                            SIXVjKHudMn3xe6Vvguc9O7cwCB+gyqNAiEAsr3kk6/de23SMZNlf8TR8Z8eyybj
                            BAuQ3BMaKzWpyjECIFMR0UFNYTYIyLF12aCoH2h2mtY1GW5jj5TQ72GFUcktAiAf
                            WWXnts7m8kZWuKjfD0MQiW+w4iAph+51j+wiL3EMAQ==
                            -----END RSA PRIVATE KEY-----";
byte[] dataToSign = Convert.FromHexString("3031300d060960864801650304020105000420d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592");

// Import private PKCS#1 key, PEM encoded
PemReader pemReader = new PemReader(new StringReader(privatePkcs1Pem));
AsymmetricKeyParameter privateKeyParameter = ((AsymmetricCipherKeyPair)pemReader.ReadObject()).Private;

// Sign raw data
ISigner signer = SignerUtilities.GetSigner("NoneWithRSA");
signer.Init(true, privateKeyParameter);
signer.BlockUpdate(dataToSign, 0, dataToSign.Length);
byte[] signature = signer.GenerateSignature();

Console.WriteLine(Convert.ToHexString(signature)); // 8C83CAD897EDA249FEC9EBA231061D585DAFC99177267E3E71BB8A3FCE07CC6663BF4DF7AF2E1C1945D2A6BB42EB25F042228B591FC18CDA82D92CAAE844670C

Special use case - when the native C# methods can be used

If rasi.bin contains the DER encoding of the DigestInfo value, BouncyCastle is not needed.
The following example assumes that rasi.bin contains the DER encoding of the DigestInfo value for the message The quick brown fox jumps over the lazy dog with SHA256 as digest. I.e. the last 32 bytes correspond to the SHA256 hash.
A possible implementation with native .NET methods is then:

using System;
using System.Security.Cryptography;

...

// For testing purposes a 512 bits key is used.
// In practice, keys >= 2048 bits must be used for security reasons!
string privatePkcs1Pem = @"-----BEGIN RSA PRIVATE KEY-----
                            MIIBOwIBAAJBANoHbFSEZoOSB9Kxt7t8PoBwmauaODjECHqJgtTU3h4MW5K3857+
                            04Flc6x6a9xxyvCKS5RtOP2gaOlOVtrph0ECAwEAAQJBALu8LpRr2RWrdV7/tfQT
                            HIJd8oQnbAe9DIvuwh/fF08IwApOE/iGL+Ded49eoHHu1OXycZhpHavN/sQMnssP
                            FNECIQDyDIW7V5UUu16ZAeupeQ7zdV6ykVngd0bb3FEn99EchQIhAOaYe3ll211q
                            SIXVjKHudMn3xe6Vvguc9O7cwCB+gyqNAiEAsr3kk6/de23SMZNlf8TR8Z8eyybj
                            BAuQ3BMaKzWpyjECIFMR0UFNYTYIyLF12aCoH2h2mtY1GW5jj5TQ72GFUcktAiAf
                            WWXnts7m8kZWuKjfD0MQiW+w4iAph+51j+wiL3EMAQ==
                            -----END RSA PRIVATE KEY-----";
byte[] sha256DigestInfoDer = Convert.FromHexString("3031300d060960864801650304020105000420d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592");
byte[] sha256HashToSign = new byte[32];
Buffer.BlockCopy(sha256DigestInfoDer, sha256DigestInfoDer.Length - sha256HashToSign.Length, sha256HashToSign, 0, sha256HashToSign.Length);

using (RSA rsa = RSA.Create())
{ 
    rsa.ImportFromPem(privatePkcs1Pem);
    byte[] signature = rsa.SignHash(sha256HashToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); // pass the SHA256 hash, internally the DER encoding of the DigestInfo is generated (which is why the digest must be specified)
    Console.WriteLine(Convert.ToHexString(signature)); // 8C83CAD897EDA249FEC9EBA231061D585DAFC99177267E3E71BB8A3FCE07CC6663BF4DF7AF2E1C1945D2A6BB42EB25F042228B591FC18CDA82D92CAAE844670C
}

which gives the same signature, since rasi.bin is identical in both cases.

However, keep in mind that the last approach only works if rasi.bin contains the DER encoding of the DigestInfo value, while the first solution works for arbitrary data in rasi.bin (as long as the length criterion is met).