Table of Contents

Private key handling

The Yubico .NET SDK supports importing and exporting private keys in standard formats using type-safe factory methods. All private key classes implement IPrivateKey and provide secure memory handling.

Supported key types

  • RSA keys: 1024, 2048, 3072, 4096-bit
  • EC keys: NIST P-256, P-384 curves
  • Curve25519 keys: Ed25519 (signing), X25519 (key agreement)

Private key formats

One of the unfortunate problems of public key cryptography is the myriad ways to represent private keys. Part of this is due to the fact that different algorithms have different elements.

For example, an RSA private key can consist of three, five, or eight integers:

3-integer RSA key 5-integer RSA key 8-integer RSA key
modulus prime P modulus
public exponent prime Q public exponent
private exponent exponent P private exponent
exponent Q prime P
coefficient prime Q
exponent P
exponent Q
coefficient

On the other hand, an "Fp" Elliptic Curve (EC) private key consists of the following elements:

EC private key component EC private key subcomponent
curve prime
order
coefficients (a, b, c)
base point (x, y)
public point x coordinate
y coordinate
private value

Standard curves (such as NIST P-256) can be represented by an object identifier (OID), public point (x,y), and a private value. In some cases, just the OID and private value are needed as the public point can be computed from the curve parameters and private value.

There is more than one standard that defines how to represent private keys. The most common definitions are PrivateKeyInfo from PKCS #8 (Public Key Cryptography Standard #8) and PEM (Privacy-Enhanced Mail). PKCS #8 is now an internet standard (RFC 5208). PEM (RFC 7468) was created to describe how to use public key cryptography to build secure email, but it has elements that turned out to be useful to cryptography in general, including its representation of keys.

Fortunately, there is some overlap. The vast majority of applications will use the PKCS #8 PrivateKeyInfo or the PEM "PRIVATE KEY" (which wraps a PrivateKeyInfo).

PrivateKeyInfo is popular because it contains algorithm information in addition to the actual key data. That is, a private key in this format contains an AlgorithmIdentifier specifying the algorithm and any parameters as well as the key data specific to that algorithm.

There are C# classes that will build or parse these structures, although it will still require some work on your part. This page will show how to build a private key object the YubiKey can read from PrivateKeyInfo and PEM formats. Note that a YubiKey will never return a private key, so there will be no need to convert from a YubiKey-formatted private key to a PrivateKeyInfo or PEM format.

Factory methods

PIV does not define its own format of encoding private keys, but Yubico has defined an encoding that is very similar to the PIV public key format. However, the SDK's PIV application APIs that work with private keys require them to be instances of the PrivateKey class. Hence, when importing a private key into a YubiKey, your application will need to be able to "convert" from PrivateKeyInfo or PEM to PrivateKey.

RSA private keys

// From PKCS#8 PrivateKeyInfo format
byte[] pkcs8Bytes = // your PKCS#8 encoded key
RSAPrivateKey rsaKey = RSAPrivateKey.CreateFromPkcs8(pkcs8Bytes);

// From RSA parameters
using var rsa = RSA.Create(2048);
RSAParameters parameters = rsa.ExportParameters(true);
RSAPrivateKey rsaKey = RSAPrivateKey.CreateFromParameters(parameters);

// From CRT parameters only
var crtParameters = new RSAParameters
{
    P = // prime P,
    Q = // prime Q,
    DP = // exponent P,
    DQ = // exponent Q,
    InverseQ = // coefficient
};
RSAPrivateKey rsaKey = RSAPrivateKey.CreateFromParameters(crtParameters);

EC private keys

// From PKCS#8 PrivateKeyInfo format
byte[] pkcs8Bytes = // your PKCS#8 encoded key
ECPrivateKey ecKey = ECPrivateKey.CreateFromPkcs8(pkcs8Bytes);

// From EC parameters
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
ECParameters parameters = ecdsa.ExportParameters(true);
ECPrivateKey ecKey = ECPrivateKey.CreateFromParameters(parameters);

// From private scalar value
ReadOnlyMemory<byte> privateValue = // your private scalar
ECPrivateKey ecKey = ECPrivateKey.CreateFromValue(privateValue, KeyType.ECP256);

Curve25519 private keys

// From PKCS#8 PrivateKeyInfo format
byte[] pkcs8Bytes = // your PKCS#8 encoded key
Curve25519PrivateKey ed25519Key = Curve25519PrivateKey.CreateFromPkcs8(pkcs8Bytes);

// From private key bytes
ReadOnlyMemory<byte> privateKeyBytes = // your 32-byte private key
Curve25519PrivateKey ed25519Key = Curve25519PrivateKey.CreateFromValue(privateKeyBytes, KeyType.Ed25519);
Curve25519PrivateKey x25519Key = Curve25519PrivateKey.CreateFromValue(privateKeyBytes, KeyType.X25519);

Importing to YubiKey

using var pivSession = new PivSession(yubiKey);
pivSession.KeyCollector = yourKeyCollector;
pivSession.AuthenticateManagementKey();

// Import any IPrivateKey implementation
pivSession.ImportPrivateKey(
    PivSlot.Authentication,
    privateKey,
    PivPinPolicy.Once,
    PivTouchPolicy.Never);

Exporting private keys

// Export to PKCS#8 format
byte[] pkcs8Bytes = rsaKey.ExportPkcs8PrivateKey();
byte[] pkcs8Bytes = ecKey.ExportPkcs8PrivateKey();

// Access key-specific properties
RSAParameters rsaParams = rsaKey.Parameters;
ECParameters ecParams = ecKey.Parameters;
ReadOnlyMemory<byte> curve25519Bytes = curve25519Key.PrivateKey;

PEM format conversion

// Export to PEM format
byte[] pkcs8Bytes = privateKey.ExportPkcs8PrivateKey();
string pemKey = "-----BEGIN PRIVATE KEY-----\n" +
                Convert.ToBase64String(pkcs8Bytes, Base64FormattingOptions.InsertLineBreaks) +
                "\n-----END PRIVATE KEY-----";

// Import from PEM format
string pemData = // your PEM PRIVATE KEY
string base64Data = pemData
    .Replace("-----BEGIN PRIVATE KEY-----\n", "")
    .Replace("\n-----END PRIVATE KEY-----", "");
byte[] pkcs8Bytes = Convert.FromBase64String(base64Data);
IPrivateKey privateKey = RSAPrivateKey.CreateFromPkcs8(pkcs8Bytes); // or ECPrivateKey, etc.
Note

When importing a private key in PEM format, there are a number of possible header and footer combinations, including the following:

   -----BEGIN PRIVATE KEY-----
   -----END PRIVATE KEY-----

   -----BEGIN RSA PRIVATE KEY-----
   -----END RSA PRIVATE KEY-----

   -----BEGIN EC PRIVATE KEY-----
   -----END EC PRIVATE KEY-----

Determining the algorithm when importing a private key in PEM format

To build a PrivateKey when importing from PEM, you will need to first determine the private key algorithm. Once you know the algorithm, you can use the appropriate C# class to read the encoded data (for example, RSAPrivateKey, ECPrivateKey, or Curve25519PrivateKey ).

The algorithm is specified in the key data itself. However, the .NET Base Class Library does not have a class that can parse PrivateKeyInfo and build the appropriate object. The only methods that can read this encoding are in classes for the specific algorithms. That is, the RSA class can read PrivateKeyInfo only if the input data is an RSA key, the ECDsa class can read it only if the input data is an ECC key, etc.

One possible workaround would be to supply the encoded key to the RSA class and if it works, we have an RSA key. If it does not work, give the encoded key to the ECDsa class. However, if the RSA class gets an encoded key that is not RSA, it throws an exception, and using exceptions to determine code flow is not best practice.

To determine the algorithm of an imported key, we need to open up the encoding and read the object identifier (OID) of the AlgorithmIdentifier. And to find the OID, we need to decode the DER encoding of PrivateKeyInfo.

PrivateKeyInfo is defined as:

PrivateKeyInfo ::= SEQUENCE {
        version                   Version,
        privateKeyAlgorithm       AlgorithmIdentifier,
        privateKey                PrivateKey,
        attributes           [0]  IMPLICIT Attributes OPTIONAL }

Version ::= INTEGER

AlgorithmIdentifier ::=  SEQUENCE  {
        algorithm                 OBJECT IDENTIFIER,
        parameter                 ANY DEFINED BY algorithm OPTIONAL  }


This means that the DER encoding will look something like the following:

    30 len  // The len octets might be one, two, or three bytes long.
       02 01 00
       30 len
          06 len
             OID bytes
         etc.

In this example, to get to the OID, we need to read the first 30 len, then the INTEGER, then the second 30 len, then the 06 len.

Secure memory handling

All private key classes implement secure cleanup and disposal patterns:

// Using disposable pattern (recommended)
using (var privateKey = RSAPrivateKey.CreateFromPkcs8(pkcs8Bytes))
{
    // Use the private key
    pivSession.ImportPrivateKey(PivSlot.Authentication, privateKey, PivPinPolicy.Once, PivTouchPolicy.Never);
} // Sensitive data automatically cleared

// Explicit cleanup
ECPrivateKey privateKey = null;
try
{
    privateKey = ECPrivateKey.CreateFromPkcs8(pkcs8Bytes);
    // Use the private key
}
finally
{
    privateKey?.Clear(); // Securely zero sensitive data
}

Error handling

Factory methods validate input and may throw exceptions:

try
{
    var privateKey = Curve25519PrivateKey.CreateFromValue(keyBytes, KeyType.X25519);
}
catch (CryptographicException ex)
{
    // Handle invalid key format or bit clamping violations
}
catch (ArgumentException ex)
{
    // Handle invalid parameters or key type mismatches
}

References