Lemon Squeezy Webhook验证Python代码转C#遇签名匹配失败问题
问题背景
我正在对接支付提供商Lemon Squeezy的Webhook功能,需要验证请求确实来自该提供商,但官方文档仅提供PHP、Node.js和Python代码示例,而我的应用基于C#开发。
签名密钥用于生成请求payload的哈希值,并通过请求的X-Signature头传递。该密钥是我在提供商后台设置的,本地通过配置依赖注入获取,签名从请求头获取,请求体取自当前请求。目前我写出的C#代码计算出的digest始终与signature不匹配,需要排查问题并找到解决方案。
官方Python示例代码
import hashlib import hmac signature = request.META['HTTP_X_SIGNATURE'] secret = '[SIGNING_SECRET]' digest = hmac.new(secret.encode(), request.body, hashlib.sha256).hexdigest() if not hmac.compare_digest(digest, signature): raise Exception('Invalid signature.')
我编写的C#代码
var signature = Request.Headers["X-Signature"]; string secret = _configuration.GetValue<string>("Keys:LMWebhookSigningSecret"); string requestBody = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync(); using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret))) { byte[] computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(requestBody)); string digest = BitConverter.ToString(computedHash).Replace("-", "").ToLower(); if (!digest.Equals(signature)) { throw new Exception("Invalid signature."); } }
排查方向及解决方案
以下是可能导致签名不匹配的原因及对应修正方案:
请求体流位置问题:如果请求体流被之前的中间件读取过,流的位置会停在末尾,导致
StreamReader读取到空内容。解决方法是在读取前重置流位置:HttpContext.Request.Body.Position = 0; // 重置流到开头 string requestBody = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync();签名头取值问题:
Request.Headers["X-Signature"]返回的是StringValues类型,若存在多个头值会导致取值错误。应改为获取单个值:var signature = Request.Headers["X-Signature"].FirstOrDefault() ?? string.Empty;安全比对方法缺失:Python使用
hmac.compare_digest防时序攻击,C#应使用对应的安全方法CryptographicOperations.FixedTimeEquals(需引用System.Security.Cryptography命名空间),同时要将字符串转为字节数组后比对:if (!CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(digest), Encoding.UTF8.GetBytes(signature))) { throw new Exception("Invalid signature."); }编码一致性验证:确保请求体的编码为UTF-8,避免因编码差异导致哈希计算错误。Lemon Squeezy的Webhook默认使用UTF-8编码,若存在特殊字符可尝试直接读取字节流而非转成字符串:
HttpContext.Request.Body.Position = 0; byte[] requestBodyBytes = new byte[HttpContext.Request.ContentLength.Value]; await HttpContext.Request.Body.ReadAsync(requestBodyBytes, 0, requestBodyBytes.Length); using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret))) { byte[] computedHash = hmac.ComputeHash(requestBodyBytes); string digest = BitConverter.ToString(computedHash).Replace("-", "").ToLower(); // 后续比对逻辑... }
修正后的完整C#代码示例
using System.Security.Cryptography; using System.Text; var signature = Request.Headers["X-Signature"].FirstOrDefault() ?? string.Empty; string secret = _configuration.GetValue<string>("Keys:LMWebhookSigningSecret"); // 重置请求体流位置并读取原始字节 HttpContext.Request.Body.Position = 0; byte[] requestBodyBytes = new byte[HttpContext.Request.ContentLength.Value]; await HttpContext.Request.Body.ReadAsync(requestBodyBytes, 0, requestBodyBytes.Length); using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret))) { byte[] computedHash = hmac.ComputeHash(requestBodyBytes); string digest = BitConverter.ToString(computedHash).Replace("-", "").ToLower(); if (!CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(digest), Encoding.UTF8.GetBytes(signature))) { throw new Exception("Invalid signature."); } }
内容的提问来源于stack exchange,提问作者nerdalert

