PIV attestation statements
Attestation was added as a feature in version 4.3 of the YubiKey, so will not be available on earlier models.
It is possible to create an attestation statement for some of the keys generated on a YubiKey. Such a statement simply offers evidence that a private key was generated on a YubiKey. It does not say anything about who owns the YubiKey, or who signed some data using the private key, only that the key is from a YubiKey.
Note
In version 1.0.0 of the SDK, it was not possible to create an attestation statement for keys in slots 82 - 95 (retired key slots). Beginning with version 1.0.1 of the SDK it is possible to create an attestation statement for the keys in those slots.
This attestation statement is provided in the form of an X.509 certificate. What this certificate attests (or asserts, affirms) is that "the private key partner to the public key in this certificate was generated on a YubiKey."
Note that any private key generated on the YubiKey, using the PIV application, is not allowed to leave the device. In addition, the YubiKey will not create an attestation statement for an imported key. Hence, it is possible to verify that a private key operation was performed (or will be performed) by the YubiKey and only the YubiKey.
If the YubiKey is version 5.0 or later, then the attestation statement also contains the YubiKey's serial number. That means anyone examining the certificate can be confident that not only was the private key generated by a YubiKey, but a specific YubiKey.
Uses
Generally, a certificate binds a name with a public key. That allows verifiers to tie private key operations to a specific entity. For example, if a public key verifies a digital signature, and that public key is certified by a certificate (and the certificate verifies, see chaining below), then the signature is said to belong to the name on the certificate.
However, an attestation statement does not tie a name to public key, it ties a process to a public key. The cert has a name, such as "YubiKey PIV Attestation 9A", but that is the name on all attestation statements of all YubiKeys, when the slot attested is 9A. It is not the name of the owner of the public key, but rather the name associated with the process used to generate the public key.
How is that useful? Some applications want correspondents to create signatures for authentication or authorization purposes. But they want to know whether the private key used to sign is a software key or a hardware key. A hardware key is much more secure, and with an attestation statement, the application can have more confidence that the key has not been stolen because someone used a weak password or left a monitor unattended.
Hardware keys are not completely secure, after all someone can steal your YubiKey. But it is much harder to mount a remote attack on someone using a YubiKey. The attacker must have access to the physical device. And if the attacker does indeed steal your YubiKey, they must enter your PIN to create a signature (assuming you did not turn off the PIN requirement for signing). Because the PIN and PUK block after too many wrong values are entered, it is likely a brute force attack will fail.
Chaining
Someone verifying your attestation statement will need to know that the certificate you provide is indeed valid. To verify that cert, verify its contents and signature. The public key used to verify a cert's signature comes from the certificate issuer, also known as the Certificate Authority (CA). You will find that public key in the CA's cert. To verify that you do indeed have the correct CA cert, verify using its issuer's public key, found in the CA's issuer's certificate. And so on.
This process ends at a root cert. This is a certificate you obtain outside the normal certificate distribution system. Your application will likely have some sort of cert store with trusted root certs. These trusted certs are those that you obtained in a trusted manner, and believe them to be valid. Most trusted certs are "baked into" software distributions. For example, web browsers come with various root certs already loaded. If you trust the software and its distribution, you trust those root certs. Most software packages that use root certs allow you to add new roots.
The process of verifying a certificate by using the cert of the issuer, and verifying the issuer's cert by using the cert of the issuer's issuer, and so on until reaching a root cert, is known as chaining.
Root Cert
|
|
[CA Cert]
(There may or may not be a CA cert
between the Root and Attestation Cert)
|
|
Attestation Cert
(Slot F9)
(from PivSession.GetAttestationCertificate)
|
|
Attestation Statement
(created using PivSession.CreateAttestationStatement)
(a cert)
To obtain the YubiKey CA and root certs, visit the Yubico Developer's PIV Attestation website. Using the appropriate cert from this location, you can verify an attestation statement built by a YubiKey.
Terminology
Start with the private key in an attestable slot (9A, 9C, 9D, 9E, and 82 - 95). This key has a partner public key.
On every YubiKey (since version 4.3) is an attestation key and certificate. These are in slot F9.
When an attestation statement is built, the private key in the attestable slot is the "attested key". A new certificate is created. This new certificate is called the attestation statement.
- Slot 9A, 9C, 9D, 9E, 82 - 95:
- public and private key pair
- private key is the attested key
- attestation statement:
- an X.509 certificate
- subject key is the public key partner to the attested key
- signed by the attestation key
- chains to the attestation cert
- Slot F9:
- public and private key pair
- private key is the attestation key
- attestation certificate:
- an X.509 certificate
- subject key is the public key partner to private key in F9
- chains to a root
Replacing the attestation key and cert
It is possible to replace the attestation key and cert that is loaded onto the YubiKey at manufacture. However, this is something very few users will want to do. If you are wondering whether you might like to replace the attestation key and cert, it is almost a certainty that you should not. But there are companies who have wanted the attestation statement to chain up to their own root instead of a one from Yubico. Hence, the YubiKey allows for it.
Note that if you replace the Yubico key and cert, there is no way to recover these original values, they will be gone for good. So use this method with caution.
The replacement key must be either RSA-2048, ECC-P256, or ECC-P384, and there are some restrictions on the certificate. YubiKeys before version 5 did allow 1024-bit RSA keys as attestation keys, but to make your application work for all YubiKeys, you should never use a 1024-bit RSA key as an attestation key.
There are two ways to replace the key: generate a new attestation key or import a key.
First, you will almost certainly NOT want to generate the attestation key. If you generate a new key, you will get a public key at the time the key is generated and will have to build a certificate for that public key, then import the certificate. That attestation key will only work for the YubiKey on which it was generated.
Generally, the same attestation key and cert is loaded onto many YubiKeys. It is much more efficient to get one attestation certificate for thousands of YubiKeys than to get thousands of attestation certificates.
If you have a private key and certificate outside the YubiKey, you can import both. However, note that the attestation certificate must be built with these restrictions:
- It must be X.509 version 2 or 3
- The encoded validity dates must be less than 66 bytes
- The encoded
SubjectName
must be less than 1029 bytes - The total length of the certificate must be fewer than 3052 bytes
The first restriction -- the certificate cannot be version 1 -- might be the most troublesome. Very few CAs build version 1 certificates these days, so is is likely there will be no issue.
However, there is one thing to think about. It is possible the certificate building software the CA uses might build version 1 certificates by default. For example, the OpenSSL command line tool can build certificates. However, it does not allow you to pick the version number. It will build a version 1 certificate unless the cert to build will contain one or more extensions.
That is, if the certificate to build will contain no extensions, the OpenSSL software will produce a version 1 certificate. If the cert will contain at least one extension, the OpenSSL software will create a version 3 cert. The OpenSSL command line tool currently does not support building version 2 certificates.
Hence, to make sure you build a version 3 certificate using the OpenSSL command line tool,
build it with at least one extension. For example, here is a command that builds a
certificate from a cert request and the cert it builds will contain the BasicConstraints
extension.
$ openssl x509 -req -in certRequest.pem -days 3640 -CA rootcert.pem -CAkey rootkey.pem
-extfile basic.txt -CAserial rootcert.srl -out cert.pem
where basic.txt contains the BasicConstraints extension:
(the following are the contents of the file basic.txt)
basicConstraints=critical,CA:true,pathlen:1
For the second restriction, if the certificate is a valid X.509 version 3 certificate, the validity dates will be less than 64 bytes, so that should not be a problem.
For the third, most names in certificates are less than 128 bytes, so specifying the name
for your attestation cert to be less than 1029 bytes should be easy. Nonetheless, keep an
eye on the length of the name. Note that the encoded SubjectName
refers to the DER
encoding of
SEQUENCE OF RelativeDistinguishedName
If you look inside the DER encoding of a certificate, you will find a SubjectName
. If
you pull out that name as an individual entity, it will look something like this.
30 68
31 0b
30 09 06 03 55 04 06 13 02 55 53
31 0b
30 09 06 03 55 04 08 0c 02 43 41
31 12
30 10 06 03 55 04 07 0c 09 50 61 6c 6f 20 41 6c
74 6f
31 19
30 17 06 03 55 04 0a 0c 10 46 61 6b 65 20 41 74
74 65 73 74 61 74 69 6f 6e
31 1d
30 1b 06 03 55 04 03 0c 14 46 61 6b 65 20 41 74
74 65 73 74 61 74 69 6f 6e 20 32 35 36
This is a TLV (tag-length-value) with a tag of 30
, a length of 68
(decimal 104) and a
value of 31 0b ... 36
.
The length in the TLV is the value's length. The total length is 6a
(decimal 106): the
length of the value plus the length of the tag (one) plus the length of the length octets
(also one).
For a SubjectName
to be invalid for an attestation cert, it would look something like
this.
30 82 04 01
31 0b 30 09 06 03 55 04 06 13 02 55 53 31 0b 30
. . .
65 20 41 74 74 65 73 74 61 74 69 6f 6e 20 32 35
36
In this case, the tag is 30
, the length is 04 01
(decimal 1025) (the 82 indicates that
the following two bytes make up the length), and there are 1025 bytes that make up the
value. The total length of this SubjectName
is 0405
(decimal 1029): the value's length
plus the length of the tag (one) plus the length of the length octets (three).
Checking the key and cert.
Due to space and compute limitations, the YubiKey itself does not verify the inputs before loading them. That means it is possible to load bad key/cert combinations. For example, it is possible to load a cert that contains a subject key that is not the partner to the private key. In that case, the YubiKey will create attestation statements that do not verify or do not chain to a root. In other cases, the YubiKey might simply return an error when requested to build an attestation statement. Hence, you must be certain the key and cert you load are correct, and you should thoroughly test the attestation statements before deployment.
There is a method in the PivSession
class to replace the attestation key and cert.
public void ReplaceAttestationKeyAndCertificate(PivPrivateKey privateKey, X509Certificate2 certificate)
If you use this method to replace the key and cert, it will check the certificate to make sure it meets the requirement. If not, the method will throw an exception.
However, this method will not verify the cert itself, nor will it verify the validity dates. It will also NOT verify that the public key represented in the cert is indeed the partner to the private key specified. If you want to make sure they match, you will need to write your own code.
The following code is one way to make this check. It uses standard C# classes. If you have
access to a secure multi-precision arithmetic library (often called BigNum
or
BigInteger
), there is a more efficient technique. However, the standard C# BigInteger
class is not one you should use with sensitive data, so we present this technique.
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
private static bool IsMatchingKeyAndCert(PivPrivateKey privateKey, X509Certificate2 certificate)
{
if (privateKey.Algorithm == PivAlgorithm.Rsa2048)
{
return IsMatchingKeyAndCertRsa((PivRsaPrivateKey)privateKey, (RSA)certificate.PublicKey.Key);
}
return IsMatchingKeyAndCertEcc((PivEccPrivateKey)privateKey, (byte[])certificate.PublicKey.EncodedKeyValue);
}
private static bool IsMatchingKeyAndCertRsa(PivRsaPrivateKey privateKey, RSA publicKey)
{
bool returnValue = isValidCert;
// In order to build a System.Security.Cryptography.RSA object
// that contains the private key, we must provide all possible
// components: modulus, public exponent, private exponent, CRT
// info.
// We have everything needed from the publicKey (an RSA object)
// and privateKey (a PivRsaPrivateKey object) except for the
// private exponent. If you have the CRT info, you don't need the
// private exponent, so the PivRsaPrivateKey class doesn't keep
// it (and the YubiKey itself does not keep it).
// But in order to build the RSA private key-containing object we
// need to obtain the private exponent. Except we don't really.
// Although the object will not build itself without that
// component, once it's built, the object ignores it. So we can
// supply anything as the private exponent, the object will
// build, and it will operate correctly anyway. That's why we're
// using an arbitrary private exponent.
RSAParameters publicParams = publicKey.ExportParameters(false);
byte[] fakeExponent = new byte[publicParams.Modulus.Length];
byte[] modCopy = new byte[publicParams.Modulus.Length];
byte[] expCopy = new byte[publicParams.Exponent.Length];
Array.Copy(publicParams.Modulus, modCopy, publicParams.Modulus.Length);
Array.Copy(publicParams.Exponent, expCopy, publicParams.Exponent.Length);
// To determine if the public key in the cert is the partner to
// the private key, encrypt arbitrary data using that public key,
// then decrypt it using the private key. If that works, they are
// partners. Use PKCS 1 padding so we don't have to involve any
// digest algorithms.
byte[] dataToEncrypt = new byte[16];
using RandomNumberGenerator randomObject = RandomNumberGenerator.Create();
randomObject.GetBytes(fakeExponent);
fakeExponent[0] &= 0x7F;
fakeExponent[^1] |= 1;
randomObject.GetBytes(dataToEncrypt);
var rsaParams = new RSAParameters();
try
{
rsaParams.D = fakeExponent;
rsaParams.DP = privateKey.ExponentP.ToArray();
rsaParams.DQ = privateKey.ExponentQ.ToArray();
rsaParams.InverseQ = privateKey.Coefficient.ToArray();
rsaParams.P = privateKey.PrimeP.ToArray();
rsaParams.Q = privateKey.PrimeQ.ToArray();
rsaParams.Modulus = modCopy;
rsaParams.Exponent = expCopy;
using var rsaObject = RSA.Create();
rsaObject.ImportParameters(rsaParams);
byte[] encryptedData = rsaObject.Encrypt(dataToEncrypt, RSAEncryptionPadding.Pkcs1);
byte[] decryptedData = rsaObject.Decrypt(encryptedData, RSAEncryptionPadding.Pkcs1);
return MemoryExtensions.SequenceEqual<byte>(dataToEncrypt, decryptedData);
}
finally
{
CryptographicOperations.ZeroMemory(rsaParams.P);
CryptographicOperations.ZeroMemory(rsaParams.Q);
CryptographicOperations.ZeroMemory(rsaParams.DP);
CryptographicOperations.ZeroMemory(rsaParams.DQ);
CryptographicOperations.ZeroMemory(rsaParams.InverseQ);
CryptographicOperations.ZeroMemory(rsaParams.D);
}
}
private static bool IsMatchingKeyAndCertEcc(PivEccPrivateKey privateKey, byte[] publicKey)
{
bool returnValue = false;
ECCurve eccCurve = privateKey.Algorithm == PivAlgorithm.EccP256 ?
ECCurve.CreateFromValue("1.2.840.10045.3.1.7") :
ECCurve.CreateFromValue("1.3.132.0.34");
var eccParams = new ECParameters
{
Curve = (ECCurve)eccCurve
};
try
{
int coordLength = (publicKey.Length - 1) / 2;
byte[] xCoord = new byte[coordLength];
byte[] yCoord = new byte[coordLength];
Array.Copy(publicKey, 1, xCoord, 0, coordLength);
Array.Copy(publicKey, 1 + coordLength, yCoord, 0, coordLength);
eccParams.Q.X = xCoord;
eccParams.Q.Y = yCoord;
eccParams.D = privateKey.PrivateValue.ToArray();
// To determine if the public key in the cert is the partner
// to the private key, sign random data using that private
// key, then verify it using the public key. If that works,
// they are partners. Use the SignHash method so we don't
// have to involve any digest algorithms.
using var eccObject = ECDsa.Create(eccParams);
byte[] dataToSign = new byte[coordLength];
using RandomNumberGenerator randomObject = RandomNumberGenerator.Create();
randomObject.GetBytes(dataToSign);
byte[] signature = eccObject.SignHash(dataToSign);
return eccObject.VerifyHash(dataToSign, signature);
}
finally
{
CryptographicOperations.ZeroMemory(eccParams.D);
}
}