I am currently using Forge Webhooks API to handle different events that might occur on a project. Everything works fine, except the payload signature check.
The reason why I want to check the payload is because the callback will end up on my API and I want to reject all requests that do not come from Forge's webhook service.
Steps I followed:
Add (register) secret key (token) on Forge. API Reference
Trigger an event that will eventually call my API for handling it.
Validating signature header. Followed this tutorial.
PROBLEM!!! My computedSignature is different from the signature received from Forge.
My C# code looks like this:
private const string SHA_HASH = "sha1hash";
var secretKeyBytes = Encoding.UTF8.GetBytes(ForgeAuthConfiguration.AntiForgeryToken);
using var hmac = new HMACSHA1(secretKeyBytes);
var computedHash = hmac.ComputeHash(request.Body.ReadAsBytes());
var computedSignature = $"{SHA_HASH}={computedHash.Aggregate("", (s, e) => s + $"{e:x2}", s => s)}";
For one example, Forge's request has this signature header: sha1hash=303c4e7d2a94ccfa559560dc2421cee8496d2d83
My C# code computes this signature: sha1hash=3bb8d41c3c1cb6c9652745f5996b4e7f832ca8d5
The same AntiForgeryToken was sent to Forge at step 1
Ok, I thought my C# code is broken, then I tried this online HMAC generator and for the given input, result is: 3bb8d41c3c1cb6c9652745f5996b4e7f832ca8d5 (same as C#)
Ok, maybe the online generator is broken, I tried their own code in node js and this is the result:
I have 3 ways of encrypting the SAME body using the SAME key and I get the SAME result every time. BUT those results are DIFFERENT from the signature provided by Forge, resulting in failing the check and rejecting a valid request...
Does anyone know what is happening with that signature?
Why is it different from my result if I follow their tutorial?
How are you validating your requests?
The code below is working at my side. Could you give it a try if it helps?
[HttpPost]
[Route("api/forge/callback/webhookbysig")]
public async Task<IActionResult> WebhookCallbackBySig()
{
try
{
var encoding = Encoding.UTF8;
byte[] rawBody = null;
using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
{
rawBody = encoding.GetBytes(reader.ReadToEnd());
}
var requestSignature = Request.Headers["x-adsk-signature"];
string myPrivateToken = Credentials.GetAppSetting("FORGE_WEBHOOK_PRIVATE_TOKEN");
var tokenBytes = encoding.GetBytes(myPrivateToken);
var hmacSha1 = new HMACSHA1(tokenBytes);
byte[] hashmessage = hmacSha1.ComputeHash(rawBody);
var calculatedSignature = "sha1hash=" + BitConverter.ToString(hashmessage).ToLower().Replace("-", "");
if (requestSignature.Equals(calculatedSignature))
{
System.Diagnostics.Debug.Write("Same!");
}
else
{
System.Diagnostics.Debug.Write("diff!");
}
}
catch(Exception ex)
{
}
// ALWAYS return ok (200)
return Ok();
}
If this does not help, please share with your webhook ID (better send email at forge.help#autodesk.com). We will ask engineer team to check it.
Related
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?
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");
(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.
I would like to convert the authenticated REST request of JS (below) to C# code - using RestSharp lib (as shown) I seems not building the signature correctly. Please help..
Rest response keep response with "Invalid API Key" (which the key/secret are confirmed correct)
// sample code using the crypto lib to build the RESTFUL request
const crypto = require('crypto')
const request = require('request')
const apiKey = 'test'
const apiSecret = 'test'
const url = 'version/auth/abc'
const nonce = Date.now().toString()
const body = {}
const rBody = JSON.stringify(body)
// try build the RESTFUL signature here using crypto lib and use sha384
// Not quite sure what's the update does here though.
let signature = `/api/${url}${nonce}${rBody}`
signature = crypto
.createHmac('sha384', apiSecret)
**.update(signature)**
.digest('hex')
// all code above is fine. And this is the sample code only, and trying to do something same in C# and have tried the following. I believe is the way create the "signature" issue.
I have written the C# code below, but not working, please kindly point out, I guess is the signature building is incorrect somehow.
private uint64 _nonce = UnixTimestamp.CurrentTimeInMillis;
public uint64 MyNonce()
{
return ++_nonce;
}
private string MySignature(string jsonData)
{
byte[] data = Encoding.UTF8.GetBytes(jsonData);
_hasher = _hasher ?? new HMACSHA384(Encoding.UTF8.GetBytes(PrivateApiSecret));
byte[] hash = _hasher.ComputeHash(data);
return GetHexString(hash);
}
// try to build the request below in a method.
// only show method body below:
// using the RestSharp client to build Restful req.
var client = new RestClient(new Uri("the API address goes here"));
var request = new RestRequest(string.Format("auth/abc"), Method.POST);
// try do something similar and serialize to json in order to build the signature.
Dictionary emptyBody= new Dictionary();
string url = "version/auth/abc";
string rawBody = JsonConvert.SerializeObject(emptyBody);
string sign = $"/api/{url}{MyNonce()}{rawBody}";
string sign64= Convert.ToBase64String(Encoding.UTF8.GetBytes(sign));
string signature = MySignature(sign64);
// add the key and signature above to the header of reqest.
request.AddHeader("nonce", GetNonce());
request.AddHeader("apikey", PrivateApiKey);
request.AddHeader("signature", signature);
// well, you don't need me to explain this line don't you?
var response = request.Execute(....);
Hope this is clear enough. Thanks in advance.
I'm hoping someone can help. I have been asked to write a test application to consume a Web API.
The method I'm trying to consume, has the following signature:
[Transaction]
[HttpPost]
[Route("api2/Token/")]
public ApiToken Token(Guid companyId, DateTime dateTime, string passCode)
I've written a simple C# console application. However whatever I send to the API returns with a 404 error message and I can't see what my issue is.
My code to consume the API is as follows:
_client.BaseAddress = new Uri("http://localhost:1390");
_client.DefaultRequestHeaders.Accept.Clear();
_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var companyId = Guid.Parse("4A55A43A-5D58-4245-AD7C-A72300A69865");
var apiKey = Guid.Parse("FD9AEE25-2ABC-4664-9333-B07D25ECE046");
var dateTime = DateTime.Now;
var sha256 = SHA256.Create();
var bytes = Encoding.UTF8.GetBytes(string.Format("{0}:{1:yyyyMMddHHmmss}:{2}", companyId, dateTime, apiKey));
var hash = sha256.ComputeHash(bytes);
var sb = new StringBuilder();
foreach (var b in hash)
{
sb.Append(b.ToString());
}
try
{
Console.WriteLine("Obtain an authorisation token");
var response = await _client.PostAsJsonAsync("api2/Token/", new { companyId = companyId, dateTime = dateTime, passCode = sb.ToString() });
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
All examples I've googled seem to post an object to a Web API method that accepts an object. Is it possible to post multiple simple types?
I don't think it's possible, from the documentation (https://learn.microsoft.com/en-us/aspnet/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api)
They've stated that
If the parameter is a "simple" type, Web API tries to get the value
from the URI.
For complex types, Web API tries to read the value from the message body
You can try to use uri parameters instead
var response = await _client.PostAsJsonAsync("api2/Token/{companyId}/{dateTime}/{sb.ToString()}");
For various reasons I need to be able to make Chef API requests in C#. I've followed the guides here (Header specification) and here (Bash example) but have reached a dead end. Every request I send comes back 401 Unauthorized with the content
{"error":["Invalid signature for user or client 'myuser'"]}
I have also configured Knife locally to use Fiddler as a HTTP proxy so I can inspect Knife HTTP requests and have copied the visible headers as accurately as possible, but naturally I cannot see the canonical header it generated nor the one the server expects.
However, I have confirmed in this way that the hashes I'm generating for the content (empty) and path are the same as Knife is generating.
Here's my code. Loading the RSA private key from PEM format is using an extension method taken from Christian Etter's blog - run out of links sorry.
const string path = "/cookbooks"
const string basePath = "https://chefserver.internal:443";
var timestamp = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ssZ");
var method = "GET";
var clientName = "myuser";
var hashedPath = ToBase64EncodedSha1String(path);
var hashedBody = ToBase64EncodedSha1String(String.Empty);
var canonicalHeader = String.Format("Method:{0}\nHashed Path:{1}\nX-Ops-Content-Hash:{2}\nX-Ops-Timestamp:{3}\nX-Ops-UserId:{4}",
method, hashedPath, hashedBody, timestamp, clientName);
var privateKey = File.ReadAllText("C:\\chef\\myuser.private.pem");
string signature;
byte[] rawData;
using (var rsa = new RSACryptoServiceProvider())
{
rsa.PersistKeyInCsp = false;
rsa.LoadPrivateKeyPEM(privateKey);
using (var sha1 = new SHA1CryptoServiceProvider())
{
rawData = rsa.SignData(Encoding.UTF8.GetBytes(canonicalHeader), sha1);
signature = Convert.ToBase64String(rawData);
}
}
var client = new HttpClient();
var message = new HttpRequestMessage();
message.Method = HttpMethod.Get;
message.RequestUri = new Uri(basePath + path);
message.Headers.Add("Accept", "application/json");
message.Headers.Add("Host", "chefserver.internal:443");
message.Headers.Add("X-Chef-Version", "11.12.4");
message.Headers.Add("X-Ops-Timestamp", timestamp);
message.Headers.Add("X-Ops-Sign", "version=1.0;");
message.Headers.Add("X-Ops-Userid", clientName);
message.Headers.Add("X-Ops-Content-Hash", hashedBody);
message.Headers.Add("User-Agent", "Chef Knife/11.4.0 (ruby-1.9.2-p320; ohai-6.16.0; x86_64-darwin11.3.0; +http://opscode.com)");
var currentItem = new StringBuilder();
var i = 1;
foreach (var l in signature)
{
currentItem.Append(l);
if (currentItem.Length == 60)
{
message.Headers.Add("X-Ops-Authorization-" + i, currentItem.ToString());
i++;
currentItem = new StringBuilder();
}
}
message.Headers.Add("X-Ops-Authorization-" + i, currentItem.ToString());
var response = await client.SendAsync(message);
var content = await response.Content.ReadAsStringAsync();
And the helper
private string ToBase64EncodedSha1String(string input)
{
return
Convert.ToBase64String(new SHA1CryptoServiceProvider().ComputeHash(Encoding.UTF8.GetBytes(input)));
}
I had a similar problem and found that the .NET crypto libraries did not support what was needed to make this work. I ended up using the BouncyCastle crypto libraries to accomplish this.
I made a simple C# class library that wraps this up. Take a look at that here: https://github.com/mattberther/dotnet-chef-api
I'm not super familiar with C# crypto, but the docs on SHA1CryptoServiceProvider seem to show that is is signing a SHA1 hash. Chef doesn't use signed hashes, you actually need to do an RSA signature on canonicalHeader itself.
SignData(Byte[], Object): Computes the hash value of the specified byte array using the specified hash algorithm, and signs the resulting hash value.
Is there a null or pass-through hash you can use?
I am trying to use the REST interface of AWS S3 for a web service which stores and retrieves file pieces in a simmilar way git does (via hash and a directory system based off of it). I am using the RestSharp client library to make these calls, as the AWS SDK is out of the question (the web service is actually required to work with AWS-like stores such as Hitachi HDS) and in general, as more storage platforms would be added, it was felt a standardised method would be best to perform over-the-wire communication.
The problem is that RestSharp may be adding some extra payload, as S3 is crying about having more than one data element to save.
The following code is the core storage logic, and it should be noted I am using Ninject to handle any dependancies.
public bool PutBytesInStore(string piecehash, byte[] data)
{
string method = "POST";
string hash;
using (var sha1 = new SHA1CryptoServiceProvider())
{
hash = Convert.ToBase64String(sha1.ComputeHash(data));
}
string contentType = "application/octet-stream";
string date = new DateTime().ToString("{EEE, dd MMM yyyy HH:mm:ss}");
string file = string.Format("pieces/{0}/{1}/{2}", piecehash.Substring(0, 2), piecehash.Substring(0, 6),
piecehash);
//Creating signature
var sBuilder = new StringBuilder();
sBuilder.Append(method).Append("\n");
sBuilder.Append(contentType).Append("\n");
sBuilder.Append(date).Append("\n");
sBuilder.Append(hash).Append("\n");
sBuilder.Append(file).Append("\n");
var signature = Convert.ToBase64String(new HMACSHA1(Encoding.UTF8.GetBytes(_password)).ComputeHash(Encoding.UTF8.GetBytes(sBuilder.ToString())));
_request.Method = Method.POST;
_request.AddFile(piecehash, data, piecehash);
_request.AddHeader("Date", date);
_request.AddHeader("Content-MD5", hash);
_request.AddHeader("Authorisation", string.Format("AWS {0}:{1}", _identifier, signature));
var response = _client.Execute(_request);
//Check responses for any errors
var xmlResponse = XDocument.Parse(response.Content);
switch (response.StatusCode)
{
case HttpStatusCode.Forbidden:
ErrorCodeHandler(xmlResponse);
break;
case HttpStatusCode.BadRequest:
ErrorCodeHandler(xmlResponse);
break;
case HttpStatusCode.Accepted:
return true;
default:
return false;
}
return false;
}
The problem lies with the response sent, which reads;
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>InvalidArgument</Code>
<Message>POST requires exactly one file upload per request.</Message>
<ArgumentValue>0</ArgumentValue>
<ArgumentName>file</ArgumentName>
<RequestId>SomeRequest</RequestId
<HostId>SomeID</HostId>
</Error>
The AWS API seems pretty sparse on this message, and I cant quite seem to be able to figure out why the RestSharp client would be adding more than two files to the payload.
Any help is greatly appreciated.
Its because of webkitboundary. pleas try your stuff on postman - webkitboundary is very important on uploading.