0
votes

We are trying to call AWS API Gateway from C# Windows Service, for a background job. Which was supposed to trigger API Gateway periodically initialize request?

We used RestSharp to invoke API Endpoint, the class called AwsAuthenticator , which is inherited from RestSharp.Authenticators.IAuthenticator. But when we invoke API Gateway we received with error as "The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details."

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;

namespace ConsoleApp3
{
public class AwsAuthenticator : RestSharp.Authenticators.IAuthenticator
{
    public string AccessKeyId { get; }
    public string AccessKeySecret { get; }
    public string Region { get; }

    public AwsAuthenticator(string accessKeyId, string accessKeySecret, string region)
    {
        AccessKeyId = accessKeyId;
        AccessKeySecret = accessKeySecret;
        Region = region;
    }

    private static HashSet<string> ignoredHeaders = new HashSet<string>() {
        "authorization",
        "content-length",
        "content-type",
        "user-agent"
    };

    public void Authenticate(RestSharp.IRestClient client, RestSharp.IRestRequest request)
    {
        DateTime signingDate = DateTime.UtcNow;
        SetContentMd5(request);
        SetContentSha256(request);
        SetHostHeader(request, client);
        SetDateHeader(request, signingDate);
        SortedDictionary<string, string> headersToSign = GetHeadersToSign(request);
        string signedHeaders = GetSignedHeaders(headersToSign);
        string canonicalRequest = GetCanonicalRequest(client, request, headersToSign);
        byte[] canonicalRequestBytes = System.Text.Encoding.UTF8.GetBytes(canonicalRequest);
        string canonicalRequestHash = BytesToHex(ComputeSha256(canonicalRequestBytes));
        string stringToSign = GetStringToSign(Region, signingDate, canonicalRequestHash);
        byte[] signingKey = GenerateSigningKey(Region, signingDate);

        byte[] stringToSignBytes = System.Text.Encoding.UTF8.GetBytes(stringToSign);

        byte[] signatureBytes = SignHmac(signingKey, stringToSignBytes);

        string signature = BytesToHex(signatureBytes);

        string authorization = GetAuthorizationHeader(signedHeaders, signature, signingDate, Region);
        request.AddHeader("Authorization", authorization);

    }

    public string GetCredentialString(DateTime signingDate, string region)
    {
        return AccessKeyId + "/" + GetScope(region, signingDate);
    }

    private string GetAuthorizationHeader(string signedHeaders, string signature, DateTime signingDate, string region)
    {
        return "AWS4-HMAC-SHA256 Credential=" + this.AccessKeyId + "/" + GetScope(region, signingDate) +
            ", SignedHeaders=" + signedHeaders + ", Signature=" + signature;
    }

    private string GetSignedHeaders(SortedDictionary<string, string> headersToSign)
    {
        return string.Join(";", headersToSign.Keys);
    }

    private byte[] GenerateSigningKey(string region, DateTime signingDate)
    {
        byte[] formattedDateBytes = System.Text.Encoding.UTF8.GetBytes(signingDate.ToString("yyyMMdd"));
        byte[] formattedKeyBytes = System.Text.Encoding.UTF8.GetBytes("AWS4" + this.AccessKeySecret);
        byte[] dateKey = SignHmac(formattedKeyBytes, formattedDateBytes);

        byte[] regionBytes = System.Text.Encoding.UTF8.GetBytes(region);
        byte[] dateRegionKey = SignHmac(dateKey, regionBytes);

        byte[] serviceBytes = System.Text.Encoding.UTF8.GetBytes("execute-api");
        byte[] dateRegionServiceKey = SignHmac(dateRegionKey, serviceBytes);

        byte[] requestBytes = System.Text.Encoding.UTF8.GetBytes("aws4_request");
        return SignHmac(dateRegionServiceKey, requestBytes);
    }

    private byte[] SignHmac(byte[] key, byte[] content)
    {
        HMACSHA256 hmac = new HMACSHA256(key);
        hmac.Initialize();
        return hmac.ComputeHash(content);
    }

    private string GetStringToSign(string region, DateTime signingDate, string canonicalRequestHash)
    {
        return "AWS4-HMAC-SHA256\n" +
            signingDate.ToString("yyyyMMddTHHmmssZ") + "\n" +
            GetScope(region, signingDate) + "\n" +
            canonicalRequestHash;
    }

    private string GetScope(string region, DateTime signingDate)
    {
        string formattedDate = signingDate.ToString("yyyyMMdd");
        return formattedDate + "/" + region + "/execute-api/aws4_request";
    }

    private byte[] ComputeSha256(byte[] body)
    {

        SHA256 sha256 = SHA256.Create();
        return sha256.ComputeHash(body);
    }

    private string BytesToHex(byte[] checkSum)
    {
        return BitConverter.ToString(checkSum).Replace("-", string.Empty).ToLower();
    }

    public string PresignPostSignature(string region, DateTime signingDate, string policyBase64)
    {
        byte[] signingKey = this.GenerateSigningKey(region, signingDate);
        byte[] stringToSignBytes = System.Text.Encoding.UTF8.GetBytes(policyBase64);

        byte[] signatureBytes = SignHmac(signingKey, stringToSignBytes);
        string signature = BytesToHex(signatureBytes);

        return signature;
    }

    public string PresignURL(RestSharp.IRestClient client, RestSharp.IRestRequest request, int expires)
    {
        DateTime signingDate = DateTime.UtcNow;
        string requestQuery = "";
        string path = request.Resource;

        requestQuery = "X-Amz-Algorithm=AWS4-HMAC-SHA256&";
        requestQuery += "X-Amz-Credential="
            + this.AccessKeyId
            + Uri.EscapeDataString("/" + GetScope(Region, signingDate))
            + "&";
        requestQuery += "X-Amz-Date="
            + signingDate.ToString("yyyyMMddTHHmmssZ")
            + "&";
        requestQuery += "X-Amz-Expires="
            + expires
            + "&";
        requestQuery += "X-Amz-SignedHeaders=host";

        string canonicalRequest = GetPresignCanonicalRequest(client, request, requestQuery);
        byte[] canonicalRequestBytes = System.Text.Encoding.UTF8.GetBytes(canonicalRequest);
        string canonicalRequestHash = BytesToHex(ComputeSha256(canonicalRequestBytes));
        string stringToSign = GetStringToSign(Region, signingDate, canonicalRequestHash);
        byte[] signingKey = GenerateSigningKey(Region, signingDate);
        byte[] stringToSignBytes = System.Text.Encoding.UTF8.GetBytes(stringToSign);
        byte[] signatureBytes = SignHmac(signingKey, stringToSignBytes);
        string signature = BytesToHex(signatureBytes);

        // Return presigned url.
        return client.BaseUrl + path + "?" + requestQuery + "&X-Amz-Signature=" + signature;
    }

    private string GetPresignCanonicalRequest(RestSharp.IRestClient client, RestSharp.IRestRequest request, string requestQuery)
    {
        LinkedList<string> canonicalStringList = new LinkedList<string>();
        canonicalStringList.AddLast(request.Method.ToString());

        string path = request.Resource;
        if (!path.StartsWith("/"))
        {
            path = "/" + path;
        }
        canonicalStringList.AddLast(path);
        canonicalStringList.AddLast(requestQuery);
        canonicalStringList.AddLast("host:" + client.BaseUrl.Host);
        canonicalStringList.AddLast("");
        canonicalStringList.AddLast("host");
        canonicalStringList.AddLast("UNSIGNED-PAYLOAD");

        return string.Join("\n", canonicalStringList);
    }

    private string GetCanonicalRequest(RestSharp.IRestClient client, RestSharp.IRestRequest request,
        SortedDictionary<string, string> headersToSign)
    {
        LinkedList<string> canonicalStringList = new LinkedList<string>();
        canonicalStringList.AddLast(request.Method.ToString());

        string[] path = request.Resource.Split(new char[] { '?' }, 2);
        if (!path[0].StartsWith("/"))
        {
            path[0] = "/" + path[0];
        }
        canonicalStringList.AddLast(path[0]);

        string query = "";
        if (path.Length == 2)
        {
            var parameterString = path[1];
            var parameterList = parameterString.Split('&');
            SortedSet<string> sortedQueries = new SortedSet<string>();
            foreach (string individualParameterString in parameterList)
            {
                if (individualParameterString.Contains('='))
                {
                    string[] splitQuery = individualParameterString.Split(new char[] { '=' }, 2);
                    sortedQueries.Add(splitQuery[0] + "=" + splitQuery[1]);
                }
                else
                {
                    sortedQueries.Add(individualParameterString + "=");
                }
            }
            query = string.Join("&", sortedQueries);
        }
        canonicalStringList.AddLast(query);

        foreach (string header in headersToSign.Keys)
        {
            canonicalStringList.AddLast(header + ":" + headersToSign[header]);
        }
        canonicalStringList.AddLast("");
        canonicalStringList.AddLast(string.Join(";", headersToSign.Keys));
        if (headersToSign.Keys.Contains("x-amz-content-sha256"))
        {
            canonicalStringList.AddLast(headersToSign["x-amz-content-sha256"]);
        }
        else
        {
            canonicalStringList.AddLast("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
        }

        return string.Join("\n", canonicalStringList);
    }

    private SortedDictionary<string, string> GetHeadersToSign(RestSharp.IRestRequest request)
    {
        var headers = request.Parameters.Where(p => p.Type.Equals(RestSharp.ParameterType.HttpHeader)).ToList();

        SortedDictionary<string, string> sortedHeaders = new SortedDictionary<string, string>();
        foreach (var header in headers)
        {
            string headerName = header.Name.ToLower();
            string headerValue = header.Value.ToString();
            if (!ignoredHeaders.Contains(headerName))
            {
                sortedHeaders.Add(headerName, headerValue);
            }
        }
        return sortedHeaders;
    }

    private void SetDateHeader(RestSharp.IRestRequest request, DateTime signingDate)
    {
        request.AddHeader("x-amz-date", signingDate.ToString("yyyyMMddTHHmmssZ"));
    }

    private void SetHostHeader(RestSharp.IRestRequest request, RestSharp.IRestClient client)
    {
        request.AddHeader("Host", client.BaseUrl.Host + (client.BaseUrl.Port != 80 ? ":" + client.BaseUrl.Port : string.Empty));
    }

    private void SetContentSha256(RestSharp.IRestRequest request)
    {
        if (request.Method == RestSharp.Method.PUT || request.Method.Equals(RestSharp.Method.POST))
        {
            var bodyParameter = request.Parameters.Where(p => p.Type.Equals(RestSharp.ParameterType.RequestBody)).FirstOrDefault();
            if (bodyParameter == null)
            {
                request.AddHeader("x-amz-content-sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
                return;
            }
            byte[] body = null;
            if (bodyParameter.Value is string)
            {
                body = System.Text.Encoding.UTF8.GetBytes(bodyParameter.Value as string);
            }
            if (bodyParameter.Value is byte[])
            {
                body = bodyParameter.Value as byte[];
            }
            if (body == null)
            {
                body = new byte[0];
            }
            SHA256 sha256 = System.Security.Cryptography.SHA256.Create();
            byte[] hash = sha256.ComputeHash(body);
            string hex = BitConverter.ToString(hash).Replace("-", string.Empty).ToLower();
            request.AddHeader("x-amz-content-sha256", hex);
        }
        else
        {
            request.AddHeader("x-amz-content-sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
        }
    }

    private void SetContentMd5(RestSharp.IRestRequest request)
    {
        if (request.Method == RestSharp.Method.PUT || request.Method.Equals(RestSharp.Method.POST))
        {
            var bodyParameter = request.Parameters.Where(p => p.Type.Equals(RestSharp.ParameterType.RequestBody)).FirstOrDefault();
            if (bodyParameter == null)
            {
                return;
            }
            byte[] body = null;
            if (bodyParameter.Value is string)
            {
                body = System.Text.Encoding.UTF8.GetBytes(bodyParameter.Value as string);
            }
            if (bodyParameter.Value is byte[])
            {
                body = bodyParameter.Value as byte[];
            }
            if (body == null)
            {
                body = new byte[0];
            }
            MD5 md5 = MD5.Create();
            byte[] hash = md5.ComputeHash(body);

            string base64 = Convert.ToBase64String(hash);
            request.AddHeader("Content-MD5", base64);
        }
    }
}
////////////////////////
public class MainClass
    {
    public void Execute()
        {
            var client = new RestClient("https://nm47849kod.execute-api.ap-southeast1.amazonaws.com/samplegateway/");
            var request = new RestRequest("/", Method.POST);
            var postData = new { Mode = 4 };
            request.AddParameter("application/json",JsonConvert.SerializeObject(postData),ParameterType.RequestBody); AwsAuthenticator awsAuthenticator = new AwsAuthenticator("AccessKeyXXXXX", "SECKEYxxxx12313123123123123", "apsoutheast-1");
            awsAuthenticator.Authenticate(client,request);

            IRestResponse response = client.Execute(request);
            var content = response.Content; // raw content as string
            Console.WriteLine(content);
            Console.ReadLine();
        }
}

Error Details:

{"message":"The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.\n\nThe Canonical String for this request should have been\n'POST\n/samplegateway/\n\ncontent-md5:rkT7BbUvFInBgrPCuA0UZw==\nhost:nm47849kod.execute-api.ap-southeast-1.amazonaws.com\nx-amz-content-sha256:0318f62547c9078687e73f987ec26fa557047b67f54bb99b8047c950990ae42c\nx-amz-date:20190601T102835Z\n\ncontent-md5;host;x-amz-content-sha256;x-amz-date\n0318f62547c9078687e73f987ec26fa557047b67f54bb99b8047c950990ae42c'\n\nThe String-to-Sign should have been\n'AWS4-HMAC-SHA256\n20190601T102835Z\n20190601/ap-southeast-1/execute-api/aws4_request\n8f89bd5010655fb26a8de5e29d48d6129ac7875e5eb6bc2faeb8e41864b4d49e'\n"}.

1

1 Answers

1
votes

We identified the problem.

Below is the working code and this resolves my issue. I am sharing this so that the group can get benefitted. The above class is entirely rewritten and when invoked it worked.

public class ApiRequest
{
    private const string ServiceName = "execute-api";
    private const string Algorithm = "AWS4-HMAC-SHA256";
    private const string ContentType = "application/json";
    private const string SignedHeaders = "content-type;host;x-amz-date;x-api-key";
    private const string DateTimeFormat = "yyyyMMddTHHmmssZ";
    private const string DateFormat = "yyyyMMdd";

    public AwsApiGatewayRequest AwsApiGatewayRequest;

    public ApiRequest(AwsApiGatewayRequest request)
    {
        AwsApiGatewayRequest = request;

        if (string.IsNullOrEmpty(AwsApiGatewayRequest.RequestMethod))
            AwsApiGatewayRequest.RequestMethod = "POST";

        if (string.IsNullOrEmpty(AwsApiGatewayRequest.xApiKey))
            AwsApiGatewayRequest.xApiKey = "";
    }

    public WebResponse GetPostResponse()
    {
        var request = GetPostRequest();
        return request.GetResponse();
    }

    public WebRequest GetPostRequest()
    {
        string hashedRequestPayload = CreateRequestPayload(AwsApiGatewayRequest.JsonData);

        string authorization = Sign(hashedRequestPayload, AwsApiGatewayRequest.RequestMethod, AwsApiGatewayRequest.AbsolutePath, AwsApiGatewayRequest.QueryString);
        string requestDate = DateTime.UtcNow.ToString(DateTimeFormat);

        var webRequest = WebRequest.Create($"https://{AwsApiGatewayRequest.Host}{AwsApiGatewayRequest.AbsolutePath}");

        webRequest.Timeout = AwsApiGatewayRequest.RequestTimeout.HasValue ? AwsApiGatewayRequest.RequestTimeout.Value : 50000;
        webRequest.Method = AwsApiGatewayRequest.RequestMethod;
        webRequest.ContentType = ContentType;
        webRequest.Headers.Add("X-Amz-date", requestDate);
        webRequest.Headers.Add("Authorization", authorization);
        webRequest.Headers.Add("x-amz-content-sha256", hashedRequestPayload);

        if (!string.IsNullOrEmpty(AwsApiGatewayRequest.AdditionalHeaders))
        {
            // parse apart and apply the additional headers
            string[] headers = AwsApiGatewayRequest.AdditionalHeaders.Split(';');
            foreach (string header in headers)
            {
                var headervalue = header.Split('=');
                if (headervalue.Count() == 2)
                    webRequest.Headers.Add(headervalue[0], headervalue[1]);
            }
        }

        if (!string.IsNullOrEmpty(AwsApiGatewayRequest.SessionToken))
            webRequest.Headers.Add("X-Amz-Security-Token", AwsApiGatewayRequest.SessionToken);
        webRequest.ContentLength = AwsApiGatewayRequest.JsonData.Length;

        var encoding = new ASCIIEncoding();
        var data = encoding.GetBytes(AwsApiGatewayRequest.JsonData);

        using (var newStream = webRequest.GetRequestStream())
        {
            newStream.Write(data, 0, data.Length);
            newStream.Close();
        }

        return webRequest;
    }

    private string CreateRequestPayload(string jsonString)
    {
        return HexEncode(Hash(ToBytes(jsonString)));
    }

    private string Sign(string hashedRequestPayload, string requestMethod, string canonicalUri, string canonicalQueryString)
    {
        var currentDateTime = DateTime.UtcNow;

        var dateStamp = currentDateTime.ToString(DateFormat);
        var requestDate = currentDateTime.ToString(DateTimeFormat);
        var credentialScope = $"{dateStamp}/{AwsApiGatewayRequest.RegionName}/{ServiceName}/aws4_request";

        var headers = new SortedDictionary<string, string> {
            { "content-type", ContentType },
            { "host", AwsApiGatewayRequest.Host },
            { "x-amz-date", requestDate },
            { "x-api-key", AwsApiGatewayRequest.xApiKey }
        };

        var canonicalHeaders = string.Join("\n", headers.Select(x => x.Key.ToLowerInvariant() + ":" + x.Value.Trim())) + "\n";

        // Task 1: Create a Canonical Request For Signature Version 4
        var canonicalRequest = $"{requestMethod}\n{canonicalUri}\n{canonicalQueryString}\n{canonicalHeaders}\n{SignedHeaders}\n{hashedRequestPayload}";
        var hashedCanonicalRequest = HexEncode(Hash(ToBytes(canonicalRequest)));

        // Task 2: Create a String to Sign for Signature Version 4
        var stringToSign = $"{Algorithm}\n{requestDate}\n{credentialScope}\n{hashedCanonicalRequest}";

        // Task 3: Calculate the AWS Signature Version 4
        var signingKey = GetSignatureKey(AwsApiGatewayRequest.SecretKey, dateStamp, AwsApiGatewayRequest.RegionName, ServiceName);
        var signature = HexEncode(HmacSha256(stringToSign, signingKey));

        // Task 4: Prepare a signed request
        // Authorization: algorithm Credential=access key ID/credential scope, SignedHeadaers=SignedHeaders, Signature=signature
        var authorization = $"{Algorithm} Credential={AwsApiGatewayRequest.AccessKey}/{dateStamp}/{AwsApiGatewayRequest.RegionName}/{ServiceName}/aws4_request, SignedHeaders={SignedHeaders}, Signature={signature}";

        return authorization;
    }

    private byte[] GetSignatureKey(string key, string dateStamp, string regionName, string serviceName)
    {
        var kDate = HmacSha256(dateStamp, ToBytes("AWS4" + key));
        var kRegion = HmacSha256(regionName, kDate);
        var kService = HmacSha256(serviceName, kRegion);
        return HmacSha256("aws4_request", kService);
    }

    private byte[] ToBytes(string str)
    {
        return Encoding.UTF8.GetBytes(str.ToCharArray());
    }

    private string HexEncode(byte[] bytes)
    {
        return BitConverter.ToString(bytes).Replace("-", string.Empty).ToLowerInvariant();
    }

    private byte[] Hash(byte[] bytes)
    {
        return SHA256.Create().ComputeHash(bytes);
    }

    private byte[] HmacSha256(string data, byte[] key)
    {
        return new HMACSHA256(key).ComputeHash(ToBytes(data));
    }
}

Execution Parameter:

var request = new AwsApiGatewayRequest()
       {
           RegionName = "",
           Host = ,
           AccessKey = "",
           SecretKey = "",
           RequestMethod = "POST",
           AbsolutePath = ,
           JsonData = "{\"Mode\":\"4\"}",
           SessionToken = ""
       };//Invoke this using RestClient...

The problem here is we failed to add an additional header which was required by AWS. In this version, we have added hence it rectified.

Thanks for your support.