Chris's coding blog

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, and cd /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 is certificate.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 is certificate.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.ImportPkcs8PrivateKey(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:

  1. The text file I sent you (plainText in the example below, but could be a file using byte[]).
  2. The public key I sent you - public.pem file.
  3. 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>();
});
}

net-coresecuritypublic-private-keyrsassl

Chris Small

I'm Chris Small, a software engineer working in London. This is my tech blog. Find out more about me via GithubStackoverflowResume