Cryptography Improvements in .NET 5 - Support for PEM

.NET always had a broad range of cryptographic services. That said, there was always one area that was lacking - importing keys. Some of the existing methods are hard to use and require deep knowledge of how keys are constructed. This is why people who just wanted to get things working resorted to a number of different solutions starting from hybrids between .NET and Bouncy Castle and finishing with pure Bouncy Castle implementations.

What's New in .NET 5?

To be precise, the progress started with .NET Core 3.0 which brought support for PKCS #8 and X.509 SubjectPublicKeyInfo. This was a step forward, but it didn't solve one of the key problems - reading keys from their textually encoded form. This has been addressed in .NET 5.0 by bringing direct support for PEM. This, together with other APIs like ASN.1 reader/writer, opened ways for easier usage of keys stored in different forms.

An Example

The primary use case for PEM support is reading keys directly from .pem files content, but I wanted to show something else. The pure Bouncy Castle implementation I've brought up previously is part of my Web Push library and was created to provide an ES256 signature based on a VAPID private key. VAPID private key can't be used directly with .NET, but with new .NET 5 APIs, it can be encoded into PEM and imported into an ECDsa instance.

internal class ES256Signer : IDisposable
{
    private const string PRIVATE_DER_IDENTIFIER = "1.2.840.10045.3.1.7";
    private const string PRIVATE_PEM_KEY_PREFIX = "-----BEGIN EC PRIVATE KEY-----";
    private const string PRIVATE_PEM_KEY_SUFFIX = "-----END EC PRIVATE KEY-----";

    private readonly ECDsa _internalSigner;

    public ES256Signer(byte[] privateKey)
    {
        _internalSigner = ECDsa.Create(ECCurve.NamedCurves.nistP256);
        _internalSigner.ImportFromPem(GetPrivateKeyPem(privateKey));
    }

    ...

    private static ReadOnlySpan<char> GetPrivateKeyPem(byte[] privateKey)
    {
        AsnWriter asnWriter = new AsnWriter(AsnEncodingRules.DER);
        asnWriter.PushSequence();
        asnWriter.WriteInteger(1);
        asnWriter.WriteOctetString(privateKey);
        asnWriter.PushSetOf(new Asn1Tag(TagClass.ContextSpecific, 0, true));
        asnWriter.WriteObjectIdentifier(PRIVATE_DER_IDENTIFIER);
        asnWriter.PopSetOf(new Asn1Tag(TagClass.ContextSpecific, 0, true));
        asnWriter.PopSequence();

        return PRIVATE_PEM_KEY_PREFIX + Environment.NewLine
            + Convert.ToBase64String(asnWriter.Encode()) + Environment.NewLine
            + PRIVATE_PEM_KEY_SUFFIX;
    }
}

That's a lot less code, and it doesn't require that much secret knowledge. The ECDsa class makes the signing itself trivial.

internal class ES256Signer : IDisposable
{
    ...

    public byte[] GenerateSignature(string input)
    {
        return _internalSigner.SignData(Encoding.UTF8.GetBytes(input), HashAlgorithmName.SHA256);
    }

    ...
}

This is an example of important usability improvement which allows .NET developers to reduce LOC, simplify the code, and reduce the number of dependencies.