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
- RFC 5208 - PKCS#8 Private Key format
- RFC 7748 - Curve25519 and Curve448
- RFC 7468 - PEM encoding
- SDK PIV integration tests
- SDK unit tests for additional usage examples