Last Updated on February 11, 2024


Nowadays, security is an essential aspect of the software development process. With the evolution of the Internet and the rise of social media platforms, computers are processing more private and confidential data. Therefore, software developers need to protect users’ private and confidential information before, during, and after the manipulation of the data.

In a nutshell, it is necessary to verify the authenticity and integrity of the data and keep the confidentiality aspect of the information at the same time. This is where cryptographic concepts like encryption, signature, and hashing come to the rescue.

In this article, I will talk about the famous asymmetric encryption algorithm RSA and I will explain the different ways to create RSA keys with Microsoft .NET platforms.

Why did I decide to write an article on this topic? Well, as a software developer, I sometimes find myself doing some experiments at home during my spare time, and I stumbled around an issue that bothered me during one of those experiments.

Basically, I coded a small client/server application and as recommended by the JSON Web Encryption standard (RFC 7516), I used asymmetric encryption to protect the symmetric key used to encrypt the communication channel between the client and the server. Since I am using the .NET Framework, I decided to go with the RSACryptoServiceProvider class to generate 2048-bit RSA keys.

To my surprise, while testing the system, I noticed that the size of the RSA keys was 1024 bits instead of 2048 bits as specified in the code. This issue was reported and explained on GitHub as a limitation. Therefore, I started digging more into this topic and found some interesting tricks and limitations that I will explain in this article.

But before going further let’s take a look a look at the RSA algorithm.

RSA: the asymmetric encryption side

RSA is an acronym for Rivest–Shamir–Adleman, which comes from the names of the 3 scientists (Ron RivestAdi Shamir, and Leonard Adleman)  who published the first description of the algorithm in 1977.

It is a cryptographic algorithm that uses a key pair to allow public-key encryption. The key pair has 2 elements: the private key and the public key. A third party can use the public key to encrypt a message before sending the message to the private key holder: only the private key can be used to decrypt the message.

Therefore, the public key can be widely shared but the private key must be kept secret. Only the keyholder should be in possession of the private key. This mechanism is also known as asymmetric encryption because it requires the usage of a key-pair in the process.

The asymmetric encryption process is heavier than symmetric encryption. Therefore, it is recommended to use asymmetric encryption to encrypt only small bytes arrays (i.e not large data). The more common scenario consists of encrypting symmetric keys with asymmetric encryption and then use those symmetric keys to encrypt larger messages or files (e.g. Encrypting Data with RSA and AES). In this case, only the older of the private key can decrypt the file.

Here is an overview of this process:

Asymmetric encryption example

RSA: the digital signature side

In the digital signature use case, the key holder signs a message with the private key before sending it to a third party who is able to verify the signature with the public key. This mechanism ensures messages’ authenticity and integrity. Of course, the receiver has to trust the sender first…

Since the sender is the only one in possession of the private key, any receiver who validates the signature can be almost certain that the message is really coming from the sender. The words “almost certain” mean there is a high probability that the message is coming from the sender: because if the private key of the sender is compromised then, any person in possession of the private key can impersonate the sender. Therefore, the authenticity of the information depends on the capacity of the sender to keep the private key secret.

Example of digital signature in action




How to create RSA Keys with .NET

Now, let’s go through the different classes or methods that are available in .NET to create RSA Keys.

In the code samples, we will use the key size recommended by NIST at the moment of writing this article: 2048 bits. Technology is evolving at a fast pace and 1024-bit RSA keys are breakable nowadays.  According to NIST, 2048-bit keys are strong enough to keep your data secure until 2030, and 3072-bit keys should be strong beyond 2030. 

Basically, NIST says that a 2048-bit RSA key has the same cryptographic strength as a 112-bit symmetric key. Therefore, anyone doing a brute force attack on a 2048-bit RSA key will have to go through 2112 possibilities: this operation requires a lot of resources, that’s why it is safe to go with a 2048 bit for now.

Some people already started using 4096 bit RSA keys because they want their software to be future-proof (well, in my opinion, it’s better to move to Elliptic Curve Cryptography (ECC) all the way, but that’s just a personal preference).

One thing to keep in mind about the RSA algorithm is the fact that the CPU required for the encryption/decryption process increases with the key size. Therefore, you have to be careful not to choose a key size that will impact the overall performance of your system.

RSA Key properties

In a nutshell, here are the properties of an RSA key:

  • p and q:  two prime numbers, generated with the Rabin-Miller primality test algorithm.
  • n: the modulus, calculated by multiplying p and q. This number is used in both the private key and the public key. The number of bits in the modulus is in fact the RSA key size.
  • e: the public exponent, which is a random prime number that has to be between 1 and λ(n) (the Carmichael’s totient for n). Basically, the public key has 2 elements: e and n. For efficiency purposes, since e is meant to be shared publicly, most RSA key generators will simply set this value to the number 65537, which is 01 00 01 in hexadecimal (Big Endian of course). This hexadecimal value encoded to Base64 gives the string value that you often see in RSA keys “AQAB“.
  • d: the private exponent, which must be kept secret. d is computed by using e and λ(n) (d ≡ e−1 (mod λ(n))). As a matter of fact, λ(n) needs also to be kept secret.
  • dp, dq, and qi (InverseQ): values calculated based on the previous numbers. These properties are usually stored in the private key because they are used in different optimization techniques for decryption and signature (i.e Chinese remainder theorem).

NB: To keep it simple, we will skip the mathematical explanation behind the key generation process. If you like mathematic concepts and want to learn more about RSA key generation, just google the following words and have fun : RSA prime numbers generation, or Carmichael’s totient, or Extended Euclidian Algorithm.

Create RSA Keys with RSACryptoServiceProvider

RSACryptoServiceProvider is one of the first implementations of the RSA algorithm in the .NET Framework (mscorlib), provided by the Cryptographic Service Provider (CSP) on Windows.  So this implementation is based on the Microsoft Cryptographic API on Windows, also known as CryptoAPI (available since Windows NT 4.0).

Create RSA Keys with RSACryptoServiceProvider

Here is the code sample to create a 2048-bit ephemeral key:

//This code creates a 2048-bit key
using (var rsa = new RSACryptoServiceProvider(2048))
{
      UseRsaKey(rsa);
}

If you don’t specify a key size, the default constructor will generate a 1024-bit key.

Also, you have to be careful with setting the key size after creation, it just does not work. Setting the key size will not throw any exception, but it will just be just ignored (i.e explained in this github issue). Basically, the following code will generate a 1024 bit RSA Key instead of a 2048 bit key:

//Warning: this code will create a 1024-bit key
using (var rsa = new RSACryptoServiceProvider())
{
      rsa.KeySize = 2048;
      UseRsaKey(rsa);
}

Create RSA Keys with RSACng

RSACng is an implementation of the RSA algorithm based on Windows Cryptography API Next Generation (CNG). This is what Microsoft says about CNG: “Cryptography API: Next Generation (CNG) is the long-term replacement for the CryptoAPI. CNG is designed to be extensible at many levels and cryptography agnostic in behavior.”

So, CNG is more recent than CryptoAPI and is the replacement for CryptoAPI. However, CNG has been introduced only since Windows Server 2008 and Windows Vista.

Create RSA Keys with RSACng

Here is the code sample to create a 2048-bit ephemeral key:

//This code creates a 2048-bit key
using (var rsa = new RSACng(2048))
{
      UseRsaKey(rsa);
}

If you don’t specify a key size, the default constructor will generate a 2048-bit key as well.

The Set_KeySize method works very well in this class. For example, the following code will generate a 3072-bit key:

//This code creates a 3072-bit key
using (var rsa = new RSACng())
{
      rsa.KeySize = 3072;
      UseRsaKey(rsa);
}




Create RSA Keys with RSAOpenSsl

RSAOpenSsl is a .NET Core implementation of the RSA algorithm based on OpenSSL. This class is available for programs that will run on non-windows platforms where OpenSSL is installed.

Create RSA Keys with RSAOpenSsl

Here is the code sample to create a 2048-bit ephemeral key:

//The code creates a 2048-bit key as well
using (var rsa = new RSAOpenSsl(2048))
{
      UseRsaKey(rsa);
}

Create RSA Keys with RSA.Create

RSA.Create is a static factory method that instantiates and returns an implementation of the RSA algorithm. The implementation returned by this factory method depends on the platform where the code is running.

Microsoft strongly recommends using the factory methods available in the RSA class, since they are platform agnostic.

Create RSA Keys with RSA.Create

Here is the code sample to create a 2048-bit ephemeral key:

//This code creates a 2048-bit key
using (var rsa = RSA.Create(2048))
{
      UseRsaKey(rsa);
}

Warning: In .NET Framework, the parameterless RSA.Create returns an instance of RSACryptoServiceProvider that doesn’t have an implementation of the set_KeySize method. If you set the KeySize, it will just be ignored. So, the following code will generate a 1024 bit RSA key instead of a 2048 bit RSA Key:

//Warning: this code creates a 1024-bit key
using (var rsa = RSA.Create())
{
      rsa.KeySize = 2048;
      UseRsaKey(rsa);
}

Conclusion

In a nutshell, the best way to generate RSA keys with .NET is to use one of the factory methods RSA.Create.  In order to have a clean code and an easy way to write your unit tests, I strongly suggest using these factory methods behind an interface. This way, you can use dependency injection and isolate your business logic code from the code that creates RSA keys. This way, you can update your code if needed without doing a refactoring.

Special Thanks!

Special Thanks!

I would like to give a shout out to Jeremy Barton! His posts on GitHub and Stack Overflow were really helpful when I was testing different scenarios.