// 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();
}
}
}