A Yubico OTP (one-time password) is a unique 44-character string that is generated by the YubiKey when it is touched (while plugged into a host device over USB or Lightning) or scanned by an NFC reader.
More specifically, the OTP is generated when an OTP application slot that is configured for Yubico OTP is activated. If a YubiKey is connected to a host over USB or Lightning, slot activation occurs when the key is touched, and the duration of touch determines which slot is activated. If a YubiKey is scanned by an NFC reader, the slot that is pointed to by the OTP application's NDEF tag will activate.
The OTP is made up of various YubiKey device fields and encrypted with a unique 128-bit AES (Advanced Encryption Standard) key. Yubico OTPs look similar to the following:
You'll notice that the example OTP above only includes a subset of Latin alphabet characters. This is because Yubico OTPs are only represented by the 16 ModHex characters. When YubiKeys are connected to a host device over USB or Lightning, OTPs are communicated to the host via the HID communication protocol. The YubiKey acts like a keyboard, with the characters of the OTP being sent to the host as key presses. ModHex allows Yubico OTPs to be intrepreted correctly by hosts, regardless of the host's keyboard language configuration. For more information on how this works and why it's important, please see the articles on HID communication and ModHex.
Yubico OTPs can be used for user authentication in single-factor and two-factor authentication scenarios. In order to authenticate a user with a Yubico OTP, the OTP must be checked to confirm that it is both associated with the user account in question and valid. Verifying OTPs is the job of the validation server, which stores the YubiKey's AES key and uses it to decrypt and validate OTPs.
Off-the-shelf YubiKeys are preconfigured with a Yubico OTP in the short-press slot (for NFC-enabled YubiKeys, the NDEF tag is also pointed to the short-press slot). The OTP secrets are uploaded to Yubico's validation server (YubiCloud) at the time of manufacturing, which enables out-of-the-box OTP validation functionality over USB/Lightning and NFC. If you have an off-the-shelf YubiKey and would like to demo the two-factor OTP authentication experience, check out the Yubico Playground.
If you reconfigure the short-press slot of an off-the-shelf key, you will not be able to revert back to the original Yubico OTP configuration that was set up during manufacturing. OTP secrets cannot be extracted from the device.
The .NET SDK provides developers with the resources to build Yubico OTP configuration functionality into their applications. More specifically, the API includes functions that allow you to configure either slot with a Yubico OTP and customize how the OTP is sent to host devices.
Components of a Yubico OTP
Yubico OTPs are 44 ModHex characters long. The first 12 characters (6 bytes of binary data) represent the key's public ID--this remains constant across all OTPs generated by a single Yubico OTP configuration.
The public ID is typically the first thing that is checked when an OTP is submitted. If the OTP's public ID doesn't match the ID that is registered with a user's account, the OTP will be rejected.
Public IDs must be unique to a single Yubico OTP configuration. You may not register the same public ID with different private IDs and AES keys with the same validation server.
Off-the-shelf YubiKeys are preconfigured with Yubico OTPs that have public IDs that start with "cccc". The SDK provides the option to explicitly set the public ID to something of your choice or to use a ModHex representation of the YubiKey's serial number as the public ID. See the SDK functionality section below for more information.
The final 32 characters of the OTP represent the unique 128-bit passcode. The passcode is generated by concatenating various YubiKey fields into a 128-bit long string and encrypting the string with the YubiKey configuration's unique 128-bit AES key. These fields include the following:
- private ID (48 bits)
- session usage counter (8 bits)
- usage counter (16 bits)
- timestamp (24 bits)
- random number (16 bits)
- checksum (16 bits)
The private ID is a 6-byte/48-bit field that is unique to the Yubico OTP configuration. The validation server's key storage module (KSM) stores the configuration's AES key and private ID pair. When an OTP is decrypted by the KSM, the private ID field in the OTP is compared to the private ID in the KSM. If they do not match, the OTP is rejected.
When a Yubico OTP application slot is configured for a Yubico OTP, the private ID can be set to a specific value or generated randomly. See the SDK functionality section below for more information on how to do this with the SDK.
Session usage counter
The session usage counter is used to keep track of how many OTPs have been generated during a single "session" (i.e. a continuous period of time where the key has been powered on).
When the YubiKey is powered up (either through a physical connection to a host or by coming into contact with an NFC reader), the session usage counter (1 byte/8 bits) is initiated to zero. When a new OTP is generated while the key is still powered on, this field is incremented by one. If this field wraps from 0xff to 0, the usage counter is automatically incremented. The session usage counter is stored in little-endian format (least significant byte first).
If two OTPs are generated during the same session, the second OTP received by the validation server should have a higher session usage counter than the previous OTP. If not, the second OTP will be rejected.
Each OTP application slot has its own session usage counter. This means that if you configure both slots with a Yubico OTP, their session usage will be tracked separately. For example, if you were to generate an OTP from the short-press slot, the session usage counter for that slot would increment by 1, but the long-press slot's session usage counter would not change.
The usage counter (2 bytes/16 bits) keeps track of how many times the YubiKey has been powered on. It is non-volatile, meaning its value is preserved even when the YubiKey is powered off. The first time the YubiKey is used after a power-up, this value is incremented by 1, and the session usage counter is reset to zero. If a validation server receives an OTP that has a lower usage counter than the previously recorded counter, the OTP will be rejected.
This field has a usable range of 1 – 0x7fff. When this counter reaches 0x7fff, it stops. However, this does not render the YubiKey useless; the OTP application slot can simply be reconfigured with new Yubico OTP secrets (which need to be shared with the validation server). Every time a slot is reconfigured, the usage counter will be reset. Each OTP application slot has its own usage counter.
The usage counter is also stored in little-endian format.
The timestamp is a 3-byte/24-bit field incremented at a rate of approximately 8 Hz. After powering on, the key's internal random number generator sets the timestamp to a random value. Therefore, the timestamp does not reflect the current time. However, it can be used by the validation server to check how much time has elapsed between two OTPs received during a single session. For example, if one password is supposed to have been generated just a few seconds after the previous one, the timestamps should reflect that. But what if the timestamps show that the "first" one was generated four days after the "second" one? That is a red flag indicating the possibility of a replay attack.
The YubiCloud validation server does not use the timestamp to validate OTPs. If you are self-hosting your own validation server, you may elect to use the timestamp if you wish.
The timestamp wraps from 0xffffff to 0 without any further action. If the timestamp is used by a self-hosted validation server, this condition must be taken into account. Given an 8 Hz rate, the timestamp will wrap approximately every 24 days (given the key is continuously powered on). The field is stored in little-endian format.
The random number is a 2-byte/16-bit field that is generated by the key's random number generator for each OTP. The goal of the random number is to help guarantee that the OTP's binary ciphertext is always unique and to ensure that the OTP block size is 128 bits. This field is also stored in little-endian format.
The 128-bit AES encryption key that Yubico OTPs use requires a 128-bit block size (the block is the data being encrypted). So in a sense, the random number functions as padding that brings the block size to the necessary 128-bits.
Once all other OTP fields have been set, a 2-byte/16-bit ISO 13239 one's complement checksum is computed over the binary passcode data from the private ID to the random number and added to the end of the OTP string (in little-endian format). This checksum allows the validation server to confirm that an OTP was decrypted properly during authentication.
To verify decryption, the validation server calculates another checksum over all bytes of the OTP, including the original checksum field. If decryption was successful, this will give a fixed residual of 0xf0b8. If a different residual is computed by the validation server, the OTP will be rejected.
Yubico OTP generation
Now that we've covered the Yubico OTP components, let's look at the typical OTP generation process:
The YubiKey is plugged into a host device over USB/Lightning or comes into contact with a host's NFC reader.
The key is powered on.
The session usage counter is reset to 0, and the usage counter is incremented by 1.
The key's random number generator initializes the timestamp to a random value and begins incrementing it at a rate of 8 Hz.
A slot containing a Yubico OTP configuration is activated, prompting OTP generation.
The key collects the public ID, private ID, current session usage counter, current usage counter, and current timestamp.
The key generates the random number.
The key computes the checksum over all OTP passcode fields (private ID through random number).
The private ID, session usage counter, usage counter, timestamp, random number, and checksum are concatenated into a 128-bit string and encrypted by the 128-bit AES encryption key.
If the key is connected to a host over USB/Lightning, the key translates the bits of the full OTP (public ID + encrypted passcode) to the HID usage IDs of the ModHex characters that represent those bits. These HID usage IDs are collected into HID usage reports.
If the key is in contact with an NFC reader, the key translates the bits of the OTP to the binary UTF codes of the ModHex characters that represent those bits. The OTP (as UTF codes) is added to an NDEF message as a text string or URI, depending on the key's NDEF tag configuration.
The key sends the OTP to the host (through HID usage reports or an NDEF message).
After an OTP is generated, the session usage counter is incremented by 1.
Authentication with a Yubico OTP
In order to validate Yubico OTPs, a central validation server is needed. Validation servers store the OTP configuration secrets (private ID and AES key) and keep track of the usage counter and session usage counter from previous valid OTPs.
When a Yubico OTP is submitted during the user authentication process, the OTP is routed to the validation server by your application, which uses the appropriate AES key to decrypt the OTP. After decrypting, the private ID, usage counter, and session usage counter are used to check the OTP's validity.
When a key's OTP application slot is configured with a Yubico OTP, the OTP secrets must be shared with the validation server and the configuration's public ID must be registered with a user account of an application the user wishes to authenticate to.
It is up to the application developer to correctly handle OTP account registration and communication with the validation server.
Yubico provides a network of validation servers around the world known collectively as YubiCloud. During manufacturing, YubiKeys are configured with a Yubico OTP (in the short-press slot), and the OTP secrets are uploaded to YubiCloud. This enables out-of-the-box OTP authentication.
To test authentication with YubiCloud, submit a Yubico OTP from your YubiKey here.
To integrate YubiCloud validation into your application, you must add calls to YubiCloud to verify OTPs. To get started, get a YubiCloud API key.
Self-hosted validation servers
Yubico strongly recommends using YubiCloud. If for some reason YubiCloud is not suitable for your application, you may host your own validation server.
Validation servers are composed of two major components:
- verification server
- key storage module (KSM)
The KSM stores OTP secrets (AES keys and private IDs) and performs OTP decryption. The verification server validates decrypted OTPs. Yubico provides a reference architecture for the validation server and KSM in the YubicoLabs GitHub repository. Please note that these architectures have been deprecated and are no longer supported. At this time, Yubico does not offer an on-premises service.
If you are interested in hosting your own validation server to use within your company, please reach out to customer support. Yubico has the ability to manufacture company-specific keys with a different public ID prefix (off-the-shelf YubiKeys use the "cccc" prefix). These keys can be pre-configured with Yubico OTPs, and the secrets will be provided to you for uploading to your self-hosted server.
Step-by-step OTP authentication process
Once a YubiKey's private ID and AES key have been shared with a validation server and the key has been registered with a user account of an application that uses that particular validation server, OTP authentication works as follows:
For two-factor authentication, the user enters their username and password on the login screen of the application.
The user is prompted to insert their YubiKey into their device or scan their NFC-enabled key with their host device's NFC reader. If the key is physically connected to the host, the green LED of the YubiKey will begin to flash.
For a key connected over USB/Lightning, the user must touch the YubiKey to generate and submit an OTP. If the key was scanned by an NFC reader, an OTP is generated automatically. After the OTP is generated, the key sends it to the host via the appropriate communication protocol (HID for USB/Lightning connections or NDEF for NFC).
For specifics about OTP generation, please see the previous section.
The YubiKey's public ID is checked by the application to verify that it is associated with the user account.
The application sends the OTP to the validation server.
The OTP is received by the validation server and passed to the Key Storage Module (KSM).
The KSM uses the public ID to locate the corresponding OTP secrets.
The KSM uses the AES key to decrypt the OTP.
The KSM calculates a checksum over all bytes of the OTP, including the original checksum field. If decryption was successful, this will give a fixed residual of 0xf0b8. If a different residual is computed, the OTP is rejected.
The KSM checks that the private ID of the OTP matches the private ID in the KSM. If they do not match, the OTP is rejected.
If decryption was successful and the private ID is correct, the KSM passes the validity of the OTP and the usage counters back to the validation server.
The validation server checks the session usage counter and usage counter against those from the last valid OTP from that YubiKey. If the usage counter is lower than the previously recorded usage counter, the OTP is rejected. If it is the same as the previous counter, the session usage counter must be higher than the previous session usage counter for the OTP to be valid, otherwise the OTP is rejected. If the usage counter is higher than the previous usage counter, the OTP is accepted as valid.
The validation server reports the OTP validity to the application.
If the OTP is valid and its public ID is registered with the user account, the user is logged in to the application.
Yubico OTP security
The security of Yubico OTPs relies on three major areas:
- the cryptographic strength of the OTP
- the write-only properties of YubiKey OTP configuration data
- the integrity of the validation server
Yubico OTPs use a 128-bit AES encryption key. This means that there are 2^128 or 3.4028237e+38 possible key combinations. Trying to guess the correct key through a brute-force attack with the world's current computational power would take billions of years.
For more information on the Advanced Encryption Standard (AES), please see the 2001 specification.
YubiKey configuration data is write-only. This means that an OTP slot may be reconfigured with a new AES key and private ID, but the current configuration (as well as any previous configurations) may not be extracted from the device.
Validation server integrity
The validation server is responsible for storing secrets (the AES keys and private IDs) as well as keeping track of the usage counter and session usage counter for each key-ID pair.
If the server isn't tracking the usage counter and session usage counter properly, this leaves the door open for attackers to find and use previously generated OTPs.
Similarly, OTPs emitted outside the login process present a security vulnerability. Until a subsequent OTP is sent to a validation server for authentication, these "unused" OTPs may be stolen and submitted for verification by an attacker.
To protect against this scenario, we recommend regularly "burning" OTPs, meaning that you generate and submit an OTP for verification with the goal of simply updating the server's usage counter/session usage counter. If you use YubiCloud, you can do this through the demo webpage. Companies that use self-hosted validation servers should provide a similar facility to burn OTPs.
The SDK's Yubico OTP functionality is rooted in the ConfigureYubicoOtp method. This method allows you to configure one of the OTP application slots with a Yubico OTP. After configuration, that slot will generate a Yubico OTP every time it is activated.
With ConfigureYubicoOtp, you have the ability to provide your own AES key and private ID via UseKey() and UsePrivateId() or randomly generate those credentials via GenerateKey() and GeneratePrivateId(). Similarly, you may set the public ID to an explicit value of your choosing with UsePublicId() or use your YubiKey's serial number as the public ID with UseSerialNumberAsPublicId().
Generated credentials will need to be shared with the validation server before they are cleared from memory. There is no way to extract credentials from the YubiKey after configuration. If using YubiCloud, you can upload secrets using this form. Note that credentials will need to be converted from bytes to ASCII characters before they can be submitted through the key upload form. If your private ID and/or public ID has already been registered with YubiCloud, you will need to redo the configuration with new credentials. Therefore, you may only configure a slot with the key's serial number as the public ID (via UseSerialNumberAsPublicId()) once if you are uploading those configuration secrets to YubiCloud.
The YubiKey also allows you to control how the OTP is sent to a host, depending on the intended use case. You can set a time delay between characters of the OTP as they are sent to a host device with Use10msPacing() and Use20msPacing(). Similarly, you can add a 500ms delay after sending the fixed part of the OTP (the 12-character public ID of the key) and/or the 32-character unique passcode of the OTP with AppendDelayToFixed() and AppendDelayToOtp(), respectively.
You can also add additional keystrokes as needed for your intended application with SendTabFirst() (sends a tab before the OTP characters), AppendTabToFixed() (sends a tab after the public ID of the OTP), and AppendCarriageReturn() (sends an Enter key after the full OTP has been sent to a device).
For a full list of the methods in the ConfigureYubicoOtp class, please see the API documentation.
For an example of how to use ConfigureYubicoOtp(), please see How to program a slot with a Yubico OTP credential.