Tuesday, 17 March 2015

Visual C#: RSA encryption using certificate

Intro

RSA is a well-known cryptosystem using assymetric encryption. It performs encryption using a public key, decryption using a private key. The private key should be protected. The most efficient way of managing these keys in a Windows environment is by using certificates. To protect the private key, you should make it not exportable. This way the private key is only available on the machine it is being used.

Create certificate

Request certificate from CA

When enrolling for a certificate, make sure that the template has the Legacy Cryptographic Service Provider selected. Otherwise .Net will not be able to use the certificate. It will crash with this exception:
Unhandled Exception: System.Security.Cryptography.CryptographicException: Invalid provider type specified.

rsa_template

Generate self-signed certifiate

Windows Server 2012 R2 provides a cmdlet, New-SelfSignedCertificate, to generate a certificate. However, this cmdlet does not provide sufficient parameters to generate a certificate that can be used in C#. I used following script to generate my self-signed certificate, New-SelfsignedCertificateEx:
This script is an enhanced open-source PowerShell implementation of deprecated makecert.exe tool and utilizes the most modern certificate API — CertEnroll.
In my search for the correct value for the ProviderName parameter, I came across the interface of IX509PrivateKey, which provides a LegacyCsp boolean flag. I added following $PrivateKey.LegacyCsp = $true in #region Private Key [line 327]. After this addition, following powershell command gave me a certificate I could use to perform RSA encryption and decryption:
param($certName)
Import-Module .\New-SelfSignedCertificateEx.psm1
New-SelfsignedCertificateEx -Subject "CN=$certName" 
  -KeyUsage "KeyEncipherment, DigitalSignature" 
  -StoreLocation "LocalMachine" -KeyLength 4096

Grant access to private key

The account(s) that will perform the decryption requires read access to the private key of the certificate. To configure this, open a management console (mmc). Add the certificates snap-in for the local computer. In the certificate store, right click the certificate, go to all tasks and click Manage Private Keys. Add the account and select Read. Apply the changes.
rsa_privKey
Alternatively, you can script the process using an extra module to find the private key location:
param($certName, $user)
Import-Module .\Get-PrivateKeyPath.psm1
$privateKeyPath = Get-PrivateKeyPath CN=$certName -StoreName My 
    -StoreScope LocalMachine
& icacls.exe $privateKeyPath /grant ("{0}:R" -f $user)

Export public key

The certificate with public key can be published and/or transported to a partner that needs to send sensitive data. To export the certificate with public key execute following script:
param($certName)
$Thumbprint = (Get-ChildItem -Path Cert:\LocalMachine\My 
  | Where-Object {$_.Subject -match "CN=$certName"}).Thumbprint;
Export-Certificate -FilePath "C:\certName.crt" 
  -Cert Cert:\LocalMachine\My\$Thumbprint
 
Do not forget to refresh the certificate keys on a regular basis.

Load certificate

Following code snippet shows how to locate and load the certificate.
using System;
using System.Security.Cryptography.X509Certificates;
 
private X509Certificate2 getCertificate(string certificateName)
{
    X509Store my = new X509Store(StoreName.My, StoreLocation.LocalMachine);
    my.Open(OpenFlags.ReadOnly);
    X509Certificate2Collection collection = 
    my.Certificates.Find(X509FindType.FindBySubjectName, certificateName, false);
    if (collection.Count == 1)
    {
        return collection[0];
    }
    else if (collection.Count > 1)
    {
        throw new Exception(string.Format("More than one certificate with name
   '{0}' found in store LocalMachine/My.", certificateName));
    }
    else
    {
        throw new Exception(string.Format("Certificate '{0}' not found 
  in store LocalMachine/My.", certificateName));
    }
}

Encryption

Following code snippet shows how to encrypt the input using a certificate.
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
 
private string EncryptRsa(string input)
{
       string output = string.Empty;
       X509Certificate2 cert = getCertificate(certificateName);
       using (RSACryptoServiceProvider csp = (RSACryptoServiceProvider)cert.PublicKey.Key)
       {
              byte[] bytesData = Encoding.UTF8.GetBytes(input);
              byte[] bytesEncrypted = csp.Encrypt(bytesData, false);
              output = Convert.ToBase64String(bytesEncrypted);
       }
       return output;
}

Decryption

Following code snippet shows how to decrypt the input using a certificate. Make sure that the account running this has read access to the private key.
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
 
private string decryptRsa(string encrypted)
{
    string text = string.Empty;
    X509Certificate2 cert = getCertificate(certificateName);
    using (RSACryptoServiceProvider csp = (RSACryptoServiceProvider)cert.PrivateKey)
    {
           byte[] bytesEncrypted = Convert.FromBase64String(encrypted);
           byte[] bytesDecrypted = csp.Decrypt(bytesEncrypted, false);
           text = Encoding.UTF8.GetString(bytesDecrypted);
    }
    return text;
}

References