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