// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using AWSSignatureV4_S3_Sample.Util; namespace AWSSignatureV4_S3_Sample.Signers { /// /// Common methods and properties for all AWS4 signer variants /// public abstract class AWS4SignerBase { // SHA256 hash of an empty request body public const string EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; public const string SCHEME = "AWS4"; public const string ALGORITHM = "HMAC-SHA256"; public const string TERMINATOR = "aws4_request"; // format strings for the date/time and date stamps required during signing public const string ISO8601BasicFormat = "yyyyMMddTHHmmssZ"; public const string DateStringFormat = "yyyyMMdd"; // some common x-amz-* parameters public const string X_Amz_Algorithm = "X-Amz-Algorithm"; public const string X_Amz_Credential = "X-Amz-Credential"; public const string X_Amz_SignedHeaders = "X-Amz-SignedHeaders"; public const string X_Amz_Date = "X-Amz-Date"; public const string X_Amz_Signature = "X-Amz-Signature"; public const string X_Amz_Expires = "X-Amz-Expires"; public const string X_Amz_Content_SHA256 = "X-Amz-Content-SHA256"; public const string X_Amz_Decoded_Content_Length = "X-Amz-Decoded-Content-Length"; public const string X_Amz_Meta_UUID = "X-Amz-Meta-UUID"; // the name of the keyed hash algorithm used in signing public const string HMACSHA256 = "HMACSHA256"; // request canonicalization requires multiple whitespace compression protected static readonly Regex CompressWhitespaceRegex = new Regex("\\s+"); // algorithm used to hash the canonical request that is supplied to // the signature computation public static HashAlgorithm CanonicalRequestHashAlgorithm = HashAlgorithm.Create("SHA-256"); /// /// The service endpoint, including the path to any resource. /// public Uri EndpointUri { get; set; } /// /// The HTTP verb for the request, e.g. GET. /// public string HttpMethod { get; set; } /// /// The signing name of the service, e.g. 's3'. /// public string Service { get; set; } /// /// The system name of the AWS region associated with the endpoint, e.g. us-east-1. /// public string Region { get; set; } /// /// Returns the canonical collection of header names that will be included in /// the signature. For AWS4, all header names must be included in the process /// in sorted canonicalized order. /// /// /// The set of header names and values that will be sent with the request /// /// /// The set of header names canonicalized to a flattened, ;-delimited string /// protected string CanonicalizeHeaderNames(IDictionary headers) { var headersToSign = new List(headers.Keys); headersToSign.Sort(StringComparer.OrdinalIgnoreCase); var sb = new StringBuilder(); foreach (var header in headersToSign) { if (sb.Length > 0) sb.Append(";"); sb.Append(header.ToLower()); } return sb.ToString(); } /// /// Computes the canonical headers with values for the request. /// For AWS4, all headers must be included in the signing process. /// /// The set of headers to be encoded /// Canonicalized string of headers with values protected virtual string CanonicalizeHeaders(IDictionary headers) { if (headers == null || headers.Count == 0) return string.Empty; // step1: sort the headers into lower-case format; we create a new // map to ensure we can do a subsequent key lookup using a lower-case // key regardless of how 'headers' was created. var sortedHeaderMap = new SortedDictionary(); foreach (var header in headers.Keys) { sortedHeaderMap.Add(header.ToLower(), headers[header]); } // step2: form the canonical header:value entries in sorted order. // Multiple white spaces in the values should be compressed to a single // space. var sb = new StringBuilder(); foreach (var header in sortedHeaderMap.Keys) { var headerValue = CompressWhitespaceRegex.Replace(sortedHeaderMap[header], " "); sb.AppendFormat("{0}:{1}\n", header, headerValue.Trim()); } return sb.ToString(); } /// /// Returns the canonical request string to go into the signer process; this /// consists of several canonical sub-parts. /// /// /// /// /// /// The set of header names to be included in the signature, formatted as a flattened, ;-delimited string /// /// /// /// /// Precomputed SHA256 hash of the request body content. For chunked encoding this /// should be the fixed string ''. /// /// String representing the canonicalized request for signing protected string CanonicalizeRequest(Uri endpointUri, string httpMethod, string queryParameters, string canonicalizedHeaderNames, string canonicalizedHeaders, string bodyHash) { var canonicalRequest = new StringBuilder(); canonicalRequest.AppendFormat("{0}\n", httpMethod); canonicalRequest.AppendFormat("{0}\n", CanonicalResourcePath(endpointUri)); canonicalRequest.AppendFormat("{0}\n", queryParameters); canonicalRequest.AppendFormat("{0}\n", canonicalizedHeaders); canonicalRequest.AppendFormat("{0}\n", canonicalizedHeaderNames); canonicalRequest.Append(bodyHash); return canonicalRequest.ToString(); } /// /// Returns the canonicalized resource path for the service endpoint /// /// Endpoint to the service/resource /// Canonicalized resource path for the endpoint protected string CanonicalResourcePath(Uri endpointUri) { if (string.IsNullOrEmpty(endpointUri.AbsolutePath)) return "/"; // encode the path per RFC3986 return HttpHelpers.UrlEncode(endpointUri.AbsolutePath, true); } /// /// Compute and return the multi-stage signing key for the request. /// /// Hashing algorithm to use /// The clear-text AWS secret key /// The region in which the service request will be processed /// Date of the request, in yyyyMMdd format /// The name of the service being called by the request /// Computed signing key protected byte[] DeriveSigningKey(string algorithm, string awsSecretAccessKey, string region, string date, string service) { const string ksecretPrefix = SCHEME; char[] ksecret = null; ksecret = (ksecretPrefix + awsSecretAccessKey).ToCharArray(); byte[] hashDate = ComputeKeyedHash(algorithm, Encoding.UTF8.GetBytes(ksecret), Encoding.UTF8.GetBytes(date)); byte[] hashRegion = ComputeKeyedHash(algorithm, hashDate, Encoding.UTF8.GetBytes(region)); byte[] hashService = ComputeKeyedHash(algorithm, hashRegion, Encoding.UTF8.GetBytes(service)); return ComputeKeyedHash(algorithm, hashService, Encoding.UTF8.GetBytes(TERMINATOR)); } /// /// Compute and return the hash of a data blob using the specified algorithm /// and key /// /// Algorithm to use for hashing /// Hash key /// Data blob /// Hash of the data protected byte[] ComputeKeyedHash(string algorithm, byte[] key, byte[] data) { var kha = KeyedHashAlgorithm.Create(algorithm); kha.Key = key; return kha.ComputeHash(data); } /// /// Helper to format a byte array into string /// /// The data blob to process /// If true, returns hex digits in lower case form /// String version of the data public static string ToHexString(byte[] data, bool lowercase) { var sb = new StringBuilder(); for (var i = 0; i < data.Length; i++) { sb.Append(data[i].ToString(lowercase ? "x2" : "X2")); } return sb.ToString(); } } }