The following function is for receiving a Lemon Squeezy webhook request:
public ActionResult lsHook(LemonsqueezyWebhook lemonsqueezyWebhook)
{
...
}
It works as expected in terms of the payload data. Lemon Squeezy uses X-Signature to help confirm the request is actually from Lemon Squeezy, not a phishing site. The following is their code example:
$secret = '[SIGNING_SECRET]'; // from your webhook settings
$payload = file_get_contents('php://input');
$hash = hash_hmac('sha256', $payload, $secret);
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
if (!hash_equals($hash, $signature)) {
throw new Exception('Invalid signature.');
}
Here is my C# code to do the same in ActionResult lsHook(LemonsqueezyWebhook lemonsqueezyWebhook)
:
StringBuilder sb = new StringBuilder("X-Signature")
if (Request.Headers.AllKeys.Contains("X-Signature"))
{
foreach (string sValue in Request.Headers.GetValues("X-Signature"))
{
sb.Append(Environment.NewLine).Append(sValue);
}
Request.InputStream.Seek(0, SeekOrigin.Begin);
string sPayload = new StreamReader(Request.InputStream).ReadToEnd();
using (var sha256 = SHA256.Create())
{
byte[] payloadBytes = Encoding.UTF8.GetBytes(sPayload);
byte[] secretBytes = Encoding.UTF8.GetBytes("mysecret");
// Combine payload and secret
byte[] data = payloadBytes.Concat(secretBytes).ToArray();
// Hash the combined data
byte[] hash = sha256.ComputeHash(data);
sb.Append(Environment.NewLine + "hash: ").Append(BitConverter.ToString(hash).Replace("-", "").ToLower());
}
}
Here is what I got from one request for sb:
dab5a3c5f205f60aa09ab00098f67917e8fcc6b05e64d510b656d82f35bca97c
hash: 3ee77ac08f050e1338ca3a4a8c0162eb8ff9aa1fb7d89418881492a52da1cc37
They do not match. sPayload
looks correct. It is the json object. Could anyone offer a hint about the possible cause?
Update:
After var sha256 = SHA256.Create()
was replace with var sha256 = HMACSHA256.Create()
, I got the following:
10de6aa343a95017309a7e7d3b683fb35afa4be9f67c41c2d87ae1f9872e0914
hash: 9dceac34439623bff6549b2dd264672e0cc20910
The bug in the posted code was that the C# code only calculated the SHA256 value instead of the HMAC/SHA56 value.
After fixing this, the key was not specified correctly and the Create()
method was applied, which requires the specification of the digest, which was missing.
Determining an HMAC/SHA256 can be implemented on .NET as follows using HMASCSHA256
:
using System;
using System.Security.Cryptography;
using System.Text;
...
byte[] secret = Encoding.UTF8.GetBytes("some secret");
byte[] payload = Encoding.UTF8.GetBytes("some payload");
using (var hmacSha256 = new HMACSHA256(secret))
{
byte[] hash = hmacSha256.ComputeHash(payload);
Console.WriteLine(Convert.ToHexString(hash)); // 22A2E09F97E933DB48BA6EF24C6BE11A5A10024BD9A6A18E662E94BF3C35F257
}
The key can of course also be set later:
...
using (var hmacSha256 = new HMACSHA256())
{
hmacSha256.Key = secret;
byte[] hash = hmacSha256.ComputeHash(payload);
Console.WriteLine(Convert.ToHexString(hash)); // 22A2E09F97E933DB48BA6EF24C6BE11A5A10024BD9A6A18E662E94BF3C35F257
}
...
Note that HMACSHA256.Create()
does not work on .NET 5+ (System.PlatformNotSupportedException: This platform does not allow the automatic selection of an algorithm).
On .NET Framework it works, but returns an HMAC/SHA1 value by default:
...
using (var hmacSha256 = HMACSHA256.Create())
{
hmacSha256.Key = secret;
byte[] hash = hmacSha256.ComputeHash(payload);
Console.WriteLine(ByteArrayToString(hash)); // FF81B9A37F9DA6EA0DE2CFF78216B697722E9A08 // for ByteArrayToString() see https://stackoverflow.com/a/311179/9014097
}
...
This is probably the cause of the shorter hash in your environment. In order for HMAC/SHA256 to be used, it must be explicitly specified:
...
using (var hmacSha256 = HMACSHA256.Create("HMACSHA256"))
{
hmacSha256.Key = secret;
byte[] hash = hmacSha256.ComputeHash(payload);
Console.WriteLine(ByteArrayToString(hash)); // 22A2E09F97E933DB48BA6EF24C6BE11A5A10024BD9A6A18E662E94BF3C35F257 // for ByteArrayToString() see https://stackoverflow.com/a/311179/9014097
}
...
The reason is that HMACSHA256.Create()
calls HMAC.Create()
, and the latter requires a specification of the digest, otherwise it defaults to HMAC/SHA1, s. here.
Since new HMACSHA256(secret)
works in all environments, it is probably most robust to use this (in accordance with the example in the documentation).