Wojciech X
Wojciech X

Reputation: 375

Azure/.NET CORE 3.1 - Github Webhook secret validation in Function App

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

Answers (1)

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

Related Questions