Azure/.NET CORE 3.1 - Github Function App 中的 Webhook 秘密验证

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

我正在尝试验证从 Github Webhook 传递的 Function App Secret Key,使用 .NET 核心 3.1.

在我的 Github webhook 中,我将来自 Azure 函数 的默认密钥插入 “秘密” 字段。现在,我正尝试在我的代码中验证它。由于某种原因,我的加密密钥与 webhook 中的密钥不同。

注意: 来自 Github Webhook 的秘密使用 SHA1 算法加密。

代码:

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());
}

输出:

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 仅涵盖 Node.js 解决方案。

.NET CORE 3.1 中验证 Github Webhook Secret 的正确方法是什么?感谢您的帮助。

您没有将此处的有效负载传递给您的 GetHash 方法,并且 GetHash 方法不接受密钥。这是我的实现:

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();
        }
    }
}