如何验证 yubico otp 响应

Howto verify a yubico otp response

我想在我的申请中使用 yubico OTP 作为第二个因素。 Yubico OTP 文档:https://developers.yubico.com/OTP/

以下是一个通过控制台读取OTP的c#(.net 6)例子(你需要按下U盘上的按钮,然后otp被用作rest服务请求的参数)。此示例基于版本 2.0 或验证服务 (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");

这一切工作正常,甚至 returns status=OK 当我使用由 yubikey 生成的有效 OTP 时作为响应的一部分。

问题:我可以使用我的 yubico api 私钥以某种方式验证响应吗?如果不是,则此身份验证似乎容易受到中间人攻击。

附带问题:该请求需要一个 api id,我什至通过 https://upgrade.yubico.com/getapikey/ 创建了一个,但我可以使用任何 id 并且该请求都一样。这是设计使然吗?如果是,首先这个 id 参数的意义是什么?

实际上有这方面的文档:https://developers.yubico.com/OTP/Specifications/OTP_validation_protocol.html

必须为参数创建 hmac-sha1,然后必须将此签名添加为附加参数。

//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}";

这样的 url 看起来像这样(签名是 h 参数的一部分):

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

完整示例

以下是如何在 .net 应用程序中使用 Yubikey OTP 的完整self-contained示例(包括签名验证)

执行以下步骤:

  • 为请求创建参数
    • 创建随机数
    • 从 yubikey 获取 OTP
    • 使用 API 密钥对参数签名
  • 从 yubico 调用验证服务
  • 检查otp
    • 检查return状态
    • 比较returned 签名与内置签名
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");


(我显然没有足够的声誉,所以我只允许 post 'answers')

@Manuel

我在整个网络上看到了这个示例以各种语言显示,但是 none 对我来说正确。
return 状态始终是 status=OK,无论我使用的是什么物理密钥。
我可以使用一盒 50 个 yubikeys 5 nfc,如果我使用你的例子,状态就可以了。
如果我篡改了 ID,我将收到类似 NO_SUCH_CLIENTBAD_SIGNATURE 等的回复
所以某些参数匹配很重要,但实际的 OTP 不是其中的一部分。
我可以在 https://upgrade.yubico.com/getapikey

注册一个 ID 和 Secret

在您的代码中进行验证,它将是 status=OK
然后拿一个全新的yubikey试试,状态还是可以的。
我尝试使用同事的 yubikey 验证我的 ID 和 Secret,你可以猜到,status=OK.

所以我真正要证明的唯一一件事就是我拥有 'a' yubikey。