如何验证 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_CLIENT
或 BAD_SIGNATURE
等的回复
所以某些参数匹配很重要,但实际的 OTP 不是其中的一部分。
我可以在 https://upgrade.yubico.com/getapikey
注册一个 ID 和 Secret
在您的代码中进行验证,它将是 status=OK
。
然后拿一个全新的yubikey试试,状态还是可以的。
我尝试使用同事的 yubikey 验证我的 ID 和 Secret,你可以猜到,status=OK
.
所以我真正要证明的唯一一件事就是我拥有 'a' yubikey。
我想在我的申请中使用 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_CLIENT
或 BAD_SIGNATURE
等的回复
所以某些参数匹配很重要,但实际的 OTP 不是其中的一部分。
我可以在 https://upgrade.yubico.com/getapikey
在您的代码中进行验证,它将是 status=OK
。
然后拿一个全新的yubikey试试,状态还是可以的。
我尝试使用同事的 yubikey 验证我的 ID 和 Secret,你可以猜到,status=OK
.
所以我真正要证明的唯一一件事就是我拥有 'a' yubikey。