PIV data objects
The YubiKey's PIV application has space for storing certain elements other than keys and certificates. Most of these elements are defined by the PIV standard, but there are also Yubico-defined items. It is also possible for an app to store its own data in its own locations.
A data object is made up of a tag and data. The tag is simply a number and the data is different for each tag. That is, a particular number will be defined as a PIV DataTag, and associated with it is a set of elements that are combined into a single blob of data following a specific encoding.
The DataTags
There are three classes of DataTag in the YubiKey:
- PIV standard-defined tags
- Yubico-defined tags
- undefined tags
On a YubiKey, any number between 0x005F0000
and 0x005FFFFF
(inclusive) can be a valid
DataTag. In addition, there are two numbers not in that range that are valid DataTags:
0x0000007E
and 0x00007F61
.
The following table lists the numbers the PIV standard defines as DataTags (see also the table of PIV tags in the article on PIV commands).
Table 1A: PIV standard-defined DataTags
Number | Name | PIN required for read |
---|---|---|
0x0000007E | DISCOVERY | No |
0x00007F61 | BITGT | No |
0x005FC101 | Card Auth (cert) | No |
0x005FC102 | CHUID | No |
0x005FC103 | Fingerprints | Yes |
0x005FC104 | -unused- | No |
0x005FC105 | Auth (cert) | No |
0x005FC106 | Security | No |
0x005FC107 | CCC | No |
0x005FC108 | Facial Image | Yes |
0x005FC109 | Printed | Yes |
0x005FC10A | Signature (cert) | No |
0x005FC10B | Key Mgmt (cert) | No |
0x005FC10C | Key History | No |
0x005FC10D - 0x005FC120 | Retired (certs) | No |
0x005FC121 | Iris | Yes |
0x005FC122 | SM Signer (cert) | No |
0x005FC123 | PC Ref Data | No |
This next table lists the numbers Yubico defines as DataTags (see also the table of Yubico tags in the article on PIV commands).
Table 1B: Yubico-defined DataTags
Number | Name | PIN required for read |
---|---|---|
0x005FFF00 | Admin Data | No |
0x005FFF01 | Attestation Cert | No |
0x005FFF10 | MSCMAP | No |
0x005FFF11 - 0x005FFF15 | MSROOTS | No |
Finally, these are the numbers a YubiKey will accept as a DataTag, but currently have no specific meaning or data assigned to them. None of them require PIN verification in order to read the contents.
Table 1C: Undefined DataTags
Number range | Count | PIN required for read |
---|---|---|
0x005F0000 - 0x005FC100 | over 6 million possible numbers | No |
0x005FC124 - 0x005FFEFF | over 6 million possible numbers | No |
0x005FFF02 - 0x005FFF0F | 14 numbers | No |
0x005FFF16 - 0x005FFFFF | 223 numbers | No |
It is possible for an application to store whatever information it wants on a YubiKey under an undefined DataTag. However, there are space limitations. It is possible to store at most approximately 3,052 bytes under any single undefined DataTag, and the total space on a YubiKey for all storage is about 51,000 bytes.
The Data
Associated with each DataTag is a specified set of elements that make up the data, along with a definition of its encoding. The encoding is a TLV structure. TLV stands for "tag-length-value". So there is a DataTag for the data itself, specifying where, in the YubiKey, the object will be stored. Then there are tags used to encode the data itself.
The YubiKey itself will enforce only one part of the encoding, the initial tag and length. Most elements are encoded as
53 length
something
There are two exceptions: Discovery and BITGT, see the entry on commands.
The YubiKey verifies that the data for a data object sent in has the leading 53
tag (or
the two exceptions) with a correct length, but other than that, it does not check the
encoding. However, the SDK itself makes sure any input data follows the defined encoding.
For example, if you want to store CHUID data in the CHUID storage area, the SDK can
encode it for you if you use the
CHUID class. But if you use the
GetDataCommand, you must make sure the
data is properly encoded. If you want to store some other data in the CHUID area, not
encoded as defined, you will have to use a different tool.
The encoding definitions are specified in the table of PIV tags.
Reading and writing data objects
In the SDK, there are two ways to read data into and write data out of these storage locations:
PivSession ReadObject
and WriteObject
These methods require a subclass of PivDataObject. Each subclass is a representation of a data object. It will know what data it holds and how to Encode and Decode it.
For example, the CHUID class represents the PIV standard's Cardholder Unique ID. It contains properties for each element of a CHUID:
- FASC Number
- GUID
- Expiration Date
It knows how to encode these three elements into a single byte array following the PIV standard, and how to decode a CHUID encoding into the three elements.
The Write
method will be able to take the data out of the PivDataObject
it is given
and store it in the appropriate location on the YubiKey. The Read
method will be able to
retrieve the requested data from the YubiKey and return the appropriate PivDataObject
object containing that data. For example,
using (var pivSession = new PivSession(yubiKey))
{
var collectorObj = new SomeKeyCollector();
pivSession.KeyCollector = collectorObj.KeyCollectorDelegate;
KeyHistory history = pivSession.ReadObject<KeyHistory>();
if (history.IsEmpty)
{
// There was no KeyHistory data on the YubiKey
// Code to handle this case here.
}
DisplayResults(
history.OnCardCertificates, history.OffCardCertificates, history.OffCardCertificateUrl);
using (var pivSession = new PivSession(yubiKey))
{
var collectorObj = new SomeKeyCollector();
pivSession.KeyCollector = collectorObj.KeyCollectorDelegate;
// Build a KeyHistory to store.
var history = new KeyHistory();
history.OnCardCertificates = 1;
history.OffCardCertificates = 2;
history.OffCardCertificateUrl = new Uri("file://user/certs");
pivSession.WriteObject(history);
}
Currently there are PivDataObjects
for the following DataTags:
- CHUID
- CCC
- Key History
- Admin Data
- Pin-Protected Data (a special case, see below)
If you need to store/retrieve other elements, use GET DATA
and PUT DATA
. Yubico will
add more Data Objects based on customer demand.
The data stored and IsEmpty
Lets look at Key History as an example.
The KeyHistory data object is specified by the PIV standard to contain three things:
- number of on-card certificates
- number of off-card certificates
- URL where the off-card certificates can be found
The KeyHistory class contains properties for each of these elements.
Suppose you call
using KeyHistory keyHistory = pivSession.ReadObject<KeyHistory>();
Upon return, look at the
IsEmpty property. If it is
true
, then there was no KeyHistory
data on the YubiKey. It also means that the data at
the other properties (OnCardCertificates
, etc.) is meaningless. Sure, if you access the
OnCardCertificates
, it will say zero. But because the object is empty, that value is not
necessarily accurate.
If IsEmpty
is false
, then the Read operation was able to find Key History data on the
YubiKey, decode it, and set the new KeyHistory
object with the data it found.
Look at the properties, this is what had been written to the YubiKey. Maybe the
OnCardCertificates
is 2
. But it can also be zero. But now because we know that there
was indeed data on the YubiKey in the Key History storage area, we know that number
reflects what was stored there.
There is also a property
public Uri? OffCardCertificateUrl { get; set; }
// Note that the `Uri` class is a reference type, so `Uri?` means it is a
// "nullable reference type". It is NOT `Nullable<Uri>`. That is only
// possible with value types, such as `ReadOnlyMemory<T>`.
Check to see if it is null. If so, then there was no URL. The standard specifies that it is possible the Key History data has no URL.
if (!(keyHistory.OffcardCertificateUrl is null))
{
ProcessUrl(keyHistory.OffcardCertificateUrl);
}
The data in the storage location is simply what some application has set it to. It is not placed there by the YubiKey. For example, suppose there are four PIV key slots on a YubiKey that have both keys and certificates. The YubiKey itself will not set the Key History data object. If you read the Key History, it will be empty.
Now suppose an application sets the Key History, and says there are two
OnCardCertificates
. The YubiKey is not going to check the input against the contents of
the slots. Even though there are four certificates on the card, the Key History will be
set to two.
Writing data
When you create a new instance of a PivDataObject
, it starts out as empty, IsEmpty
is
true
. The contents of the other properties are meaningless, although they might start
out as zero or null. Some properties are fixed (e.g. see the CHUID FASC number) so their
initial value is correct, even if an object is empty.
If you tried to encode or Write this object (see Encode and Write), you would get an exception.
When you set one of the properties, the object is no longer empty. For example,
using var adminData = new AdminData();
// At this point, adminData.IsEmpty is true.
adminData.PinProtected = true;
// At this point, adminData.IsEmpty is false.
Now you can Encode or Write. Because you have not set the Salt
nor the PinLastUpdated
,
the encoding won't include those elements.
The encoded ADMIN DATA is
53 length
80 length
81 01
--optional bit field--
82 length
--optional salt--
83 length
--optional PinLastUpdated time
Hence, the encoding with a bit field but
no salt and no time value is the following.
53 05
80 03
81 01
02
It is possible to set a property to "no contents" and the object will not be empty. For example,
using var pinProtected = new PinProtectedData();
// IsEmpty is true;
pinProtected.ManagementKey = null;
// IsEmpty is now false, the object is not empty, even though
// we set it to contain no management key.
byte[] encoding = pinProtected.Encode();
// This will produce an output with no data
// 53 04
// 88 02
// 89 00
Using an alternate DataTag
It is possible to store data specified by a sppecific DataTag under an alternate numer.
That is, there are specific DataTags defined for specific data constructions. For example,
there is a DataTag for CHUID (0x005FC102
), and specific data formatted following a
specific TLV construction. However, if you want to store CHUID data under an alternate
DataTag (it will still be the CHUID data formatted following the CHUID definition), you
can set the DataTag.
See the DataTag property.
You will likely never have a use case in your application for an alternate DataTag, but this feature is available for those rare cases when it can be useful. For example, someone might want to use a specific CHUID for one application, and a different CHUID for a second application. Hence, there could be two CHUIDs stored on a single YubiKey, one under the CHUID DataTag and one under an alternate tag.
Note that it can be dangerous to store data under an alternate DataTag, because some tags
require the PIN to read and others do not. For example, if you store some sensitive data
in the PRINTED storage area, PIN verification is required to retrieve it. But suppose you
store that data under an alternate tag, one that is currently undefined (such as
0x005F0010
). That storage area does not require the PIN to retrieve the data.
The tables above include a column indicating whether a DataTag requires the PIN for reading or not.
The SDK makes it easy to store data under a different DataTag, as long as there is a PivDataObject class for the tag. For example, there is a class CardholderUniqueId for the CHUID DataTag. In this case, to store the CHUID data under a different number, set the DataTag property.
It is not possible to change the
If you change the DataTag
, then the data specified in the object, including its format,
will be stored under a different tag. For example, if you build a
CardholderUniqueId object and leave
the DataTag
alone, then when you store the data it will be stored in the YubiKey's CHUID
storage area. But if you build the object and then change the DataTag
to, say,
0x005F0010
(one of the undefined numbers), when you store the data, it will be the CHUID
data formatted according to the PIV specification for CHUID, but stored in the
0x005F0010
storage area.
PinProtectedData
This is an unusual PivDataObject
because there is no specified Data Object called
"PIN-Protected Data". It is used to store a specific set of elements in the PRINTED
storage area. Currently, only the YubiKey's management key is included in the set.
It would be possible to create a PivDataObject
for PRINTED, just as there are classes
for CHUID, CCC, and so on. There is none, however, because the PRINTED storage area is
really designed for "credit-card-like" smart cards, storing the information printed on the
card itself (and other data). But the YubiKey is not such a smart card, so there is no
such printed information and no need to use the PRINTED storage area.
Because this is an "unused" Data Object, Yubico uses it to store the management key if the customer wants a "PIN-only" YubiKey. If, in the future, Yubico decides to store more PIN-protected data, this will be extended (see below).
PIN-only
There are many PIV operations that require management key authentication in order to execute. For example, a YubiKey will not generate a new private key unless the management key has been authenticated in the current session.
In order to authenticate, the management key must be entered. But that might not be an easy operation. The PIV PIN will almost certainly be "keyboard characters" and is at most eight characters long. It is easy for an application to pop up a window to enter the PIN, and it is not too hard for a user to remember an 8-character value.
But the management key is 24 binary bytes. How does a user enter binary data? And very few people could remember such a long value.
Some applications prefer to configure the YubiKey to PIN-only. There are two ways to do that on a YubiKey: PIN-derived and PIN-protected.
Warning
PIN-derived should never be used and is provided only for backwards compatibility.
PIN-protected simply stores the management key in the PRINTED storage area and retrieves it whenever it is needed. The YubiKey will not return the data inside PRINTED unless the PIN has been verified in the current session. In this way, the management key is PIN-protected.
In order to authenticate the management key, verify the PIN, retrieve the data from the PRINTED storage area, decode, and use the resulting 24 bytes to authenticate.
Note that there are PivSession
methods that will do all this work for you. Most
applications will never use the PinProtectedData
class directly.
Encoding
The PIV standard specifies the encoding format of the data stored in PRINTED. When storing the management key, however, another format is used. In this way, it is possible to know whether the data in the Data Object is PRINTED or PIN-protected management key. If the data in the storage area looks like the following
53 length
01 length
--data--
02 length
--data--
03 length
--data--
04 length
--data--
05 length
--data--
06 length
--data--
07 length
--data--
08 length
--data--
FE 00
then this is PRINTED data. If, however, it is encoded as the following
53 1C
88 1A
89 18
--management key--
then it is the PIN-protected management key.
Using PinProtectedData
To store something using the PinProtectedData
class create an instance, load the data
into the appropriate property and call PivSession.WriteObject
.
To read the PIN-protected data out of a YubiKey, call the PivSession.ReadObject
method.
The result is an object. Look at the properties you are interested in to see any data
retrieved.
using var pinProtect = new PinProtectedData();
pinProtect.ManagementKey = mgmtKeyData;
pivSession.WriteObject(pinProtect);
PinProtectedData getPinProtect = pivSession.ReadObject<PinProtectedData>();
if (!(getPinProtect.ManagementKey is null))
{
// process mgmt key.
}
Future extensions
For now, the only thing that can be PIN-protected using this construction is the
management key. Hence, there is a property in the PinProtectedData
class called
ManagementKey
. In the future, however, if Yubico decides to store some other data in the
PRINTED storage area, this class will be updated with other properties.