Reputation: 375
I am trying to validate my Function App Secret Key, that is passed from Github Webhook, using .NET CORE 3.1.
In my Github webhook, I inserted default key from Azure function into "Secret" field. Now, I am trying to validate it in my code. For some reason my encrypted secret key is different from the one in webhook.
NOTE: Secret from Github Webhook is encrypted with SHA1 algorithm.
Code:
public static async Task<IActionResult> Run(HttpRequest req, ILogger log)
{
var secretKey = "my_key";
StringValues outHeader;
if (req.Headers.TryGetValue("x-hub-signature", out outHeader))
{
log.LogWarning("==========");
log.LogWarning(outHeader);
log.LogWarning(GetHash(secretKey));
log.LogWarning("==========");
}
string responseMessage = "Everything went well!";
return new OkObjectResult(responseMessage);
}
public static string GetHash(string input)
{
return "sha1=" + string.Join("",
(new SHA1Managed()
.ComputeHash(Encoding.UTF8.GetBytes(input)))
.Select(x => x.ToString("x2"))
.ToArray());
}
Output:
2020-12-13T16:46:47.592 [Warning] ==========
2020-12-13T16:46:47.592 [Warning] sha1=f859bebbf5ec452a7ecd42efc69e0d86a4f25b16
2020-12-13T16:46:47.593 [Warning] sha1=fa1167715f137edff21d55d00adf63afb318b2a6
2020-12-13T16:46:47.593 [Warning] ==========
Official docs covers Node.js solution only.
What is the right way to validate Github Webhook Secret in .NET CORE 3.1? Thank you for any help.
Upvotes: 4
Views: 1335
Reputation: 1408
You're not passing the payload here to your GetHash method and the GetHash method doesn't accept the secret. This is my implementation:
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using System;
using System.IO;
using System.Net;
using System.Net.Mail;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace GitHubWebhooks
{
public static class Security
{
private const string ShaPrefix = "sha256=";
private const string keyVaultUrl = "<keyvault URL or replace with some other security>";
private const string gitHubWebhookSecretSecretName = "GitHubWebHookSecret";
private static KeyVaultSecret gitHubWebhookSecret;
private static async Task FetchSecrets(CancellationToken cancellationToken)
{
var client = new SecretClient(new Uri(keyVaultUrl), new DefaultAzureCredential());
var gitHubWebHookSecretSecretResponse = await client.GetSecretAsync(gitHubWebhookSecretSecretName, cancellationToken: cancellationToken);
gitHubWebhookSecret = gitHubWebHookSecretSecretResponse.Value;
}
// https://davidpine.net/blog/github-profanity-filter/
// https://docs.github.com/en/developers/webhooks-and-events/securing-your-webhooks
public static async Task<bool> IsGithubPushAllowedAsync(HttpRequest request, CancellationToken cancellationToken)
{
if (gitHubWebhookSecret == null)
{
await FetchSecrets(cancellationToken);
}
request.Headers.TryGetValue("X-GitHub-Event", out StringValues eventName);
request.Headers.TryGetValue("X-Hub-Signature-256", out StringValues signatureWithPrefix);
request.Headers.TryGetValue("X-GitHub-Delivery", out StringValues delivery);
if (string.IsNullOrWhiteSpace(eventName))
{
return false;
}
if (string.IsNullOrWhiteSpace(signatureWithPrefix))
{
return false;
}
if (string.IsNullOrWhiteSpace(delivery))
{
return false;
}
string payload;
// https://justsimplycode.com/2020/08/02/reading-httpcontext-request-body-content-returning-empty-after-upgrading-to-net-core-3-from-2-0/
// Request buffering needs to be enabled in app startup configuration.
// The snippet is:
// app.Use((context, next) =>
// {
// context.Request.EnableBuffering();
// return next();
// });
request.Body.Position = 0;
// We don't close the stream as we're not the one who's opened it.
using (var reader = new StreamReader(request.Body, leaveOpen: true))
{
payload = await reader.ReadToEndAsync();
}
if (string.IsNullOrWhiteSpace(payload))
{
return false;
}
string signatureWithPrefixString = signatureWithPrefix;
if (signatureWithPrefixString.StartsWith(ShaPrefix, StringComparison.OrdinalIgnoreCase))
{
var signature = signatureWithPrefixString.Substring(ShaPrefix.Length);
var secret = Encoding.ASCII.GetBytes(gitHubWebhookSecret.Value);
var payloadBytes = Encoding.UTF8.GetBytes(payload);
using (var sha = new HMACSHA256(secret))
{
var hash = sha.ComputeHash(payloadBytes);
var hashString = ToHexString(hash);
if (hashString.Equals(signature))
{
return true;
}
}
}
return false;
}
public static string ToHexString(byte[] bytes)
{
var builder = new StringBuilder(bytes.Length * 2);
foreach (byte b in bytes)
{
builder.AppendFormat("{0:x2}", b);
}
return builder.ToString();
}
}
}
Upvotes: 3