/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using Amazon.Internal;
using Amazon.Util;
using Amazon.Runtime.Internal.Util;
namespace Amazon.Runtime.Internal.Auth
{
///
/// AWS4 protocol signer for service calls that transmit authorization in the header field "Authorization".
///
public class AWS4Signer : AbstractAWSSigner
{
public const string Scheme = "AWS4";
public const string Algorithm = "HMAC-SHA256";
public const string Sigv4aAlgorithm = "ECDSA-P256-SHA256";
public const string AWS4AlgorithmTag = Scheme + "-" + Algorithm;
public const string AWS4aAlgorithmTag = Scheme + "-" + Sigv4aAlgorithm;
public const string Terminator = "aws4_request";
public static readonly byte[] TerminatorBytes = Encoding.UTF8.GetBytes(Terminator);
public const string Credential = "Credential";
public const string SignedHeaders = "SignedHeaders";
public const string Signature = "Signature";
public const string EmptyBodySha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
public const string StreamingBodySha256 = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD";
public const string StreamingBodySha256WithTrailer = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER";
public const string V4aStreamingBodySha256 = "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD";
public const string V4aStreamingBodySha256WithTrailer = "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER";
public const string AWSChunkedEncoding = "aws-chunked";
public const string UnsignedPayload = "UNSIGNED-PAYLOAD";
public const string UnsignedPayloadWithTrailer = "STREAMING-UNSIGNED-PAYLOAD-TRAILER";
const SigningAlgorithm SignerAlgorithm = SigningAlgorithm.HmacSHA256;
private static IEnumerable _headersToIgnoreWhenSigning = new HashSet(StringComparer.OrdinalIgnoreCase) {
HeaderKeys.XAmznTraceIdHeader,
HeaderKeys.TransferEncodingHeader,
HeaderKeys.AmzSdkInvocationId,
HeaderKeys.AmzSdkRequest
};
public AWS4Signer()
: this(true)
{
}
public AWS4Signer(bool signPayload)
{
SignPayload = signPayload;
}
public bool SignPayload
{
get;
private set;
}
public override ClientProtocol Protocol
{
get { return ClientProtocol.RestProtocol; }
}
///
/// Calculates and signs the specified request using the AWS4 signing protocol by using the
/// AWS account credentials given in the method parameters. The resulting signature is added
/// to the request headers as 'Authorization'. Parameters supplied in the request, either in
/// the resource path as a query string or in the Parameters collection must not have been
/// uri encoded. If they have, use the SignRequest method to obtain a signature.
///
///
/// The request to compute the signature for. Additional headers mandated by the AWS4 protocol
/// ('host' and 'x-amz-date') will be added to the request before signing.
///
///
/// Client configuration data encompassing the service call (notably authentication
/// region, endpoint and service name).
///
///
/// Metrics for the request
///
///
/// The AWS public key for the account making the service call.
///
///
/// The AWS secret key for the account making the call, in clear text.
///
///
/// If any problems are encountered while signing the request.
///
public override void Sign(IRequest request,
IClientConfig clientConfig,
RequestMetrics metrics,
string awsAccessKeyId,
string awsSecretAccessKey)
{
var signingResult = SignRequest(request, clientConfig, metrics, awsAccessKeyId, awsSecretAccessKey);
request.Headers[HeaderKeys.AuthorizationHeader] = signingResult.ForAuthorizationHeader;
}
///
/// Calculates and signs the specified request using the AWS4 signing protocol by using the
/// AWS account credentials given in the method parameters. The resulting signature is added
/// to the request headers as 'Authorization'. Parameters supplied in the request, either in
/// the resource path as a query string or in the Parameters collection must not have been
/// uri encoded. If they have, use the SignRequest method to obtain a signature.
///
///
/// The request to compute the signature for. Additional headers mandated by the AWS4 protocol
/// ('host' and 'x-amz-date') will be added to the request before signing.
///
///
/// Client configuration data encompassing the service call (notably authentication
/// region, endpoint and service name).
///
///
/// Metrics for the request
///
///
/// The AWS credentials for the account making the service call.
///
///
/// If any problems are encountered while signing the request.
///
public override void Sign(IRequest request,
IClientConfig clientConfig,
RequestMetrics metrics,
ImmutableCredentials credentials)
{
Sign(request, clientConfig, metrics, credentials.AccessKey, credentials.SecretKey);
}
///
/// Calculates and signs the specified request using the AWS4 signing protocol by using the
/// AWS account credentials given in the method parameters.
///
///
/// The request to compute the signature for. Additional headers mandated by the AWS4 protocol
/// ('host' and 'x-amz-date') will be added to the request before signing.
///
///
/// Client configuration data encompassing the service call (notably authentication
/// region, endpoint and service name).
///
///
/// Metrics for the request.
///
///
/// The AWS public key for the account making the service call.
///
///
/// The AWS secret key for the account making the call, in clear text.
///
///
/// If any problems are encountered while signing the request.
///
///
/// Parameters passed as part of the resource path should be uri-encoded prior to
/// entry to the signer. Parameters passed in the request.Parameters collection should
/// be not be encoded; encoding will be done for these parameters as part of the
/// construction of the canonical request.
///
public AWS4SigningResult SignRequest(IRequest request,
IClientConfig clientConfig,
RequestMetrics metrics,
string awsAccessKeyId,
string awsSecretAccessKey)
{
ValidateRequest(request);
var signedAt = InitializeHeaders(request.Headers, request.Endpoint);
var serviceSigningName = !string.IsNullOrEmpty(request.OverrideSigningServiceName) ? request.OverrideSigningServiceName : DetermineService(clientConfig);
if (serviceSigningName == "s3")
{
// Older versions of the S3 package can be used with newer versions of Core, this guarantees no double encoding will be used.
// The new behavior uses endpoint resolution rules, which are not present prior to 3.7.100
request.UseDoubleEncoding = false;
}
request.DeterminedSigningRegion = DetermineSigningRegion(clientConfig, clientConfig.RegionEndpointServiceName, request.AlternateEndpoint, request);
SetXAmzTrailerHeader(request.Headers, request.TrailingHeaders);
var parametersToCanonicalize = GetParametersToCanonicalize(request);
var canonicalParameters = CanonicalizeQueryParameters(parametersToCanonicalize);
// If the request should use a fixed x-amz-content-sha256 header value, determine the appropriate one
var bodySha = request.TrailingHeaders?.Count > 0
? StreamingBodySha256WithTrailer
: StreamingBodySha256;
var bodyHash = SetRequestBodyHash(request, SignPayload, bodySha, ChunkedUploadWrapperStream.V4_SIGNATURE_LENGTH);
var sortedHeaders = SortAndPruneHeaders(request.Headers);
var canonicalRequest = CanonicalizeRequest(request.Endpoint,
request.ResourcePath,
request.HttpMethod,
sortedHeaders,
canonicalParameters,
bodyHash,
request.PathResources,
request.UseDoubleEncoding);
if (metrics != null)
metrics.AddProperty(Metric.CanonicalRequest, canonicalRequest);
return ComputeSignature(awsAccessKeyId,
awsSecretAccessKey,
request.DeterminedSigningRegion,
signedAt,
serviceSigningName,
CanonicalizeHeaderNames(sortedHeaders),
canonicalRequest,
metrics);
}
#region Public Signing Helpers
///
/// Sets the AWS4 mandated 'host' and 'x-amz-date' headers, returning the date/time that will
/// be used throughout the signing process in various elements and formats.
///
/// The current set of headers
///
/// Date and time used for x-amz-date, in UTC
public static DateTime InitializeHeaders(IDictionary headers, Uri requestEndpoint)
{
return InitializeHeaders(headers, requestEndpoint, CorrectClockSkew.GetCorrectedUtcNowForEndpoint(requestEndpoint.ToString()));
}
///
/// Sets the AWS4 mandated 'host' and 'x-amz-date' headers, accepting and returning the date/time that will
/// be used throughout the signing process in various elements and formats.
///
/// The current set of headers
///
///
/// Date and time used for x-amz-date, in UTC
public static DateTime InitializeHeaders(IDictionary headers, Uri requestEndpoint, DateTime requestDateTime)
{
// clean up any prior signature in the headers if resigning
CleanHeaders(headers);
if (!headers.ContainsKey(HeaderKeys.HostHeader))
{
var hostHeader = requestEndpoint.Host;
if (!requestEndpoint.IsDefaultPort)
hostHeader += ":" + requestEndpoint.Port;
headers.Add(HeaderKeys.HostHeader, hostHeader);
}
var dt = requestDateTime;
headers[HeaderKeys.XAmzDateHeader] = dt.ToUniversalTime().ToString(AWSSDKUtils.ISO8601BasicDateTimeFormat, CultureInfo.InvariantCulture);
return dt;
}
///
/// Sets the x-amz-trailer header for the given set of trailing headers
///
/// request's headers
/// request's trailing headers
public static void SetXAmzTrailerHeader(IDictionary headers, IDictionary trailingHeaders)
{
if (trailingHeaders == null || trailingHeaders.Count == 0)
{
return;
}
// The x-amz-trailer HTTP header MUST be set with the value as comma-separated
// string consisting of trailing header names in the order they are written on the HTTP request.
headers[HeaderKeys.XAmzTrailerHeader] = string.Join(",", trailingHeaders.Keys.OrderBy(key => key).ToArray());
}
private static void CleanHeaders(IDictionary headers)
{
headers.Remove(HeaderKeys.AuthorizationHeader);
headers.Remove(HeaderKeys.XAmzContentSha256Header);
if (headers.ContainsKey(HeaderKeys.XAmzDecodedContentLengthHeader))
{
headers[HeaderKeys.ContentLengthHeader] =
headers[HeaderKeys.XAmzDecodedContentLengthHeader];
headers.Remove(HeaderKeys.XAmzDecodedContentLengthHeader);
}
}
private static void ValidateRequest(IRequest request)
{
Uri url = request.Endpoint;
// Before we sign the request, we need to validate if the request is being
// sent over https when DisablePayloadSigning is true.
if((request.DisablePayloadSigning ?? false) && url.Scheme != "https")
{
throw new AmazonClientException("When DisablePayloadSigning is true, the request must be sent over HTTPS.");
}
}
///
/// Computes and returns an AWS4 signature for the specified canonicalized request
///
///
///
///
///
///
///
///
public static AWS4SigningResult ComputeSignature(ImmutableCredentials credentials,
string region,
DateTime signedAt,
string service,
string signedHeaders,
string canonicalRequest)
{
return ComputeSignature(credentials.AccessKey,
credentials.SecretKey,
region,
signedAt,
service,
signedHeaders,
canonicalRequest);
}
///
/// Computes and returns an AWS4 signature for the specified canonicalized request
///
///
///
///
///
///
///
///
///
public static AWS4SigningResult ComputeSignature(string awsAccessKey,
string awsSecretAccessKey,
string region,
DateTime signedAt,
string service,
string signedHeaders,
string canonicalRequest)
{
return ComputeSignature(awsAccessKey, awsSecretAccessKey, region, signedAt, service, signedHeaders, canonicalRequest, null);
}
///
/// Computes and returns an AWS4 signature for the specified canonicalized request
///
///
///
///
///
///
///
///
///
///
public static AWS4SigningResult ComputeSignature(string awsAccessKey,
string awsSecretAccessKey,
string region,
DateTime signedAt,
string service,
string signedHeaders,
string canonicalRequest,
RequestMetrics metrics)
{
var dateStamp = FormatDateTime(signedAt, AWSSDKUtils.ISO8601BasicDateFormat);
var scope = string.Format(CultureInfo.InvariantCulture, "{0}/{1}/{2}/{3}", dateStamp, region, service, Terminator);
var stringToSignBuilder = new StringBuilder();
stringToSignBuilder.AppendFormat(CultureInfo.InvariantCulture, "{0}-{1}\n{2}\n{3}\n",
Scheme,
Algorithm,
FormatDateTime(signedAt, AWSSDKUtils.ISO8601BasicDateTimeFormat),
scope);
var canonicalRequestHashBytes = ComputeHash(canonicalRequest);
stringToSignBuilder.Append(AWSSDKUtils.ToHex(canonicalRequestHashBytes, true));
if (metrics != null)
metrics.AddProperty(Metric.StringToSign, stringToSignBuilder);
var key = ComposeSigningKey(awsSecretAccessKey,
region,
dateStamp,
service);
var stringToSign = stringToSignBuilder.ToString();
var signature = ComputeKeyedHash(SignerAlgorithm, key, stringToSign);
return new AWS4SigningResult(awsAccessKey, signedAt, signedHeaders, scope, key, signature);
}
///
/// Formats the supplied date and time for use in AWS4 signing, where various formats are used.
///
///
/// The required format
/// The UTC date/time in the requested format
public static string FormatDateTime(DateTime dt, string formatString)
{
return dt.ToUniversalTime().ToString(formatString, CultureInfo.InvariantCulture);
}
///
/// Compute and return the multi-stage signing key for the request.
///
/// The clear-text AWS secret key, if not held in secureKey
/// 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
public static byte[] ComposeSigningKey(string awsSecretAccessKey, string region, string date, string service)
{
char[] ksecret = null;
try
{
ksecret = (Scheme + awsSecretAccessKey).ToCharArray();
var hashDate = ComputeKeyedHash(SignerAlgorithm, Encoding.UTF8.GetBytes(ksecret), Encoding.UTF8.GetBytes(date));
var hashRegion = ComputeKeyedHash(SignerAlgorithm, hashDate, Encoding.UTF8.GetBytes(region));
var hashService = ComputeKeyedHash(SignerAlgorithm, hashRegion, Encoding.UTF8.GetBytes(service));
return ComputeKeyedHash(SignerAlgorithm, hashService, TerminatorBytes);
}
finally
{
// clean up all secrets, regardless of how initially seeded (for simplicity)
if (ksecret != null)
Array.Clear(ksecret, 0, ksecret.Length);
}
}
///
/// If the caller has already set the x-amz-content-sha256 header with a pre-computed
/// content hash, or it is present as ContentStreamHash on the request instance, return
/// the value to be used in request canonicalization.
/// If not set as a header or in the request, attempt to compute a hash based on
/// inspection of the style of the request content.
///
/// Request to sign
/// The fixed value to set for the x-amz-content-sha256 header for chunked requests
/// Length of the signature for each chunk in a chuncked request, in bytes
///
/// The computed hash, whether already set in headers or computed here. Null
/// if we were not able to compute a hash.
///
public static string SetRequestBodyHash(IRequest request, string chunkedBodyHash, int signatureLength)
{
return SetRequestBodyHash(request, true, chunkedBodyHash, signatureLength);
}
///
/// If signPayload is false set the x-amz-content-sha256 header to
/// the UNSIGNED-PAYLOAD magic string and return it.
/// Otherwise, if the caller has already set the x-amz-content-sha256 header with a pre-computed
/// content hash, or it is present as ContentStreamHash on the request instance, return
/// the value to be used in request canonicalization.
/// If not set as a header or in the request, attempt to compute a hash based on
/// inspection of the style of the request content.
///
/// Request to sign
/// Whether to sign the payload
/// The fixed value to set for the x-amz-content-sha256 header for chunked requests
/// Length of the signature for each chunk in a chuncked request, in bytes
///
/// The computed hash, whether already set in headers or computed here. Null
/// if we were not able to compute a hash.
///
public static string SetRequestBodyHash(IRequest request, bool signPayload, string chunkedBodyHash, int signatureLength)
{
// If unsigned payload, set the appropriate magic string in the header and return it
if (request.DisablePayloadSigning != null ? request.DisablePayloadSigning.Value : !signPayload)
{
if (request.TrailingHeaders?.Count > 0)
{
// Set X-Amz-Decoded-Content-Length with the true size of the data
request.Headers[HeaderKeys.XAmzDecodedContentLengthHeader] = request.Headers[HeaderKeys.ContentLengthHeader];
// Substitute the originally declared content length with the inflated length due to trailing headers
var originalContentLength = long.Parse(request.Headers[HeaderKeys.ContentLengthHeader], CultureInfo.InvariantCulture);
request.Headers[HeaderKeys.ContentLengthHeader]
= TrailingHeadersWrapperStream.CalculateLength(request.TrailingHeaders, request.SelectedChecksum, originalContentLength).ToString(CultureInfo.InvariantCulture);
SetContentEncodingHeader(request);
return SetPayloadSignatureHeader(request, UnsignedPayloadWithTrailer);
}
else // request does not have trailing headers (and is still unsigned payload)
{
return SetPayloadSignatureHeader(request, UnsignedPayload);
}
}
// if the body hash has been precomputed and already placed in the header, just extract and return it
string computedContentHash;
var shaHeaderPresent = request.Headers.TryGetValue(HeaderKeys.XAmzContentSha256Header, out computedContentHash);
if (shaHeaderPresent && !request.UseChunkEncoding)
return computedContentHash;
// otherwise continue to calculate the hash and set it in the headers before returning
if (request.UseChunkEncoding)
{
computedContentHash = chunkedBodyHash;
if (request.Headers.ContainsKey(HeaderKeys.ContentLengthHeader))
{
// Set X-Amz-Decoded-Content-Length with the true size of the data
request.Headers[HeaderKeys.XAmzDecodedContentLengthHeader] = request.Headers[HeaderKeys.ContentLengthHeader];
// Substitute the originally declared content length with the inflated length due to chunking metadata and/or trailing headers
var originalContentLength = long.Parse(request.Headers[HeaderKeys.ContentLengthHeader], CultureInfo.InvariantCulture);
request.Headers[HeaderKeys.ContentLengthHeader]
= ChunkedUploadWrapperStream.ComputeChunkedContentLength(originalContentLength, signatureLength, request.TrailingHeaders, request.SelectedChecksum).ToString(CultureInfo.InvariantCulture);
}
SetContentEncodingHeader(request);
}
else
{
if (request.ContentStream != null)
computedContentHash = request.ComputeContentStreamHash();
else
{
byte[] payloadBytes = GetRequestPayloadBytes(request);
byte[] payloadHashBytes = CryptoUtilFactory.CryptoInstance.ComputeSHA256Hash(payloadBytes);
computedContentHash = AWSSDKUtils.ToHex(payloadHashBytes, true);
}
}
// set the header if needed and return it
return SetPayloadSignatureHeader(request, computedContentHash ?? UnsignedPayload);
}
///
/// Appends "aws-chunked" to the Content-Encoding header if it's already set
///
/// Request to modify
private static void SetContentEncodingHeader(IRequest request)
{
if (request.Headers.TryGetValue(HeaderKeys.ContentEncodingHeader, out var originalEncoding) &&
!originalEncoding.Contains(AWSChunkedEncoding))
{
request.Headers[HeaderKeys.ContentEncodingHeader] = $"{originalEncoding}, {AWSChunkedEncoding}";
}
}
///
/// Returns the HMAC256 for an arbitrary blob using the specified key
///
///
///
///
public static byte[] SignBlob(byte[] key, string data)
{
return SignBlob(key, Encoding.UTF8.GetBytes(data));
}
///
/// Returns the HMAC256 for an arbitrary blob using the specified key
///
///
///
///
public static byte[] SignBlob(byte[] key, byte[] data)
{
return CryptoUtilFactory.CryptoInstance.HMACSignBinary(data, key, SignerAlgorithm);
}
///
/// Compute and return the hash of a data blob using the specified key
///
/// Algorithm to use for hashing
/// Hash key
/// Data blob
/// Hash of the data
public static byte[] ComputeKeyedHash(SigningAlgorithm algorithm, byte[] key, string data)
{
return ComputeKeyedHash(algorithm, key, Encoding.UTF8.GetBytes(data));
}
///
/// Compute and return the hash of a data blob using the specified key
///
/// Algorithm to use for hashing
/// Hash key
/// Data blob
/// Hash of the data
public static byte[] ComputeKeyedHash(SigningAlgorithm algorithm, byte[] key, byte[] data)
{
return CryptoUtilFactory.CryptoInstance.HMACSignBinary(data, key, algorithm);
}
///
/// Computes the non-keyed hash of the supplied data
///
///
///
public static byte[] ComputeHash(string data)
{
return ComputeHash(Encoding.UTF8.GetBytes(data));
}
///
/// Computes the non-keyed hash of the supplied data
///
///
///
public static byte[] ComputeHash(byte[] data)
{
return CryptoUtilFactory.CryptoInstance.ComputeSHA256Hash(data);
}
#endregion
#region Private Signing Helpers
static string SetPayloadSignatureHeader(IRequest request, string payloadHash)
{
if (request.Headers.ContainsKey(HeaderKeys.XAmzContentSha256Header))
request.Headers[HeaderKeys.XAmzContentSha256Header] = payloadHash;
else
request.Headers.Add(HeaderKeys.XAmzContentSha256Header, payloadHash);
return payloadHash;
}
public static string DetermineSigningRegion(IClientConfig clientConfig,
string serviceName,
RegionEndpoint alternateEndpoint,
IRequest request)
{
// Alternate endpoint (IRequest.AlternateEndpoint) takes precedence over
// client config properties.
if (alternateEndpoint != null)
{
var serviceEndpoint = alternateEndpoint.GetEndpointForService(serviceName, clientConfig.ToGetEndpointForServiceOptions());
if (serviceEndpoint.AuthRegion != null)
return serviceEndpoint.AuthRegion;
return alternateEndpoint.SystemName;
}
string authenticationRegion = clientConfig.AuthenticationRegion;
// We always have request.AuthenticationRegion defined, as per
// Amazon.Runtime.Internal.BaseEndpointResolver implementation.
// request.AuthenticationRegion value is set either based on endpoint rules or
// overriden by clientConfig.AuthenticationRegion if defined.
// Normally, users should only override clientConfig.AuthenticationRegion value for non-AWS services
if (request != null && request.AuthenticationRegion != null)
authenticationRegion = request.AuthenticationRegion;
if (!string.IsNullOrEmpty(authenticationRegion))
return authenticationRegion.ToLowerInvariant();
if (!string.IsNullOrEmpty(clientConfig.ServiceURL))
{
var parsedRegion = AWSSDKUtils.DetermineRegion(clientConfig.ServiceURL);
if (!string.IsNullOrEmpty(parsedRegion))
return parsedRegion.ToLowerInvariant();
}
var endpoint = clientConfig.RegionEndpoint;
if (endpoint != null)
{
var serviceEndpoint = endpoint.GetEndpointForService(serviceName, clientConfig.ToGetEndpointForServiceOptions());
if (!string.IsNullOrEmpty(serviceEndpoint.AuthRegion))
return serviceEndpoint.AuthRegion;
// Check if the region is overridden in the endpoints.json file
var overrideRegion = RegionEndpoint.GetRegionEndpointOverride(endpoint);
if (overrideRegion != null)
return overrideRegion.SystemName;
return endpoint.SystemName;
}
return string.Empty;
}
public static string DetermineService(IClientConfig clientConfig)
{
return (!string.IsNullOrEmpty(clientConfig.AuthenticationServiceName))
? clientConfig.AuthenticationServiceName
: AWSSDKUtils.DetermineService(clientConfig.DetermineServiceURL());
}
///
/// Computes and returns the canonical request
///
/// The endpoint URL
/// the path of the resource being operated on
/// The http method used for the request
/// The full request headers, sorted into canonical order
/// The query parameters for the request
///
/// The hash of the binary request body if present. If not supplied, the routine
/// will look for the hash as a header on the request.
///
/// Canonicalised request as a string
protected static string CanonicalizeRequest(Uri endpoint,
string resourcePath,
string httpMethod,
IDictionary sortedHeaders,
string canonicalQueryString,
string precomputedBodyHash)
{
return CanonicalizeRequest(endpoint, resourcePath, httpMethod, sortedHeaders, canonicalQueryString, precomputedBodyHash, null);
}
///
/// Computes and returns the canonical request
///
/// The endpoint URL
/// the path of the resource being operated on
/// The http method used for the request
/// The full request headers, sorted into canonical order
/// The query parameters for the request
///
/// The path resource values lookup to use to replace the keys within resourcePath
/// The hash of the binary request body if present. If not supplied, the routine
/// will look for the hash as a header on the request.
///
/// Canonicalised request as a string
protected static string CanonicalizeRequest(Uri endpoint,
string resourcePath,
string httpMethod,
IDictionary sortedHeaders,
string canonicalQueryString,
string precomputedBodyHash,
IDictionary pathResources)
{
return CanonicalizeRequestHelper(endpoint,
resourcePath,
httpMethod,
sortedHeaders,
canonicalQueryString,
precomputedBodyHash,
pathResources,
true);
}
///
/// Computes and returns the canonical request
///
/// The endpoint URL
/// the path of the resource being operated on
/// The http method used for the request
/// The full request headers, sorted into canonical order
/// The query parameters for the request
///
/// The path resource values lookup to use to replace the keys within resourcePath
/// The hash of the binary request body if present. If not supplied, the routine
/// will look for the hash as a header on the request.
///
/// Encode "/" when canonicalize resource path
/// Canonicalised request as a string
protected static string CanonicalizeRequest(Uri endpoint,
string resourcePath,
string httpMethod,
IDictionary sortedHeaders,
string canonicalQueryString,
string precomputedBodyHash,
IDictionary pathResources,
bool doubleEncode)
{
return CanonicalizeRequestHelper(endpoint,
resourcePath,
httpMethod,
sortedHeaders,
canonicalQueryString,
precomputedBodyHash,
pathResources,
doubleEncode);
}
private static string CanonicalizeRequestHelper(Uri endpoint,
string resourcePath,
string httpMethod,
IDictionary sortedHeaders,
string canonicalQueryString,
string precomputedBodyHash,
IDictionary pathResources,
bool doubleEncode)
{
var canonicalRequest = new StringBuilder();
canonicalRequest.AppendFormat("{0}\n", httpMethod);
canonicalRequest.AppendFormat("{0}\n", AWSSDKUtils.CanonicalizeResourcePathV2(endpoint, resourcePath, doubleEncode, pathResources));
canonicalRequest.AppendFormat("{0}\n", canonicalQueryString);
canonicalRequest.AppendFormat("{0}\n", CanonicalizeHeaders(sortedHeaders));
canonicalRequest.AppendFormat("{0}\n", CanonicalizeHeaderNames(sortedHeaders));
if (precomputedBodyHash != null)
{
canonicalRequest.Append(precomputedBodyHash);
}
else
{
string contentHash;
if (sortedHeaders.TryGetValue(HeaderKeys.XAmzContentSha256Header, out contentHash))
canonicalRequest.Append(contentHash);
}
return canonicalRequest.ToString();
}
///
/// Reorders the headers for the request for canonicalization.
///
/// The set of proposed headers for the request
/// List of headers that must be included in the signature
/// For AWS4 signing, all headers are considered viable for inclusion
protected internal static IDictionary SortAndPruneHeaders(IEnumerable> requestHeaders)
{
// Refer https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html. (Step #4: "Build the canonical headers list by sorting the (lowercase) headers by character code"). StringComparer.OrdinalIgnoreCase incorrectly places '_' after lowercase chracters.
var sortedHeaders = new SortedDictionary(StringComparer.Ordinal);
foreach (var header in requestHeaders)
{
if (_headersToIgnoreWhenSigning.Contains(header.Key))
{
continue;
}
sortedHeaders.Add(header.Key.ToLowerInvariant(), header.Value);
}
return sortedHeaders;
}
///
/// Computes the canonical headers with values for the request. Only headers included in the signature
/// are included in the canonicalization process.
///
/// All request headers, sorted into canonical order
/// Canonicalized string of headers, with the header names in lower case.
protected internal static string CanonicalizeHeaders(IEnumerable> sortedHeaders)
{
if (sortedHeaders == null || sortedHeaders.Count() == 0)
return string.Empty;
var builder = new StringBuilder();
foreach (var entry in sortedHeaders)
{
// Refer https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html. (Step #4: "To create the canonical headers list, convert all header names to lowercase and remove leading spaces and trailing spaces. Convert sequential spaces in the header value to a single space.").
builder.Append(entry.Key.ToLowerInvariant());
builder.Append(":");
builder.Append(AWSSDKUtils.CompressSpaces(entry.Value)?.Trim());
builder.Append("\n");
}
return builder.ToString();
}
///
/// Returns the set of headers included in the signature as a flattened, ;-delimited string
///
/// The headers included in the signature
/// Formatted string of header names
protected static string CanonicalizeHeaderNames(IEnumerable> sortedHeaders)
{
var builder = new StringBuilder();
foreach (var header in sortedHeaders)
{
if (builder.Length > 0)
builder.Append(";");
builder.Append(header.Key.ToLowerInvariant());
}
return builder.ToString();
}
///
/// Collects the subresource and query string parameters into one collection
/// ready for canonicalization
///
/// The in-flight request being signed
/// The fused set of parameters
protected static List> GetParametersToCanonicalize(IRequest request)
{
var parametersToCanonicalize = new List>();
if (request.SubResources != null && request.SubResources.Count > 0)
{
foreach (var subResource in request.SubResources)
{
parametersToCanonicalize.Add(new KeyValuePair(subResource.Key, subResource.Value));
}
}
if (request.UseQueryString && request.Parameters != null && request.Parameters.Count > 0)
{
var requestParameters = request.ParameterCollection.GetSortedParametersList();
foreach (var queryParameter in requestParameters.Where(queryParameter => queryParameter.Value != null))
{
parametersToCanonicalize.Add(new KeyValuePair(queryParameter.Key, queryParameter.Value));
}
}
return parametersToCanonicalize;
}
protected static string CanonicalizeQueryParameters(string queryString)
{
return CanonicalizeQueryParameters(queryString, true);
}
///
/// Computes and returns the canonicalized query string, if query parameters have been supplied.
/// Parameters with no value will be canonicalized as 'param='. The expectation is that parameters
/// have not already been url encoded prior to canonicalization.
///
/// The set of parameters being passed on the uri
///
/// Parameters must be uri encoded into the canonical request and by default the signer expects
/// that the supplied collection contains non-encoded data. Set this to false if the encoding was
/// done prior to signer entry.
///
/// The uri encoded query string parameters in canonical ordering
protected static string CanonicalizeQueryParameters(string queryString, bool uriEncodeParameters)
{
if (string.IsNullOrEmpty(queryString))
return string.Empty;
var queryParams = new Dictionary(StringComparer.OrdinalIgnoreCase);
var queryParamsStart = queryString.IndexOf('?');
var qs = queryString.Substring(++queryParamsStart);
var subStringPos = 0;
var index = qs.IndexOfAny(new char[] { '&', ';' }, 0);
if (index == -1 && subStringPos < qs.Length)
index = qs.Length;
while (index != -1)
{
var token = qs.Substring(subStringPos, index - subStringPos);
// If the next character is a space then this isn't the end of query string value
// Content Disposition is an example of this.
if (!(index + 1 < qs.Length && qs[index + 1] == ' '))
{
var equalPos = token.IndexOf('=');
if (equalPos == -1)
queryParams.Add(token, null);
else
queryParams.Add(token.Substring(0, equalPos), token.Substring(equalPos + 1));
subStringPos = index + 1;
}
if (qs.Length <= index + 1)
break;
index = qs.IndexOfAny(new char[] { '&', ';' }, index + 1);
if (index == -1 && subStringPos < qs.Length)
index = qs.Length;
}
return CanonicalizeQueryParameters(queryParams, uriEncodeParameters: uriEncodeParameters);
}
protected static string CanonicalizeQueryParameters(IEnumerable> parameters)
{
return CanonicalizeQueryParameters(parameters, true);
}
///
/// Computes and returns the canonicalized query string, if query parameters have been supplied.
/// Parameters with no value will be canonicalized as 'param='. The expectation is that parameters
/// have not already been url encoded prior to canonicalization.
///
/// The set of parameters to be encoded in the query string
///
/// Parameters must be uri encoded into the canonical request and by default the signer expects
/// that the supplied collection contains non-encoded data. Set this to false if the encoding was
/// done prior to signer entry.
///
/// The uri encoded query string parameters in canonical ordering
protected static string CanonicalizeQueryParameters(
IEnumerable> parameters,
bool uriEncodeParameters)
{
if (parameters == null)
return string.Empty;
var sortedParameters = parameters.OrderBy(kvp => kvp.Key, StringComparer.Ordinal).ToList();
var canonicalQueryString = new StringBuilder();
foreach (var param in sortedParameters)
{
var key = param.Key;
var value = param.Value;
if (canonicalQueryString.Length > 0)
canonicalQueryString.Append("&");
if (uriEncodeParameters)
{
if (string.IsNullOrEmpty(value))
canonicalQueryString.AppendFormat("{0}=", AWSSDKUtils.UrlEncode(key, false));
else
canonicalQueryString.AppendFormat("{0}={1}", AWSSDKUtils.UrlEncode(key, false), AWSSDKUtils.UrlEncode(value, false));
}
else
{
if (string.IsNullOrEmpty(value))
canonicalQueryString.AppendFormat("{0}=", key);
else
canonicalQueryString.AppendFormat("{0}={1}", key, value);
}
}
return canonicalQueryString.ToString();
}
///
/// Returns the request parameters in the form of a query string.
///
/// The request instance
/// Request parameters in query string format
static byte[] GetRequestPayloadBytes(IRequest request)
{
if (request.Content != null)
return request.Content;
var content = request.UseQueryString ? string.Empty : AWSSDKUtils.GetParametersAsString(request);
return Encoding.UTF8.GetBytes(content);
}
#endregion
}
///
/// AWS4 protocol signer for Amazon S3 presigned urls.
///
public class AWS4PreSignedUrlSigner : AWS4Signer
{
// 7 days is the maximum period for presigned url expiry with AWS4
public const Int64 MaxAWS4PreSignedUrlExpiry = 7 * 24 * 60 * 60;
public static readonly IEnumerable ServicesUsingUnsignedPayload = new HashSet()
{
"s3",
"s3-object-lambda",
"s3-outposts"
};
///
/// Calculates and signs the specified request using the AWS4 signing protocol by using the
/// AWS account credentials given in the method parameters. The resulting signature is added
/// to the request headers as 'Authorization'.
///
///
/// The request to compute the signature for. Additional headers mandated by the AWS4 protocol
/// ('host' and 'x-amz-date') will be added to the request before signing.
///
///
/// Adding supporting data for the service call required by the signer (notably authentication
/// region, endpoint and service name).
///
///
/// Metrics for the request
///
///
/// The AWS public key for the account making the service call.
///
///
/// The AWS secret key for the account making the call, in clear text
///
///
/// If any problems are encountered while signing the request.
///
public override void Sign(IRequest request,
IClientConfig clientConfig,
RequestMetrics metrics,
string awsAccessKeyId,
string awsSecretAccessKey)
{
throw new InvalidOperationException("PreSignedUrl signature computation is not supported by this method; use SignRequest instead.");
}
///
/// Calculates and signs the specified request using the AWS4 signing protocol by using the
/// AWS account credentials given in the method parameters. The resulting signature is added
/// to the request headers as 'Authorization'.
///
///
/// The request to compute the signature for. Additional headers mandated by the AWS4 protocol
/// ('host' and 'x-amz-date') will be added to the request before signing.
///
///
/// Adding supporting data for the service call required by the signer (notably authentication
/// region, endpoint and service name).
///
///
/// Metrics for the request
///
///
/// The AWS credentials for the account making the service call.
///
///
/// If any problems are encountered while signing the request.
///
public override void Sign(IRequest request,
IClientConfig clientConfig,
RequestMetrics metrics,
ImmutableCredentials credentials)
{
throw new InvalidOperationException("PreSignedUrl signature computation is not supported by this method; use SignRequest instead.");
}
///
/// Calculates the AWS4 signature for a presigned url.
///
///
/// The request to compute the signature for. Additional headers mandated by the AWS4 protocol
/// ('host' and 'x-amz-date') will be added to the request before signing. If the Expires parameter
/// is present, it is renamed to 'X-Amz-Expires' before signing.
///
///
/// Adding supporting data for the service call required by the signer (notably authentication
/// region, endpoint and service name).
///
///
/// Metrics for the request
///
///
/// The AWS public key for the account making the service call.
///
///
/// The AWS secret key for the account making the call, in clear text
///
///
/// If any problems are encountered while signing the request.
///
///
/// Parameters passed as part of the resource path should be uri-encoded prior to
/// entry to the signer. Parameters passed in the request.Parameters collection should
/// be not be encoded; encoding will be done for these parameters as part of the
/// construction of the canonical request.
///
public new AWS4SigningResult SignRequest(IRequest request,
IClientConfig clientConfig,
RequestMetrics metrics,
string awsAccessKeyId,
string awsSecretAccessKey)
{
var service = "s3";
if (!string.IsNullOrEmpty(request.OverrideSigningServiceName))
service = request.OverrideSigningServiceName;
return SignRequest(request, clientConfig, metrics, awsAccessKeyId, awsSecretAccessKey, service, null);
}
///
/// Calculates the AWS4 signature for a presigned url.
///
///
/// The request to compute the signature for. Additional headers mandated by the AWS4 protocol
/// ('host' and 'x-amz-date') will be added to the request before signing. If the Expires parameter
/// is present, it is renamed to 'X-Amz-Expires' before signing.
///
///
/// Adding supporting data for the service call required by the signer (notably authentication
/// region, endpoint and service name).
///
///
/// Metrics for the request
///
///
/// The AWS public key for the account making the service call.
///
///
/// The AWS secret key for the account making the call, in clear text
///
///
/// The service to sign for
///
///
/// The region to sign to, if null then the region the client is configured for will be used.
///
///
/// If any problems are encountered while signing the request.
///
///
/// Parameters passed as part of the resource path should be uri-encoded prior to
/// entry to the signer. Parameters passed in the request.Parameters collection should
/// be not be encoded; encoding will be done for these parameters as part of the
/// construction of the canonical request.
///
/// The X-Amz-Content-SHA256 is cleared out of the request.
/// If the request is for S3 then the UNSIGNED_PAYLOAD value is used to generate the canonical request.
/// If the request isn't for S3 then the empty body SHA is used to generate the canonical request.
///
public static AWS4SigningResult SignRequest(IRequest request,
IClientConfig clientConfig,
RequestMetrics metrics,
string awsAccessKeyId,
string awsSecretAccessKey,
string service,
string overrideSigningRegion)
{
if (service == "s3")
{
// Older versions of the S3 package can be used with newer versions of Core, this guarantees no double encoding will be used.
// The new behavior uses endpoint resolution rules, which are not present prior to 3.7.100
request.UseDoubleEncoding = false;
}
// clean up any prior signature in the headers if resigning
request.Headers.Remove(HeaderKeys.AuthorizationHeader);
if (!request.Headers.ContainsKey(HeaderKeys.HostHeader))
{
var hostHeader = request.Endpoint.Host;
if (!request.Endpoint.IsDefaultPort)
hostHeader += ":" + request.Endpoint.Port;
request.Headers.Add(HeaderKeys.HostHeader, hostHeader);
}
var signedAt = CorrectClockSkew.GetCorrectedUtcNowForEndpoint(request.Endpoint.ToString());
var region = overrideSigningRegion ?? DetermineSigningRegion(clientConfig, clientConfig.RegionEndpointServiceName, request.AlternateEndpoint, request);
// AWS4 presigned urls got S3 are expected to contain a 'UNSIGNED-PAYLOAD' magic string
// during signing (other services use the empty-body sha)
if (request.Headers.ContainsKey(HeaderKeys.XAmzContentSha256Header))
request.Headers.Remove(HeaderKeys.XAmzContentSha256Header);
var sortedHeaders = SortAndPruneHeaders(request.Headers);
var canonicalizedHeaderNames = CanonicalizeHeaderNames(sortedHeaders);
var parametersToCanonicalize = GetParametersToCanonicalize(request);
parametersToCanonicalize.Add(new KeyValuePair(HeaderKeys.XAmzAlgorithm, AWS4AlgorithmTag));
var xAmzCredentialValue = string.Format(CultureInfo.InvariantCulture, "{0}/{1}/{2}/{3}/{4}",
awsAccessKeyId,
FormatDateTime(signedAt, AWSSDKUtils.ISO8601BasicDateFormat),
region,
service,
Terminator);
parametersToCanonicalize.Add(new KeyValuePair(HeaderKeys.XAmzCredential, xAmzCredentialValue));
parametersToCanonicalize.Add(new KeyValuePair(HeaderKeys.XAmzDateHeader, FormatDateTime(signedAt, AWSSDKUtils.ISO8601BasicDateTimeFormat)));
parametersToCanonicalize.Add(new KeyValuePair(HeaderKeys.XAmzSignedHeadersHeader, canonicalizedHeaderNames));
var canonicalQueryParams = CanonicalizeQueryParameters(parametersToCanonicalize);
var canonicalRequest = CanonicalizeRequest(request.Endpoint,
request.ResourcePath,
request.HttpMethod,
sortedHeaders,
canonicalQueryParams,
ServicesUsingUnsignedPayload.Contains(service) ? UnsignedPayload : EmptyBodySha256,
request.PathResources,
request.UseDoubleEncoding);
if (metrics != null)
metrics.AddProperty(Metric.CanonicalRequest, canonicalRequest);
return ComputeSignature(awsAccessKeyId,
awsSecretAccessKey,
region,
signedAt,
service,
canonicalizedHeaderNames,
canonicalRequest,
metrics);
}
}
}