无法使用 C# 使 Typeform Webhook 签名正常工作

Unable to get Typeform Webhook Signature with C# to work

首先,这个问题已经被问到并得到回答 但它是特定于 Ruby/PHP 的,虽然我试图遵循它和 Typeform 自己的指导,但我无法实施C# 中的 Typeform 签名检查。

我编写了一个扩展方法来根据通过 webhook 发送的有效负载验证 Typeform 签名。如果签名有效,它 returns 字符串 (json) 有效载荷但如果不是 returns 错误。

public static class HttpRequestExtensions {
    private const string SignatureHeader = "Typeform-Signature";
    private static readonly Encoding encoding = new UTF8Encoding ();

    public static async Task<Result<string>> ValidateAndRetrievePayload (this HttpRequestMessage request, string key) {
        var headerValue = request.GetHeaderValue (SignatureHeader);
        if (string.IsNullOrWhiteSpace (headerValue)) return Result.Failure<string> ($"'{SignatureHeader}' Header not found or empty.");

        var json = await request.Content.ReadAsStringAsync ();
        var payload = encoding.GetBytes (json);
        using (var hmac256 = new HMACSHA256 (encoding.GetBytes (key))) {
            var hashPayload = hmac256.ComputeHash (payload);
            var base64String = Convert.ToBase64String (hashPayload);
            var hashResult = $"sha256={base64String}";
            if (hashResult.Equals (headerValue)) return Result.Success (json);
            return Result.Failure<string> ($"'{SignatureHeader}' does not match. Header: `{headerValue}` | Hash: `{hashResult}`");
        }
    }
}

根据在 SO 上发现的其他问题,我将方法修改为 运行 没有编码(见下文)但仍然得到相同的结果,哈希值不匹配。

public static class HttpRequestExtensions
{
    private const string SignatureHeader = "Typeform-Signature";

    public static async Task<Result<string>> ValidateAndRetrievePayload(this HttpRequestMessage request, string key)
    {
        var headerValue = request.GetHeaderValue(SignatureHeader);
        if (string.IsNullOrWhiteSpace(headerValue))
            return Result.Failure<string>($"'{SignatureHeader}' Header not found or empty.");

        var payload = await request.Content.ReadAsByteArrayAsync();
        var byteKey = GetBytes(key);
        using (var hmac256 = new HMACSHA256(byteKey))
        {
            var hashPayload = hmac256.ComputeHash(payload);
            var base64String = Convert.ToBase64String(hashPayload);
            var hashResult = $"sha256={base64String}";
            if (hashResult.Equals(headerValue))
                return Result.Success(await request.Content.ReadAsStringAsync());
            return Result.Failure<string>(
                $"'{SignatureHeader}' does not match. Header: `{headerValue}` | Hash: `{hashResult}`");
        }
    }

    private static byte[] GetBytes(string value)
    {
        var bytes = new byte[value.Length * sizeof(char)];
        Buffer.BlockCopy(value.ToCharArray(), 0, bytes, 0, bytes.Length);
        return bytes;
    }

    private static string GetString(byte[] bytes)
    {
        var chars = new char[bytes.Length / sizeof(char)];
        Buffer.BlockCopy(bytes, 0, chars, 0, bytes.Length);
        return new string(chars);
    }
}

我希望这会有所帮助...我也是 TypeForm API 的新手,但我有以下代码正在运行...非常混乱,我还没有抽出时间重构它...它在我的一个表单上工作正常,但出于某种原因,当我使用不同的 TypeForm 帐户时它不起作用(即使我设置了相同的秘密)......这就是为什么我还没有重构它......

但只是分享,因为它适用于我的第一个帐户并且可能有用(我的第二个帐户有同样的问题,结果不匹配......如果我找到原因,我会在这里告诉你和更新此答案

    private bool IsValid(string jsonRequest)
    {
        string typeFormSig = Request.Headers["Typeform-Signature"];
        string generatedSig = $"sha256={CreateToken(jsonRequest)}";
        _logger.LogInformation($"SIGNATURE STUFF: sec: {SECRET} typeform: {typeFormSig}  MyGen: {generatedSig}");
        return (typeFormSig == generatedSig);
    }

    private static string CreateToken(string message)
    {
        var encoding = new System.Text.ASCIIEncoding();
        byte[] keyByte = encoding.GetBytes(SECRET);
        byte[] messageBytes = encoding.GetBytes(message);
        using (var hmacsha256 = new HMACSHA256(keyByte))
        {
            byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);
            return Convert.ToBase64String(hashmessage);
        }
    }

这就是我得到 json 字符串的方式:

[HttpPost("")]
    public async Task<IActionResult> Receive()
    {
        using (var reader = new StreamReader(Request.Body))
        {
            string jsonRequest = await reader.ReadToEndAsync();

尝试使用 UTF 编码器而不是建议的原始 ASCII 编码器:

private static string CreateToken(string message)
{
    var encoding = new System.Text.UTF8Encoding();
    byte[] keyByte = encoding.GetBytes(SECRET);
    byte[] messageBytes = encoding.GetBytes(message);
    using (var hmacsha256 = new HMACSHA256(keyByte))
    {
        byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);
        return Convert.ToBase64String(hashmessage);
    }
}

private bool IsValid(string jsonRequest)
{
    string typeFormSig = Request.Headers["Typeform-Signature"];
    string generatedSig = $"sha256={CreateToken(jsonRequest)}";
    _logger.LogInformation($"SIGNATURE STUFF: sec: {SECRET} typeform: {typeFormSig}  MyGen: {generatedSig}");
    return (typeFormSig == generatedSig);
}

这是我最终使用的解决方案。这个问题的大多数答案的某些方面最终提供了解决问题的线索。

public async Task<bool> ValidateSignature(HttpRequest request, Signature signatureData)
{
    var headerValue = request.Headers[signatureData.HeaderKeyName];
    var keyBytes = Encoding.UTF8.GetBytes(signatureData.Secret);
    var messageBytes = Encoding.UTF8.GetBytes(await request.ReadAsStringAsync());
    byte[] hashMessage;

    switch (signatureData.HashType)
    {
        case HashType.HMAC_Sha1:
            hashMessage = new HMACSHA1(keyBytes).ComputeHash(messageBytes);
            break;

        case HashType.HMAC_Sha256:
            hashMessage = new HMACSHA256(keyBytes).ComputeHash(messageBytes);
            break;

        case HashType.HMAC_Sha384:
            hashMessage = new HMACSHA384(keyBytes).ComputeHash(messageBytes);
            break;

        case HashType.HMAC_Sha512:
            hashMessage = new HMACSHA512(keyBytes).ComputeHash(messageBytes);
            break;

        case HashType.HMAC_MD5:
            hashMessage = new HMACMD5(keyBytes).ComputeHash(messageBytes);
            break;

        default:
            throw new ArgumentOutOfRangeException(nameof(signatureData), "Hash type not currently supported.");
    }

    var builder = new StringBuilder();
    foreach (var t in hashMessage) builder.Append(t.ToString("x2"));

    var finalValue = builder.ToString();
    if (signatureData.HasPrefix) finalValue = $"{signatureData.PrefixValue}{builder}";

    return finalValue == headerValue;
}

如果您在测试 webhook 服务时遇到此问题,请记住 Typeform 发送的 JSON 正文已缩小 以单个 Unix 换行符 (0x0a) 字符.

结尾

这在将请求正文直接发送到 HMACSHA256 实例时关系不大,但如果您尝试使用内联 JSON 字符串验证代码,则这一点很重要。特别令人困惑的是,Typeform 在 webhooks 管理 UI.

中显示格式化的 JSON 输出

C# 中字符串的签名验证可能如下所示:

using System;

class Program {
  static void Main(string[] args) {
    var key = "secret-key";
    // Note:  
    // - No spaces after `:`, `,`, etc.
    // - No indentation.
    // - No newlines except for a single line feed character on the end.
    var body = "{\"event_id\":\"01FCR2NZ5NNGBPTWEJXV0FR5V3\",\"event_type\":\"...\"}\u000a";
    
    // Convert strings to UTF-8 byte arrays.
    var keyBytes = Text.Encoding.UTF8.GetBytes(key);
    var bodyBytes = Text.Encoding.UTF8.GetBytes(body);

    string signature;
    using (var hmac = new Security.Cryptography.HMACSHA256(keyBytes))
    {
      // Calculate a hash and convert it to a base-64 string.
      var computedHashBytes = hmac.ComputeHash(bodyBytes);
      var computedHashBase64 = Convert.ToBase64String(computedHashBytes);
      signature = $"sha256={computedHashBase64}";    
    }
    Console.WriteLine(signature);
  }
}