Table of Contents

Public key handling

The Yubico .NET SDK provides type-safe public key classes implementing IPublicKey for working with keys generated on or imported to YubiKeys.

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)

Public key formats

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

For example, an RSA public key consists of two integers: modulus and public exponent.

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

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

Standard curves (such as NIST P-256) can be represented by an object identifier (OID) and public point (x,y).

There is more than one standard that defines how to represent public keys. The most common definitions are SubjectPublicKeyInfo from X.509 (the certificate standard used by the vast majority of applications) and PEM (Privacy-Enhanced Mail). 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 SubjectPublicKeyInfo or the PEM "PUBLIC KEY" (which wraps a SubjectPublicKeyInfo).

SubjectPublicKeyInfo is popular because it contains algorithm information in addition to the actual key data. That is, a public 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 export public keys in SubjectPublicKeyInfo and PEM formats from public key objects generated by the YubiKey, as well as building public key objects a YubiKey can read from SubjectPublicKeyInfo and PEM.

Factory methods

PIV defines its own format of encoding public keys. However, the SDK's PIV application APIs that work with public keys require them to be instances of the PublicKey class. Hence, your application will need to be able to "convert" between SubjectPublicKeyInfo and PublicKey.

RSA public keys

// From SubjectPublicKeyInfo format
byte[] spkiBytes = // your SubjectPublicKeyInfo bytes
RSAPublicKey rsaKey = RSAPublicKey.CreateFromSubjectPublicKeyInfo(spkiBytes);

// From RSA parameters
using var rsa = RSA.Create(2048);
RSAParameters parameters = rsa.ExportParameters(false); // public only
RSAPublicKey rsaKey = RSAPublicKey.CreateFromParameters(parameters);

EC public keys

// From SubjectPublicKeyInfo format
byte[] spkiBytes = // your SubjectPublicKeyInfo bytes
ECPublicKey ecKey = ECPublicKey.CreateFromSubjectPublicKeyInfo(spkiBytes);

// From EC parameters
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
ECParameters parameters = ecdsa.ExportParameters(false); // public only
ECPublicKey ecKey = ECPublicKey.CreateFromParameters(parameters);

// From public point (0x04 || X-coordinate || Y-coordinate)
ReadOnlyMemory<byte> publicPoint = [/* your uncompressed EC point */]
ECPublicKey ecKey = ECPublicKey.CreateFromValue(publicPoint, KeyType.ECP256);

Curve25519 public keys

// From SubjectPublicKeyInfo format
byte[] spkiBytes = // your SubjectPublicKeyInfo bytes
Curve25519PublicKey curve25519Key = Curve25519PublicKey.CreateFromSubjectPublicKeyInfo(spkiBytes);

// From public point bytes
ReadOnlyMemory<byte> publicPoint = [/* your uncompressed EC point */];
Curve25519PublicKey ed25519Key = Curve25519PublicKey.CreateFromValue(publicPoint, KeyType.Ed25519);
Curve25519PublicKey x25519Key = Curve25519PublicKey.CreateFromValue(publicPoint, KeyType.X25519);

Generating key pairs

When you generate a new key pair in a YubiKey's PIV application, you are given the public key, which is returned as an instance of the PublicKey class. From this class, you can obtain important information about the key, including KeyDefinition and KeyType. Depending on the specific class, Parameters (EC and RSA) and PublicPoint (EC and Curve25519) are also included.

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

// Generate returns the public key
IPublicKey publicKey = pivSession.GenerateKeyPair(
    PivSlot.Authentication,
    KeyType.Ed25519,
    PivPinPolicy.Once,
    PivTouchPolicy.Never);

// Type-check for specific key properties
if (publicKey is Curve25519PublicKey ed25519Key)
{
    Console.WriteLine("Generated public key: " + ed25519Key.PublicPoint);
}

Exporting public keys

// Export to SubjectPublicKeyInfo format (standard)
byte[] spkiBytes = publicKey.ExportSubjectPublicKeyInfo();

// Access key-specific properties
if (publicKey is RSAPublicKey rsaKey)
{
    RSAParameters rsaParams = rsaKey.Parameters;
    byte[] modulus = rsaParams.Modulus;
    byte[] exponent = rsaParams.Exponent;
}

if (publicKey is ECPublicKey ecKey)
{
    ECParameters ecParams = ecKey.Parameters;
    ReadOnlyMemory<byte> publicPoint = ecKey.PublicPoint; // 0x04 || X || Y format
    byte[] xCoord = ecParams.Q.X;
    byte[] yCoord = ecParams.Q.Y;
}

if (publicKey is Curve25519PublicKey curve25519Key)
{
    ReadOnlyMemory<byte> publicPoint = curve25519Key.PublicPoint;
}

PEM format conversion

// Export to PEM format
byte[] spkiBytes = publicKey.ExportSubjectPublicKeyInfo();
string pemKey = "-----BEGIN PUBLIC KEY-----\n" +
                Convert.ToBase64String(spkiBytes, Base64FormattingOptions.InsertLineBreaks) +
                "\n-----END PUBLIC KEY-----";

// Import from PEM format
string pemData = // your PEM PUBLIC KEY
string base64Data = pemData
    .Replace("-----BEGIN PUBLIC KEY-----\n", "")
    .Replace("\n-----END PUBLIC KEY-----", "");
byte[] spkiBytes = Convert.FromBase64String(base64Data);
IPublicKey publicKey = RSAPublicKey.CreateFromSubjectPublicKeyInfo(spkiBytes); // or ECPublicKey, etc.
Note

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

   -----BEGIN PUBLIC KEY-----
   -----END PUBLIC KEY-----

   -----BEGIN RSA PUBLIC KEY-----
   -----END RSA PUBLIC KEY-----

   -----BEGIN EC PUBLIC KEY-----
   -----END EC PUBLIC KEY-----

Determining the algorithm when importing a public key in PEM format

If you have a byte array that contains the SubjectPublicKeyInfo, and you want to build a PublicKey, you will need to first determine the public key algorithm. Once you know the algorithm, you can use the appropriate C# class to read the encoded data (for example, RSAPublicKey, ECPublicKey, or Curve25519PublicKey ).

The algorithm is specified in the key data itself. However, the .NET Base Class Library does not have a class that can parse SubjectPublicKeyInfo 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 SubjectPublicKeyInfo 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 SubjectPublicKeyInfo.

SubjectPublicKeyInfo is defined as:

SubjectPublicKeyInfo ::=  SEQUENCE  {
     algorithm            AlgorithmIdentifier,
     subjectPublicKey     BIT STRING  }

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.
       30 len
          06 len
             OID bytes
         etc.

To get to the OID, we need to read the first 30 len, then the second 30 len, then the 06 len.

Error handling

Factory methods validate input and may throw exceptions:

try
{
    var publicKey = ECPublicKey.CreateFromValue(publicPoint, KeyType.ECP256);
}
catch (ArgumentException ex)
{
    // Handle invalid parameters, key type mismatches, or malformed data
}
catch (CryptographicException ex)
{
    // Handle invalid key format or cryptographic validation failures
}

References