SSL and Public private key (PEM/X509) cryptography by example in .NET Core
November 05, 2020
Introduction
This post illustrates examples of the two uses of public key cryptography in .NET Core:
- Encrypting and decrypting data.
- Signing and verifying data.
- Loading two
.pem
SSL certificates into Kestrel.
A full C# console app example is available on this Gist
Notes
- If you have a PFX file, I’ve written a separate blog post about converting your PFX file to the X509 (.
pem
, .cert
etc.) format used in these examples. - These examples are for RSA key pairs, you should be able to convert them to ECDSA easily though, with
ECDSA.Create()
. - The examples all use a simple bit of text, but this could easily be a file by using
byte[] textBytes = File.ReadAllBytes(...)
.crt
,.pem
,.key
files are all the same format, usually a text format- The padding when encrypting and decrypting needs to be the same format.
- This video explains asymmetrical encryption well
- This blog post has a few other C# examples
openssl
is a command line tool. Windows users can find it here, or alternatively just install the Linux subsystem and then get Ubuntu on the Windows Store, andcd /mnt/c/Users/yourusername
.
Encrypting and decrypting data
Overview
I want to send you a file or some text but I only want you to be able to see it, so I therefore need it to be encrypted.
First, you generate a public and private key pair, as two .PEM files.
$ openssl req -x509 -sha256 -days 365 -newkey rsa:4096 -nodes -keyout private.pem -out public.pem
You keep your private key very safe.
You send me your public key file:
public.pem
(sometimes the naming convention in examples iscertificate.pem
).
Encrypting
I encrypt my file (in the example below it’s a text file) using your public key. I then send you the output of this, which is a base-64’d string.
private static string Encrypt(string text)
{
byte[] publicPemBytes = File.ReadAllBytes("public.pem");
using var publicX509 = new X509Certificate2(publicPemBytes);
var rsa = publicX509.GetRSAPublicKey();
byte[] encrypted = rsa.Encrypt(System.Text.Encoding.Default.GetBytes(text), RSAEncryptionPadding.Pkcs1);
return Convert.ToBase64String(encrypted);
}
Decrypting
You can then decrypt the base64’d string I sent you using your private key, and you will see the file or message in its original, un-encrypted form:
private static string Decrypt(string base64Text)
{
using X509Certificate2 certificate = CombinePublicAndPrivateCerts();
var rsa = certificate.GetRSAPrivateKey();
byte[] textBytes = Convert.FromBase64String(base64Text);
byte[] decrypted = rsa.Decrypt(textBytes, RSAEncryptionPadding.Pkcs1);
return System.Text.Encoding.Default.GetString(decrypted);
}
Signing and Verifying (digital signatures)
Overview
There is another use of public key cryptography. Here’s my attempt to explain it:
I want to send your something, say a text file (or maybe a PDF), and you really want to be sure that I created this text file and therefore that it was me that sent it to you.
I generate a public-private key pair:
$ openssl req -x509 -sha256 -days 365 -newkey rsa:4096 -keyout private.pem -out public.pem
I keep my private key very safe.
I send you my public key:
public.pem
file (sometimes the naming convention in examples iscertificate.pem
).
Signing
I sign my text file (in this example it’s a string as the text
parameter). I then send you my text file and this base-64’d output.
As mentioned above, it’s easy to read in a binary file too - just use byte[] fileBytes = File.ReadAllBytes(..)
instead.
private static string SignWithPrivateKey(string text)
{
var privateKeyText = File.ReadAllText("private.pem");
var privateKeyBlocks = privateKeyText.Split("-", StringSplitOptions.RemoveEmptyEntries);
var privateKeyBytes = Convert.FromBase64String(privateKeyBlocks[1]);
using RSA rsa = RSA.Create();
rsa.ImportEncryptedPkcs8PrivateKey("your-secret-password", privateKeyBytes, out _);
byte[] fileBytes = System.Text.Encoding.Default.GetBytes(text);
byte[] signature = rsa.SignData(fileBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
return Convert.ToBase64String(signature);
}
Verifying
You can make sure the text file I sent you is the same one that I created, using:
- The text file I sent you (
plainText
in the example below, but could be a file usingbyte[]
). - The public key I sent you -
public.pem
file. - The base64’d verification string I sent you.
private static bool VerifySignature(string plainText, string base64Signature)
{
byte[] publicPemBytes = File.ReadAllBytes("public.pem");
using var publicX509 = new X509Certificate2(publicPemBytes);
var rsa = publicX509.GetRSAPublicKey();
byte[] dataBytes = System.Text.Encoding.Default.GetBytes(plainText);
byte[] signatureBytes = Convert.FromBase64String(base64Signature);
return rsa.VerifyData(dataBytes, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
Loading PEM file SSL certificates in Kestrel
Typically, when you want to use an SSL certificate you will receive a single PEM file - the public and private key inside it, maybe the chain of trust certs also inside that PEM file. You might also receive the SSL certificate as a public key and private PEM key too.
On Windows, the certificate loading inside Kestrel assumes you are always using a PFX-format certificate - PKCS12. PEM format files are typically provided to you as PKCS8, so you need to specify this to load PEM files as SSL certificates on Windows.
The workaround is below - this has been an ongoing issue since .NET Core 1.0.
// See https://github.com/dotnet/runtime/issues/23749 | |
public static class CertHelper | |
{ | |
// To generate a self-signed cert: | |
// dotnet dev-certs https -ep $pwd/selfsigned.pem --format Pem -np | |
public static X509Certificate2 GetCertificate() | |
{ | |
X509Certificate2 sslCert = CreateFromPublicPrivateKey("certs/selfsigned.pem", "certs/selfsigned.key"); | |
// work around for Windows (WinApi) problems with PEMS, still in .NET 5 | |
return new X509Certificate2(sslCert.Export(X509ContentType.Pkcs12)); | |
} | |
public static X509Certificate2 CreateFromPublicPrivateKey(string publicCert="certs/public.pem", string privateCert="certs/private.pem") | |
{ | |
byte[] publicPemBytes = File.ReadAllBytes(publicCert); | |
using var publicX509 = new X509Certificate2(publicPemBytes); | |
var privateKeyText = File.ReadAllText(privateCert); | |
var privateKeyBlocks = privateKeyText.Split("-", StringSplitOptions.RemoveEmptyEntries); | |
var privateKeyBytes = Convert.FromBase64String(privateKeyBlocks[1]); | |
using RSA rsa = RSA.Create(); | |
if (privateKeyBlocks[0] == "BEGIN PRIVATE KEY") | |
{ | |
rsa.ImportPkcs8PrivateKey(privateKeyBytes, out _); | |
} | |
else if (privateKeyBlocks[0] == "BEGIN RSA PRIVATE KEY") | |
{ | |
rsa.ImportRSAPrivateKey(privateKeyBytes, out _); | |
} | |
X509Certificate2 keyPair = publicX509.CopyWithPrivateKey(rsa); | |
return keyPair; | |
} | |
} | |
// Your program.cs | |
public static IHostBuilder CreateHostBuilder(string[] args) | |
{ | |
return Host.CreateDefaultBuilder(args) | |
.UseSerilog() | |
.ConfigureWebHostDefaults(webBuilder => | |
{ | |
webBuilder.ConfigureKestrel(options => | |
{ | |
options.ConfigureHttpsDefaults(adapterOptions => | |
{ | |
adapterOptions.ServerCertificate = CertHelper.GetCertificate(); | |
}); | |
}); | |
webBuilder.UseStartup<Startup>(); | |
}); | |
} |
I'm Chris Small, a software engineer working in London. This is my tech blog. Find out more about me via Github, Stackoverflow, Resume