Howto verify a yubico otp response

762 views Asked by At

I want to use yubico OTP as a second factor in my application. Yubico OTP documentation: https://developers.yubico.com/OTP/

The following is a c#(.net 6) example which reads the OTP via console (You need to press the button on the usbstick, then the otp is used as parameter for the rest service request). This sample is based on version 2.0 or the verify service (https://api.yubico.com/wsapi/2.0/verify)

using System.Security.Cryptography;

//Sample for validating OTP based on https://developers.yubico.com/OTP/OTPs_Explained.html
//Sample request: https://api.yubico.com/wsapi/2.0/verify?otp=vvvvvvcucrlcietctckflvnncdgckubflugerlnr&id=87&timeout=8&sl=50&nonce=askjdnkajsndjkasndkjsnad

// The yubico api clientid.
// You can open an api key here: https://upgrade.yubico.com/getapikey/
string yubicoCredentialClientId = "87";
// This is currently not required. Should be used to verify the response but its unclear whether this is possible or not.
// string yubicoCredentionPrivateKey = "";

string yubikeyValidationUrl = $"https://api.yubico.com/wsapi/2.0/verify?";
string nonce = "";

//Create a nonce
using (var random = RandomNumberGenerator.Create())
{
    var tmpNonce = new byte[16];
    random.GetBytes(tmpNonce);
    nonce = BitConverter.ToString(tmpNonce).Replace("-", "");
}

//Get the OTP from yubikey
System.Console.WriteLine("Press yubikey button and then enter");
var otp = Console.ReadLine();
System.Console.WriteLine(otp);
string validationParameter = $"otp={otp}&id={yubicoCredentialClientId}&nonce={nonce}";

HttpClient client = new HttpClient();
var url = $"{yubikeyValidationUrl}{validationParameter}";
System.Console.WriteLine(url);
var result = client.GetAsync(url).Result;

System.Console.WriteLine(result.StatusCode);
string respnse = result.Content.ReadAsStringAsync().Result;
System.Console.WriteLine(respnse);
if (respnse.ToLower().Contains("status=ok"))
    System.Console.WriteLine("OTP succsessful validated");
else
    System.Console.WriteLine("OTP invalid");

This all works fine and even returns status=OK as part of the response when i use a valid OTP generated by the yubikey.

Question: Can i somehow validate the response using my yubico api private key? If not, it seems this authentication would be vulnerable to a man in the middle attack.

Side question: The request requires an api id and i even created one via https://upgrade.yubico.com/getapikey/ but i can just use any id and the request works all the same. Is this by design? If yes, what it the point of this id parameter in the first place?

2

There are 2 answers

1
Manuel On BEST ANSWER

There is actually documentation for this: https://developers.yubico.com/OTP/Specifications/OTP_validation_protocol.html

A hmac-sha1 must be created for the parameters and then this signature must be added as an additional parameter.

//Create the signature based on https://developers.yubico.com/OTP/Specifications/OTP_validation_protocol.html
//Prepare the parameters to be signed (Ordered alphabetically)
string signatureParameters = $"id={yubicoCredentialClientId}&nonce={nonce}&otp={otp}";

//Create the key based on the api key string
byte[] base64AsByte = Convert.FromBase64String(yubicoCredentionPrivateKey);

string signature = "";
using (var hmac = new HMACSHA1(base64AsByte))
{
    //Create the hmacsha1
    var signatureAsByte = hmac.ComputeHash(Encoding.UTF8.GetBytes(signatureParameters));
    signature = Convert.ToBase64String(signatureAsByte);
}
//Add the signature 
signatureParameters+=$"&h={signature}";

Such an url then looks like this(The signature is part of the h parameter):

https://api.yubico.com/wsapi/2.0/verify?id=42&nonce=5FB3D5377640BA3FB8955AF98D6B71EC&otp=foobar&h=XXVw+vqc3k//qFGG6+WbP96xXis=

Complete example

The following is a complete self-contained example howto use the Yubikey OTP in a .net application (Including validation of the signatures)

The followings steps are performed:

  • Create the parameters for the request
    • Create a nonce
    • Get OTP from yubikey
    • Sign the parameter using the API key
  • Call the verify service from yubico
  • Check otp
    • Check return status
    • Compare returned signature with built signature
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;

//Sample for validating OTP based on https://developers.yubico.com/OTP/OTPs_Explained.html
//Sample request: "https://api.yubico.com/wsapi/2.0/verify?id=87&nonce=44D4185490BA8E77E58A38A98CF501E9&otp=cccccxxxvulhlletkijhrtifrintlerfbnbhtdnikl&h=f9Ht4a08iaFQYQBI5E0XUni3Pss="
//Sample response: h=TC/RXXcVqPWkFr4JPlf29nWEnig=\r\nt=2022-04-09T18:58:34Z0336\r\notp=ccxxxxxtbbvulhlletkijhrtifrintlerfbnbhtdnikl\r\nnonce=44D41854DDDA8E77E58A38A98CF501E9\r\nsl=100\r\nstatus=OK\r\n\r\n"

// The yubico api clientid. You can open an api key here: https://upgrade.yubico.com/getapikey/
string yubicoApiClientId = "REPLACEWITHCLIENTID";
// This is currently not required. 
string yubicoApiPrivateKey = "REPLACEWITHAPIKEY";
string yubikeyValidationUrl = $"https://api.yubico.com/wsapi/2.0/verify?";
string nonce = "";

//Create the key based on the api key string
byte[] privateKey = Convert.FromBase64String(yubicoApiPrivateKey);

//Create a nonce
using (var random = RandomNumberGenerator.Create())
{
    var tmpNonce = new byte[16];
    random.GetBytes(tmpNonce);
    nonce = BitConverter.ToString(tmpNonce).Replace("-", "");
}

//Get the OTP from yubikey (usb stick)
System.Console.WriteLine("Press yubikey button");
var otp = Console.ReadLine();

//Create the signature based on https://developers.yubico.com/OTP/Specifications/OTP_validation_protocol.html
//Prepare the parameters to be signed (Ordered alphabetically)
string verifyParameters = $"id={yubicoApiClientId}&nonce={nonce}&otp={otp}";

string signature = "";
using (var hmac = new HMACSHA1(privateKey))
{
    //Create the hmacsha1
    var signatureAsByte = hmac.ComputeHash(Encoding.UTF8.GetBytes(verifyParameters));
    signature = Convert.ToBase64String(signatureAsByte);
}
//Add the signature 
verifyParameters += $"&h={signature}";

HttpClient client = new HttpClient();
var url = $"{yubikeyValidationUrl}{verifyParameters}";
System.Console.WriteLine(url);
var result = client.GetAsync(url).Result;

System.Console.WriteLine($"http statuscode: {result.StatusCode}");
string response = result.Content.ReadAsStringAsync().Result;
System.Console.WriteLine(response);
Match m = Regex.Match(response, "status=\\w*", RegexOptions.IgnoreCase);
if (m.Success)
    Console.WriteLine($"OTP Status: {m.Value}");

//Verify signature based on https://developers.yubico.com/OTP/Specifications/OTP_validation_protocol.html
//The response contains a signature (h parameter) which was signed with the same private key
//This means we can just calculate the hmacsha1 again (Without the h parameter and with ordering of the parameter)
//and then compare the returned signature with the created siganture
var lines = response.Split(new string[] { "\r\n", "\r", "\n" }, StringSplitOptions.None).ToList();
var returnedSignature = String.Empty;

string returnParameterToCheck = String.Empty;
foreach (var item in lines.OrderBy(x => x))
{
    if (!string.IsNullOrEmpty(item) && !item.StartsWith("h="))
        returnParameterToCheck += $"&{item}";

    if (!string.IsNullOrEmpty(item) && item.StartsWith("h="))
        returnedSignature = item.Replace("h=", "");
}
//Remove the first unnecessary '&' character
returnParameterToCheck = returnParameterToCheck.Remove(0, 1);

var signatureToCompare = String.Empty;
using (var hmac1 = new HMACSHA1(privateKey))
{
    signatureToCompare = Convert.ToBase64String(hmac1.ComputeHash(Encoding.UTF8.GetBytes(returnParameterToCheck)));
}

if (returnedSignature == signatureToCompare)
    System.Console.WriteLine("Signatures are equal");
else
    System.Console.WriteLine("Signatures are not equal");


5
Ronald.ZaZ On

(I apparently don't have enough reputation, so I'm only allow to post 'answers')

@Manuel

I see this example all over the web in various languages, but none of them work correctly for me.
The return status is always status=OK regardless of what physical key I'm using.
I have access to a box of 50 yubikeys 5 nfc and if I use your example, status will be OK.
If I tamper with the ID, I will get responses like NO_SUCH_CLIENT or BAD_SIGNATURE etc.
So it is kind of important that certain parameters match, but the actual OTP isn't part of that.
I can register an ID and Secret at https://upgrade.yubico.com/getapikey

Do the verification in your code and it will be status=OK.
Then grab a brand new yubikey and try it and status will still be OK.
I tried verifying with my ID and Secret using a yubikey from a colleague and, you can guess it, status=OK.

So the only thing I'm really proving is that I possess 'a' yubikey.