Loading Elliptic Curve (EC) Keys in .NET

Scott Brady
Scott Brady
C#

In the past, I’ve talked about how to create Elliptic Curve (EC) keys using OpenSSL and how to use them to sign JWTs and sign XML in .NET and .NET Core. This time, I’m going to talk about different ways of parsing and loading EC keys in .NET.

In this article, you’re going to learn what EC keys look like and then see how to use that knowledge in four different ways to load or create an ECDsa object in .NET. By the end of this article, you should be able to load in an EC key, no matter the format.

The parameters of an Elliptic Curve (EC) key

When loading an EC key, you’re going to want to know three things:

  1. The private key (optional)
  2. The public key
  3. The curve you need to use

The private key, d, is a random integer, the length of which depends on what curve you are using. If you have the private key, you’ll be able to sign data.

The public key is a point on the elliptic curve, using coordinates x & y. As a result, the public key is specific to that curve and is generated using the private key multiplied by a generator point, G, on your curve. With the public key, you’ll be able to verify signatures.

This shows that you need to know the elliptic curve in use (e.g., NIST’s P-256 or secp256k1) in order to use Elliptic Curve Cryptography (ECC). Without knowing the curve, you won’t know how long the private key should be, the level of security provided, or if the public key is valid (if they are points on the curve).

If you want to learn more about how Elliptic Curve Cryptography works, I found Practical Cryptography for Developers to be a useful read.

.NET’s ECDsa object

.NET’s abstraction for ECDSA is the class ECDsa found in System.Security.Cryptography. This inherits from AsymmetricAlgorithm and follows a similar pattern to the RSA class you may have used before. ECDsa is an abstract class with various platforms using different implementations (e.g., CNG on Windows and OpenSSL on Linux).

Your primary usage of this class will be to call the SignData and VerifyData methods. Otherwise, you might be passing it into a SecurityKey implementation or SignedXml.

The rest of this article will be focused on how to create an ECDsa implementation.

Generating an EC key using ECDsa and named curves

The simplest way to create a new EC key is to have .NET do it for you by using the Create method on ECDsa. This will create a private key on the curve of your choice. You’re typically going to use a known, named curve such as NIST’s P-256, aka secp256r1. You can find the curves supported out-of-the-box by .NET in the ECCurve.NamedCurve class.

using ECDsa key = ECDsa.Create(ECCurve.NamedCurves.nistP256);

This approach is great for generating keys on the fly during testing, but you’ll lose the key once the object gets disposed. Luckily, you can use various export methods on ECDsa; otherwise, you can access the EC parameters directly using the ExportParameters method. These export methods give you access to the ECParameters object, containing the key’s curve, D, and Q (x & y) points, which you’ll see in action next.

Creating ECDsa using ECParameters

Let’s say you’ve received the raw points for the curve, and you need to load the key in that way. This is common when using JSON Web Keys (JWK) and OpenID Connect.

Here’s an example JWK from RFC 7517:

{
  "kty": "EC",
  "crv": "P-256",
  "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
  "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0",
  "kid": "my EC key"
}

To load the key, you’ll need to know what curve the key uses and the public key parameters (the x and y coordinates). The private key can also be included in the JWK using the d parameter; however, this is rare with JWKs.

You can again use ECDsa’s Create method to load in these coordinates, but this time you’re passing in some ECParameters.

// parse curve from JOSE format
// https://www.iana.org/assignments/jose/jose.xhtml#web-key-elliptic-curve
var curve = crv switch
{
    "P-256" => ECCurve.NamedCurves.nistP256,
    "P-384" => ECCurve.NamedCurves.nistP384,
    "P-521" => ECCurve.NamedCurves.nistP521,
    _ => throw new NotSupportedException()
};

using ECDsa key = ECDsa.Create(new ECParameters
{
    Curve = curve,
    D = Base64UrlEncoder.DecodeBytes(d), // optional (private keys only)
    Q = new ECPoint
    {
        X = Base64UrlEncoder.DecodeBytes(x),
        Y = Base64UrlEncoder.DecodeBytes(y)
    }
});

Here you are parsing the curve from the JOSE designation and base64url decoding the other parameters using the encoder found in Microsoft.IdentityModel.Tokens. An alternative is available in Microsoft.AspNetCore.Authentication.

This parsing is specific to the formatting used by JWKs, but it demonstrates how you can load in an EC key if you know its parameters.

There is a Validate method on ECParameters, which will check key and coordinate sizes and check that the curve is valid. However, the Create method on ECDsa will call this for you.

Loading ECDsa from X509Certificate2

Loading an EC key from an X509Certificate2 is a case of simply using the GetECDsaPrivateKey and GetECDsaPublicKey methods.

var cert = new X509Certificate2("cert.pfx");
ECDsa key = cert.GetECDsaPrivateKey() ?? cert.GetECDsaPublicKey();

For loading the X509Certificate2 in the first place, you could load it from a PEM file.

X509Certificate2.PrivateKey vs GetRSAPrivateKey and GetECDsaPrivateKey

In the past, you might have used the PrivateKey and PublicKey properties on X509Certificate2. However, if you try to use these properties on a certificate containing an EC key, you will get null for PrivateKey and the following exception for PublicKey:

System.NotSupportedException: The certificate key algorithm is not supported.
        at System.Security.Cryptography.X509Certificates.PublicKey.get_Key()

To use an EC key from an X.509, you’ll need to use the GetECDsaPrivateKey and GetECDsaPublicKey methods.

As of .NET 6, the PrivateKey and PublicKey properties are marked as obsolete. As a result, you’ll have to use the RSA and ECDsa specific methods to load in keys across the board.

Loading ECDsa from a hex string

Sometimes, you’ll see EC keys shared as a hexadecimal string. This is common in the crypto communities, and by that, I mean both cryptography and cryptocurrency. I couldn’t say if it has much use in cryptozoology. These keys look something like this:

Private: c711e5080f2b58260fe19741a7913e8301c1128ec8e80b8009406e5047e6e1ef
Public: 04e33993f0210a4973a94c26667007d1b56fe886e8b3c2afdd66aa9e4937478ad20acfbdc666e3cec3510ce85d40365fc2045e5adb7e675198cf57c6638efa1bdb

These keys might look a bit odd at first, but they actually map back to the parameters you’ve been using already. The private key is just the hexadecimal representation of the d parameter, and the public key is the x & y coordinates concatenated together (with the first byte being a tag).

As a result, you can again use ECParameters to create your ECDsa object:

public static ECDsa LoadFromHex()
{
    var privateKey = "c711e5080f2b58260fe19741a7913e8301c1128ec8e80b8009406e5047e6e1ef";
    var publicKey = "04e33993f0210a4973a94c26667007d1b56fe886e8b3c2afdd66aa9e4937478ad20acfbdc666e3cec3510ce85d40365fc2045e5adb7e675198cf57c6638efa1bdb";

#if NET5_0_OR_GREATER
            var privateKeyBytes = Convert.FromHexString(privateKey);
            var publicKeyBytes = Convert.FromHexString(publicKey);
#else
            var privateKeyBytes = FromHexString(privateKey);
            var publicKeyBytes = FromHexString(publicKey);
#endif

    return ECDsa.Create(new ECParameters
    {
        Curve = ECCurve.NamedCurves.nistP256, // you'd need to know the curve before hand
        D = privateKeyBytes,
        Q = new ECPoint
        {
            X = publicKeyBytes.Skip(1).Take(32).ToArray(),
            Y = publicKeyBytes.Skip(33).ToArray()
        }
    });
}

private static byte[] FromHexString(string hex) {
    var numberChars = hex.Length;
    var hexAsBytes = new byte[numberChars / 2];
    for (var i = 0; i < numberChars; i += 2)
        hexAsBytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);

    return hexAsBytes;
}

.NET 5 onwards has built-in support for hex strings (Convert.FromHexString) in the same style as the core base64 support. If you're stuck on older versions of .NET, you’ll need to do things manually.

Source Code

You can find executable code demonstrating the above approaches on my GitHub samples repository. This code is multitargeting .NET Core 3.1 onwards and runs on both Windows and Linux.

To learn more about elliptic curve cryptography in .NET, check out some of my other articles: