Building a certificate request for a PIV private key
You have generated a key pair in, or imported a private key into, a PIV slot. Now you need a certificate. To obtain a certificate, you start by building a certificate request and sending it to a Certificate Authority (CA). The CA will build a cert and return it to you.
This document describes how to build a cert request using a key in a YubiKey PIV slot.
Note that there is also some sample code that demonstrates this. Find it in
.../Yubico.YubiKey/examples/PivSampleCode/CertificateOperations
Start with the file
SampleCertificateOperations.cs
and the method
GetCertRequest
.NET Base Class Libraries
This document describes how to create a cert request using
System.Security.Cryptography.X509Certificates.CertificateRequest
This is not the only way to create a cert request. There are commercial products available with certificate APIs. However, for this documentation (and the SDK sample code), only classes available in the .NET BCL are examined.
The CertificateRequest
constructor
To build a cert request, start by building a CertificateRequest
object. To do so, use
the constructor. There are several, but let's look at this one.
CertificateRequest(
string subjectName,
System.Security.Cryptography.RSA publicKey,
System.Security.Cryptography.HashAlgorithmName hashAlgorithm,
System.Security.Cryptography.RSASignaturePadding paddingScheme);
Subject name
A certificate is a binding between a name and a public key. So your cert request will need to let the CA know what name and public key are to be in the certificate.
There are two ways to provide the subject name, as a string
and as an instance of
System.Security.Cryptography.X509Certificates.X500DistinguishedName
. The purpose of
this document is to describe how to build a cert request when the private key is on a
YubiKey. Hence, we will not describe how to build names, either by using the string
class or the X500DistinguishedName
class. For this document, we're simply going to
use the string
string sampleName = "C=US,ST=CA,L=Palo Alto,O=Fake,CN=Fake Cert";
If you want to learn more about building a subject name, either by using a string
or an
X500DistinguishedName
,
see the .NET documentation.
Public key
When you generate a key pair on the YubiKey, a PivPublicKey
is returned. The
CertificateRequest
class needs that public key as an instance of the RSA
class.
The PivSampleCode.KeyConverter
class demonstrates how to get an RSA
object from a
PivPublicKey
. Your code might look something like this.
PivRsaPublicKey rsaPublic = pivSession.GenerateKeyPair(...);
var rsaParams = new RSAParameters();
rsaParams.Modulus = rsaPublic.Modulus.ToArray();
rsaParams.Exponent = rsaPublic.PublicExponent.ToArray();
RSA rsaPublicKeyObject = RSA.Create(rsaParams);
An RSA
object can contain a public key only or both public and private keys. Later on,
we're going to sign the cert request using the private key partner to the public key
loaded in this step. Normally, that private key would need to be loaded into this RSA
object as well, because that's the object the cert request code would use to sign the
request. But the private key partner in this case is on the YubiKey and is not allowed to
leave the device. We can't load it into this RSA
object. But we will still be able to
sign. How to do so will be described later.
HashAlgorithm
To sign using RSA is to encrypt the hash of the data to sign. That is, RSA does not operate directly on the data to sign, but rather the hash (message digest) of the data. So we need to specify which hash algorithm the cert request code should use. Your best choices are the following.
HashAlgorithmName.SHA256
HashAlgorithmName.SHA384
HashAlgorithmName.SHA512
There are other algorithms available: MD5
and SHA1
. However, researchers have found
weaknesses in them, so cryptographers universally recommend not using them any more. Use
them only for legacy systems.
Which algorithm should you use? Your application might be required to use a particular digest based on a standard or protocol. If not, then you might choose SHA-256 simply because it is the most widely used digest algorithm in RSA signatures. It is the one that will likely have no interoperability issues.
Because a longer digest does add some security in digital signatures, you might choose to use the SHA-384 with 1024-bit keys. If you try to use SHA-512, the operation will likely fail because there is not enough space in a 1024-bit block (128 bytes, the size of the signature) to contain a padded, 64-byte digest. If the RSA key is 2048 bits, you can use SHA-512.
Note that there is an algorithm SHA-224, but the .NET BCL do not support it.
PaddingScheme
Every standard that deals with RSA signatures requires the digest to be padded. There are two available in the .NET BCL.
RSASignaturePadding.Pkcs1
RSASignaturePadding.Pss
PSS is recommended over PKCS #1. It is possible to encounter a legacy system that requires PKCS #1 and does not support PSS. However, if that is not the case, it is better to use PSS.
An RSA signature is the encrypted digest. However, for security reasons, the actual data to encrypt should be the same size as (or very close to the size of) the key itself. For example, if the RSA key is 1024 bits (128 bytes), then the data to sign should also be a 128-byte block. A SHA-256 digest is only 32 bytes. Hence, to create a block to encrypt, add pad bytes. The padding scheme used should be a standard one so that the verifier can know which bytes are pad and which are the digest.
Both padding schemes supported in the .NET BCL require a minimum amount of padding. In other words, a particular digest algorithm/padding scheme/key size might not be compatible because the digest is very long and not many pad bytes are needed to complete a block. That is why it is possible you will not be able to use SHA-512 with a 1024-bit key.
Extensions
Now that you have the CertificateRequest
object built, it is possible to add extensions.
To learn how to do that, see the .NET BCL documentation.
CreateSigningRequest
When the CertificateRequest
object has all the information you want, call the
appropriate CreateSigningRequest
method.
The CreateSigningRequest
method that takes no arguments will sign the request using the
private key inside the RSA
object passed to the constructor. In this case, that object
has no private key, so we'll need to use
public byte[] CreateSigningRequest (
System.Security.Cryptography.X509Certificates.X509SignatureGenerator signatureGenerator);
We will build an X509SignatureGenerator
, which is an object that knows how to sign. The
sample code contains an example of one that uses a YubiKey to sign. See
.../Yubico.YubiKey/examples/PivSampleCode/CertificateOperations
YubiKeySignatureGenerator.cs
The CreateSigningRequest
method will do much of the work to build up the cert request,
but will call on our SignatureGenerator
to do work it can't.
X509SignatureGenerator
This is an abstract class. We need to build a subclass that implements the specified methods:
BuildPublicKey
GetSignatureAlgorithmIdentifier
SignData
We will pass an instance of our class to the CreateSigningRequest
method.
Start with a "scaffold" class.
public sealed class YubiKeySignatureGenerator : X509SignatureGenerator
{
public YubiKeySignatureGenerator() { }
protected override PublicKey BuildPublicKey() { }
public override byte[] GetSignatureAlgorithmIdentifier(HashAlgorithmName hashAlgorithm) { }
public override byte[] SignData(byte[] data, HashAlgorithmName hashAlgorithm) { }
}
BuildPublicKey
When we first built the CertificateRequest
object, we supplied the public key in the
RSA
argument. It would seem that if the object (and the CreateSigningRequest
method)
has access to the public key there should be no need for our SignatureGenerator
to be
able to provide it. But nonetheless, we need to build this method.
Fortunately, the X509SignatureGenerator
base class (the abstract class from which we
derive the class we are currently constructing) contains code that can be used to
accomplish this.
Call this static
method
// Use the RSA object and padding scheme we created for the CertificateRequest
// constructor.
X509SignatureGenerator defaultGenerator = X509SignatureGenerator.CreateForRSA(
rsaPublicKeyObject, RSASignaturePadding.Pss);
You now have an X509SignatureGenerator
object. This happens to be the default. It is
what the CreateSigningRequest()
(no arg version of this method) would use. However,
this object was built using an RSA
object that contained no private key. So it won't be
able to sign. But it will be able to build a PublicKey
.
We can update our class to take advantage of this object.
public sealed class YubiKeySignatureGenerator : X509SignatureGenerator
{
private readonly X509SignatureGenerator _defaultGenerator;
private readonly RSASignaturePaddingMode _paddingMode;
// Use the RSA object and padding scheme we created for the CertificateRequest
// constructor.
public YubiKeySignatureGenerator(RSA rsaPublicKeyObject, RSASignaturePadding paddingScheme)
{
_defaultGenerator = X509SignatureGenerator.CreateForRSA(rsaPublicKeyObject, paddingScheme);
_paddingMode = paddingScheme.Mode;
}
protected override PublicKey BuildPublicKey()
{
return _defaultGenerator.PublicKey;
}
public override byte[] GetSignatureAlgorithmIdentifier(HashAlgorithmName hashAlgorithm) { }
public override byte[] SignData(byte[] data, HashAlgorithmName hashAlgorithm) { }
}
GetSignatureAlgorithmIdentifier
The point of this method is to return the DER encoding of the algorithm ID of the algorithm that will be used to sign. The algID is part of the finished cert request.
Once again, the CertificateRequest
object (and the CreateSigningRequest
method) has
access to the RSA
public key, the HashAlgorithm
, and RSASignaturePadding
. It would
seem that the object has everything it needs to build the "algID". But nonetheless, we
need to build this method.
Fortunately, the _defaultGenerator
we built earlier can build the algID for us.
public sealed class YubiKeySignatureGenerator : X509SignatureGenerator
{
. . .
public override byte[] GetSignatureAlgorithmIdentifier(HashAlgorithmName hashAlgorithm)
{
return _defaultGenerator.GetSignatureAlgorithmIdentifier(hashAlgorithm);
}
public override byte[] SignData(byte[] data, HashAlgorithmName hashAlgorithm) { }
}
SignData
This is the method we will build that calls on the YubiKey to sign the data. The
CertificateRequest
object is going to pass to our method the data to sign and the hash
algorithm it is expected to use. That means we need to digest the data using the specified
algorithm, then create a signature using that digest result. We need to pad the digest as
well.
In order to sign using a YubiKey, we need a PivSession
and we need to know in which slot
the private key we're using resides.
private readonly PivSession _pivSession;
private readonly byte _slotNumber;
private readonly int _keySizeBits;
private readonly X509SignatureGenerator _defaultGenerator;
private readonly RSASignaturePaddingMode _paddingMode;
public YubiKeySignatureGenerator(
PivSession pivSession,
byte slotNumber,
RSA rsaPublicKeyObject,
RSASignaturePadding paddingScheme)
{
_pivSession = pivSession;
_slotNumber = slotNumber;
_keySizeBits = rsaPublicKeyObject.KeySize;
_defaultGenerator = X509SignatureGenerator.CreateForRSA(rsaPublicKeyObject, paddingScheme);
_paddingMode = paddingScheme.Mode;
}
protected override PublicKey BuildPublicKey()
{
return _defaultGenerator.PublicKey;
}
public override byte[] GetSignatureAlgorithmIdentifier(HashAlgorithmName hashAlgorithm)
{
return _defaultGenerator.GetSignatureAlgorithmIdentifier(hashAlgorithm);
}
public override byte[] SignData(byte[] data, HashAlgorithmName hashAlgorithm)
{
byte[] dataToSign = DigestData(data, hashAlgorithm);
dataToSign = PadRsa(dataToSign, hashAlgorithm);
return _pivSession.Sign(_slotNumber, dataToSign);
}
private byte[] DigestData(byte[] dataToDigest, HashAlgorithmName hashAlgorithm)
{
using HashAlgorithm digester = hashAlgorithm.Name switch
{
"SHA1" => CryptographyProviders.Sha1Creator(),
"SHA256" => CryptographyProviders.Sha256Creator(),
"SHA384" => CryptographyProviders.Sha384Creator(),
"SHA512" => CryptographyProviders.Sha512Creator(),
_ => throw new ArgumentException(),
};
byte[] digest = new byte[digester.HashSize / 8];
_ = digester.TransformFinalBlock(data, 0, data.Length);
Array.Copy(digester.Hash, 0, digest, 0, digest.Length);
return digest;
}
private byte[] PadRsa(byte[] digest, HashAlgorithmName hashAlgorithm)
{
int digestAlgorithm = hashAlgorithm.Name switch
{
"SHA1" => RsaFormat.Sha1,
"SHA256" => RsaFormat.Sha256,
"SHA384" => RsaFormat.Sha384,
"SHA512" => RsaFormat.Sha512,
_ => 0,
};
if (_rsaPadding.Mode == RSASignaturePaddingMode.Pkcs1)
{
return RsaFormat.FormatPkcs1Sign(digest, digestAlgorithm, _keySizeBits);
}
return RsaFormat.FormatPkcs1Pss(digest, digestAlgorithm, _keySizeBits);
}
ECC
It is possible you will want to build a cert request for an ECC key pair. In that case,
you will need a SignatureGenerator
that can sign using ECC. To do so, you will need to
build an ECC default SignatureGenerator
, there are restrictions on the size of the
digest (it must match the key size), and there is no padding.
The sample code demonstrates how to build a SignatureGenerator
that can sign using
either RSA or ECC.