比较JSON对象的哈希

时间:2019-08-19 23:40:43

标签: c# json api hash .net-core

我正在尝试使哈希比较器正常工作,以便可以验证传入的请求。

流量:
Sender creates json object-> sender creates hash of json object with a key that they and I know-> sender sends json object and header with hash in it-> I recieve request-> I hash the json object with the common key-> I compare my hash to the one in the header to validate user sending it

我正在努力从json对象创建哈希。

这是Ruby中(来自发送者)的示例代码,其中request_payload是JSON对象。

hmac=OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'),YOUR_COMPANY_SIGNING_KEY,request_payload)  
signature = Base64.strict_encode64(hmac)

我想用C#进行此操作。

我正在使用the Call Rail API中的数据(请参见右侧),并尝试将其哈希为字符串然后进行编码。

[HttpPost]
public async Task<ActionResult> PostAsync(dynamic request)
{

    string signature = GetHash(request.ToString(), "072e77e426f92738a72fe23c4d1953b4"); // this key is from the example in Call Rail

    string encodedSignature = Base64Encode(signature);

    return Ok();
}

public static String GetHash(dynamic text, String key)
{
    ASCIIEncoding encoding = new ASCIIEncoding();

    Byte[] textBytes = encoding.GetBytes(text);
    Byte[] keyBytes = encoding.GetBytes(key);

    Byte[] hashBytes;

    using (HMACSHA1 hash = new HMACSHA1(keyBytes))
        hashBytes = hash.ComputeHash(textBytes);

    return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
}

public static string Base64Encode(string plainText)
{
    var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText);
    return System.Convert.ToBase64String(plainTextBytes);
}

我认为我苦苦挣扎的是如何获取传入的JSON

{"answered":false,"business_phone_number":"","call_type":"voicemail","company_id":155920786,"company_name":"Boost Marketing","company_time_zone":"America/Los_Angeles","created_at":"2018-02-19T13:41:00.252-05:00","customer_city":"Rochester","customer_country":"US","customer_name":"Kaylah Mills","customer_phone_number":"+12148654559","customer_state":"PA","device_type":"","direction":"inbound","duration":"13","first_call":false,"formatted_call_type":"Voicemail","formatted_customer_location":"Rochester, PA","formatted_business_phone_number":"","formatted_customer_name":"Kaylah Mills","prior_calls":16,"formatted_customer_name_or_phone_number":"Kaylah Mills","formatted_customer_phone_number":"214-865-4559","formatted_duration":"13s","formatted_tracking_phone_number":"404-555-8514","formatted_tracking_source":"Google Paid","formatted_value":"--","good_lead_call_id":715587840,"good_lead_call_time":"2016-06-17T10:23:33.363-04:00","id":766970532,"lead_status":"previously_marked_good_lead","note":"","recording":"https://app.callrail.com/calls/766970532/recording/redirect?access_key=aaaaccccddddeeee","recording_duration":8,"source_name":"Google AdWords","start_time":"2018-02-19T13:41:00.236-05:00","tags":[],"total_calls":17,"tracking_phone_number":"+14045558514","transcription":"","value":"","voicemail":true,"tracker_id":354024023,"keywords":"","medium":"","referring_url":"","landing_page_url":"","last_requested_url":"","referrer_domain":"","conversational_transcript":"","utm_source":"google","utm_medium":"cpc","utm_term":"","utm_content":"","utm_campaign":"Google AdWords","utma":"","utmb":"","utmc":"","utmv":"","utmz":"","ga":"","gclid":"","integration_data":[{"integration":"Webhooks","data":null}],"keywords_spotted":"","recording_player":"https://app.callrail.com/calls/766970532/recording?access_key=aaaabbbbccccdddd","speaker_percent":"","call_highlights":[],"callercity":"Rochester","callercountry":"US","callername":"Kaylah Mills","callernum":"+12148654559","callerstate":"PA","callsource":"google_paid","campaign":"","custom":"","datetime":"2018-02-19 18:41:00","destinationnum":"","ip":"","kissmetrics_id":"","landingpage":"","referrer":"","referrermedium":"","score":1,"tag":"","trackingnum":"+14045558514","timestamp":"2018-02-19T13:41:00.236-05:00"}

然后可以将其哈希为有用的东西。

使用给出的测试签名密钥,我应该找回UZAHbUdfm3GqL7qzilGozGzWV64=。我从APIDocs知道这一点。

我目前正在通过邮递员发送上面的JSON字符串,但我注意到当我将其视为数据类型{或{{1}时,会添加额外的'} dynamic' }。

任何见识将不胜感激!

2 个答案:

答案 0 :(得分:1)

我认为您面临的问题是.NET Core WebAPI正在帮助您将主体解析为JSON(转换为JObject)。

@dbc确定,真正需要的是用于生成HMAC签名的原始字符串主体,您可以在将主体自行解析为JSON之前进行验证。

我测试了This answer,并能够以纯字符串的形式接收尸体:

using System;
using System.Linq;
using System.Threading.Tasks;
using System.IO;
using Microsoft.AspNetCore.Mvc.Formatters;

namespace netcoretest {
    public class RawJsonBodyInputFormatter : InputFormatter
    {
        public RawJsonBodyInputFormatter()
        {
            this.SupportedMediaTypes.Add("application/json");
        }

        public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
        {
            var request = context.HttpContext.Request;
            using (var reader = new StreamReader(request.Body))
            {
                var content = await reader.ReadToEndAsync();
                return await InputFormatterResult.SuccessAsync(content);
            }
        }

        protected override bool CanReadType(Type type)
        {
            return type == typeof(string);
        }
    }
}

Startup.cs中:

// in ConfigureServices()
services.AddMvc(options => {
    options.InputFormatters.Insert(0, new RawJsonBodyInputFormatter());
});

在您的控制器中:

[HttpPost]
public async Task<ActionResult> PostTest([FromBody]string request)
{
  // here request is now the request body as a plain string;
  // you can now compute the signature on it and then later parse it to JSON.


}

但是,测试您当前的代码以生成Base64编码的签名,我没有得到正确的签名:

  1. 您要将HMAC的输出转换为十六进制字符串,然后获取该字符串的字节并将其放入Base64编码中。您链接的示例红宝石返回纯字节,而不是HMAC.digest中的十六进制字符串:
[5] pry(main)> hmac = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'), YOUR_COMPANY_SIGNING_KEY, s)
=> "Q\x90\amG_\x9Bq\xAA/\xBA\xB3\x8AQ\xA8\xCCl\xD6W\xAE"

因此,至少在实现过程中也需要纠正这一部分。

更新

我能够使用以下代码获得正确的签名:

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System;
using System.Security.Cryptography;
using System.Text;

namespace netcoretest.Controllers
{
    [Route("test")]
    [ApiController]
    public class TestController : ControllerBase
    {
        public TestController()
        {
        }

        // POST: /test
        [HttpPost]
        public async Task<ActionResult> PostTest([FromBody]string request)
        {
            ASCIIEncoding encoding = new ASCIIEncoding();
            Byte[] key = encoding.GetBytes("072e77e426f92738a72fe23c4d1953b4");
            HMACSHA1 hmac = new HMACSHA1(key);
            Byte[] bytes = hmac.ComputeHash(encoding.GetBytes(request));
            Console.WriteLine(ByteArrayToString(bytes));
            String result = System.Convert.ToBase64String(bytes);
            Console.WriteLine(result);

            return Ok();
        }

        public static string ByteArrayToString(byte[] ba)
        {
            return BitConverter.ToString(ba).Replace("-","");
        }
    }
}

我使用以下方法测试了向该端点的发布:

url --request POST https://localhost:5001/test --insecure --header 'Content-Type: application/json' --data-binary @test.json

其中test.json是API文档中的示例JSON blob。

如果使用相同的代码,则无法获得匹配的签名,请仔细检查test.json上是否没有尾随换行符或空格。

希望这会有所帮助!

答案 1 :(得分:1)

尽管@john_Ledbetter的答案很有效,但我决定使用一个动作过滤器更适合我。因此,我以他的答案为基础并为我修改了答案。我不认为此解决方案需要InputFormatter

ValidateCallRailRequestFiler.cs

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace My_API.ActionFilters
{
    public class ValidateCallRailRequestFilter: ActionFilterAttribute
    {
        //private readonly ILogger<ValidateCallRailRequestFilter> _logger;
        //public ValidateCallRailRequestFilter(ILogger<ValidateCallRailRequestFilter> logger)
        //{
        //    _logger = logger;
        //}

        public override void OnActionExecuting(ActionExecutingContext actionContext)
        {
            //executing before action is called

            // this should only return one object since that is all an API allows. Also, it should send something else it will be a bad request
            var param = actionContext.ActionArguments.SingleOrDefault();
            if (param.Value == null)
            {
                //_logger.LogError("Object sent was null. Caught in ValidateCallRailRequestFilter class.");
                actionContext.Result = new BadRequestObjectResult("Object sent is null");
                return;
            }

            var context = actionContext.HttpContext;
            if (!IsValidRequest(context.Request))
            {
                actionContext.Result = new ForbidResult();
                return;
            }

            base.OnActionExecuting(actionContext);

        }

        private static bool IsValidRequest(HttpRequest request)
        {
            string json = GetRawBodyString(request.HttpContext);
            string token = "072e77e426f92738a72fe23c4d1953b4"; // this is the token that the API (Call Rail) would provide
            string signature = request.Headers["Signature"];

            // validation for comparing encoding to bytes and hashing to be the same
            //https://rextester.com/EBR67249

            ASCIIEncoding encoding = new ASCIIEncoding();
            byte[] key = encoding.GetBytes(token);
            HMACSHA1 hmac = new HMACSHA1(key);
            byte[] bytes = hmac.ComputeHash(encoding.GetBytes(json));

            string result = System.Convert.ToBase64String(bytes);            

            return signature.Equals(result, StringComparison.OrdinalIgnoreCase);
        }

        public static string GetRawBodyString(HttpContext httpContext)
        {
            var body = "";
            if (httpContext.Request.ContentLength == null || !(httpContext.Request.ContentLength > 0) ||
                !httpContext.Request.Body.CanSeek) return body;
            httpContext.Request.EnableRewind();
            httpContext.Request.Body.Seek(0, SeekOrigin.Begin);
            using (var reader = new StreamReader(httpContext.Request.Body, System.Text.Encoding.UTF8, true, 1024, true))
            {
                body = reader.ReadToEnd();
            }
            httpContext.Request.Body.Position = 0;
            return body;
        }
    }
}

这包括一个读取JSON的正文阅读器,以及一种拒绝与403签名不匹配的传入请求的方法。

然后在我的控制器内:

[ValidateCallRailRequestFilter]
[HttpPost]
public async Task<ActionResult> PostAsync(dynamic request)
{
    ...
    return Ok();
}

带有using My_Api.ActionFilters;

然后我和邮递员打了。 note that it is not "beautified" note the header here