YubiKey OTP Implementation

 

In this article, I will be focussing on YubiKeys that are made by a Company called Yubico.

Yubico offer a number of different styles of hardware based on two-factor authentication devices. In my case, I have been implementing the YubiKey 4, using their OTP (one time password) authentication API.

Firstly, it is important to understand the process in which YubiKeys are authenticated. The general procedure can be seen in the diagram below.

 

As you can see, an extra field is added to the login page of a website and then upon submission of the form, the OTP (one time password generated by the YubiKey) value is sent to Yubico servers where a response is returned to say whether it has been successfully validated. A further security check can then be performed on the response to validate that it is authentic.

In order to get up and running, you will need to generate a Client ID and Secret API Key using the Yubico Get API Key service (https://upgrade.yubico.com/getapikey/).

The client ID is the only value that will be passed to Yubico (using HTTP GET) along with the OTP in the URL format shown below:

https://api.yubico.com/wsapi/verify?id=[CLIENT_ID]&opt=[OTP_VALUE]

 

Sending the OTP to Yubico is the easy part, but validating the returned response requires some extra care. As you will need to extract the hash from the response before each key-pair is sorted alphabetically, concatenated into one single string before being hashed using HMAC SHA1.

The response validation algorithm is as follows:

  1. Extract the hash key-pair from the response
  2. The remaining key-pairs are sorted alphabetically by key name
  3. The keys and their values are concatenated in the format keyname=keyvalue&keyname=keyvalue…..
  4. The concatenated key-pairs are then hashed using HMAC SHA1 using the API key
  5. The key-pairs hash is then compared to the hash in the response.

The validation protocol can be seen in the documentation here… https://developers.yubico.com/yubikey-val/Validation_Protocol_V2.0.html

Example Code

The code below is a simplified way one might process the Yubico API response. As you can see, the secret API key (generated here) and the response are passed through the constructor of the YubiResponse class. This will populate the object’s properties with the values from the response. Additionally, you can then use the IsValidResponse boolean property to check whether the response is valid.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace YubicoAuth
{
    public class YubiResponse
    {
        #region Properties
        
        public string ApiSecretKey { get; private set; }
        public string ApiResponse { get; private set; }

        public string Hash { get; private set; }
        public string Time { get; private set; }
        public string Status { get; private set; }
        public string PreHashString { get; private set; }
   
        public bool IsValidResponse { get; private set; }

        #endregion


        #region Constructors
        public YubiResponse(string apiResponse, string apiSecretKey)
        {
            this.ApiResponse = apiResponse;
            this.ApiSecretKey = apiSecretKey;

            var apiKeyValues = ConvertResponseToDictionary(this.ApiResponse);
            PreHashString = ConcatKeysForValidation(apiKeyValues);

            Hash = apiKeyValues["h"];
            Time = apiKeyValues["t"];
            Status = apiKeyValues["status"];

            IsValidResponse = IsResponseValid(PreHashString, Hash, ApiSecretKey);
        }
        #endregion


        #region "Private Methods"     
        private static Dictionary<string, string> ConvertResponseToDictionary(string apiResponse)
        {
            var respValues = apiResponse.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries)
                .Select(lineItem => lineItem.Split(new char[] { '=' }, 2))
                .ToDictionary(respKey => respKey[0].Trim(), respValue => respValue[1].Trim());

            return respValues;
        }


        private static string ConcatKeysForValidation(Dictionary<string, string> keyPairs)
        {
            string concatPairs = string.Join("&", keyPairs
                .Where(kp => kp.Key.ToLower() != "h") // Don't include the returned hash value
                .OrderBy(kp => kp.Key)
                .Select(kp => kp.Key + "=" + kp.Value).ToArray());

            return concatPairs;
        }


        private static bool IsResponseValid(string preHashString, string apiResponseHash, string apiKey)
        {
            byte[] keyBytes = Convert.FromBase64String(apiKey); // Decode API KEY from base 64
            HMACSHA1 hmac = new HMACSHA1(keyBytes);	// Use the decoded API KEY as our key
            
            ASCIIEncoding encoding = new ASCIIEncoding();
            byte[] hashBytes = hmac.ComputeHash(encoding.GetBytes(preHashString));

            string hashResp64 = Convert.ToBase64String(hashBytes);
            return apiResponseHash == hashResp64;
        }
        #endregion
    }
}

 

Post Categories