'C# - Calculating HMAC from using params list, json body and a secretkey?
How do I do this? I'm lost.
The header parameters:
new KeyValuePair<string, string>("checkout-account", MerchantId),
new KeyValuePair<string, string>("checkout-algorithm", "sha256"),
new KeyValuePair<string, string>("checkout-method", "POST"),
new KeyValuePair<string, string>("checkout-nonce", Guid.NewGuid().ToString()),
new KeyValuePair<string, string>("checkout-timestamp", DateTime.Now.ToString())
These I just add to the HttpClient to be sent with it, but a hmac I can not for the life of me figure out how to calculate using C#. The examples from the microsoft docs I've been looking at seems to make no sense to me.
The body:
{
"stamp": "d2568f2a-e4c6-40ba-a7cd-d573382ce548",
"reference": "3759170",
"amount": 1525,
"currency": "EUR",
"language": "FI",
"items": [
{
"unitPrice": 1525,
"units": 1,
"vatPercentage": 24,
"productCode": "#1234",
"deliveryDate": "2018-09-01"
}
],
"customer": {
"email": "[email protected]"
},
"redirectUrls": {
"success": "https://ecom.example.com/cart/success",
"cancel": "https://ecom.example.com/cart/cancel"
}
}
and a secret key for example 'WHATEVER'.
The php function from the documentation is:
function calculateHmac($secret, $params, $body = '')
{
// Keep only checkout- params, more relevant for response validation. Filter query
// string parameters the same way - the signature includes only checkout- values.
$includedKeys = array_filter(array_keys($params), function ($key) {
return preg_match('/^checkout-/', $key);
});
// Keys must be sorted alphabetically
sort($includedKeys, SORT_STRING);
$hmacPayload =
array_map(
function ($key) use ($params) {
return join(':', [ $key, $params[$key] ]);
},
$includedKeys
);
array_push($hmacPayload, $body);
return hash_hmac('sha256', join("\n", $hmacPayload), $secret);
}
Solution 1:[1]
That's a horrible spec if it's just "Here's some sample code" because it's not just about calculating a hash, it's about normalizing the request first before you calculate it. From what it looks like the process appears to be as follows
- Take the list of parameters (from headers it appears) and sort them as strings using the PHP sort rules (which appears to be case sensitive)
- Create an empty string
- For each parameter in the sorted list whose key begins with "checkout-" append it to the empty string, in the format key:value, then append a newline (\n)
- Take the request body and append it to the string. (it doesn't say what to do it the body is blank though) This is your normalized request.
- Create an hmac over the normalized request using your secret.
- Put it somewhere in your request? Who knows
If those guesses are right canonicalization and hashing will look something like this (I'm basing this on .NET 6 with a little error handling, but not much)
public static async Task<byte[]> HashRequest(HttpRequestMessage request, byte[] key)
{
return CalculateHmac256(key, await CanonicalizeRequest(request));
}
private static async Task<string> CanonicalizeRequest(HttpRequestMessage request)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
if (!request.Headers.Any())
{
throw new ArgumentException("Request has no headers");
}
var canonicalizedResourceBuilder = new StringBuilder();
SortedList<string, string> sortedCheckoutHeaderList = new();
foreach (var header in request.Headers)
{
if (header.Key.StartsWith("checkout-", StringComparison.Ordinal))
{
if (header.Value == null)
{
// Is this possible at all? Who knows, the spec doesn't say!
sortedCheckoutHeaderList.Add(header.Key, string.Empty);
}
else
{
var headerValue = header.Value.ToString();
if (headerValue == null)
{
sortedCheckoutHeaderList.Add(header.Key, string.Empty);
}
else
{
sortedCheckoutHeaderList.Add(header.Key, headerValue);
}
}
}
}
foreach (var keyValuePair in sortedCheckoutHeaderList)
{
canonicalizedResourceBuilder.Append(
keyValuePair.Key.ToLowerInvariant());
canonicalizedResourceBuilder.Append(':');
canonicalizedResourceBuilder.Append(keyValuePair.Value);
canonicalizedResourceBuilder.Append('\n');
}
// Now get the body out.
if (request.Content != null)
{
using var bodyStream = new MemoryStream();
await request.Content.CopyToAsync(bodyStream).ConfigureAwait(false);
bodyStream.Seek(0, SeekOrigin.Begin);
using var streamReader = new StreamReader(bodyStream);
var requestContent = streamReader.ReadToEnd();
if (requestContent != null)
{
canonicalizedResourceBuilder.Append(requestContent);
}
canonicalizedResourceBuilder.Append('\n');
}
return canonicalizedResourceBuilder.ToString();
}
private static byte[] CalculateHmac256(byte[] key, string plainText)
{
using HashAlgorithm hashAlgorithm = new HMACSHA256(key);
byte[] messageBuffer = new UTF8Encoding(false).GetBytes(plainText);
return hashAlgorithm.ComputeHash(messageBuffer);
}
If that works because the canonicalization is right then it becomes possible to do it automatically using an HttpMessageHandler, which you can put in the HTTP Pipeline before the HTTP client sends a request and can change the request on its way out
public class AwfulSpecHttpMessageHandler : DelegatingHandler
{
public AwfulSpecHttpMessageHandler(byte[] key)
{
Key = key;
}
protected async override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
byte[] hash = await HashRequest(request, Key);
// Now put that hash somewhere in the message
// You'll probably have to base64 encode it first with Convert.ToBase64String(hash)
// Then send it on its way
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
private byte[] Key { get; set; }
}
And then you'd wire that up
var authenticationHandler = new AwfulSpecHttpMessageHandler(key)
{
InnerHandler = new HttpClientHandler()
};
using (var httpClient = new HttpClient(authenticationHandler))
{
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://localhost");
{
httpRequestMessage.Content = new StringContent("myMessage");
await httpClient.SendAsync(httpRequestMessage);
};
}
Or using the HttpClientFactory in ASP.NET where you can add handlers for named clients
I cannibalized this from a shared secret implementation I wrote
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |
