/*
* 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 Amazon.Runtime;
using Amazon.Runtime.Internal;
using Amazon.Runtime.Internal.Auth;
using Amazon.Runtime.Internal.Util;
using Amazon.Runtime.SharedInterfaces;
using Amazon.Util;
using Aws.Crt.Auth;
using Aws.Crt.Http;
using AWSSDK.Extensions.CrtIntegration;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
namespace Amazon.Extensions.CrtIntegration
{
///
/// Asymmetric Sigv4 (SigV4a) protocol signer using the implementation provided by Aws.Crt.Auth
///
public class CrtAWS4aSigner : IAWSSigV4aProvider
{
public CrtAWS4aSigner() :
this(true)
{ }
public CrtAWS4aSigner(bool signPayload)
{
SignPayload = signPayload;
}
public bool SignPayload
{
get;
private set;
}
///
/// Protocol for the requests being signed
///
public ClientProtocol Protocol
{
get { return ClientProtocol.RestProtocol; }
}
///
/// Calculates and signs the specified request using the Asymmetric SigV4 signing protocol
/// by using theAWS 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
/// SigV4a protocol 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
public void Sign(IRequest request,
IClientConfig clientConfig,
RequestMetrics metrics,
ImmutableCredentials credentials)
{
SignRequest(request, clientConfig, metrics, credentials);
}
///
/// Calculates the signature for the specified request using the Asymmetric SigV4 signing protocol
///
///
/// The request to compute the signature for. Additional headers mandated by the
/// SigV4a protocol 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
/// AWS4aSigningResult for the given request
public AWS4aSigningResult SignRequest(IRequest request,
IClientConfig clientConfig,
RequestMetrics metrics,
ImmutableCredentials credentials)
{
var signedAt = AWS4Signer.InitializeHeaders(request.Headers, request.Endpoint);
var serviceSigningName = !string.IsNullOrEmpty(request.OverrideSigningServiceName) ? request.OverrideSigningServiceName : AWS4Signer.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;
}
var regionSet = AWS4Signer.DetermineSigningRegion(clientConfig, clientConfig.RegionEndpointServiceName, request.AlternateEndpoint, request);
request.DeterminedSigningRegion = regionSet;
AWS4Signer.SetXAmzTrailerHeader(request.Headers, request.TrailingHeaders);
var signingConfig = PrepareCRTSigningConfig(
AwsSignatureType.HTTP_REQUEST_VIA_HEADERS,
regionSet,
serviceSigningName,
signedAt,
credentials,
request.UseDoubleEncoding);
// If the request should use a fixed x-amz-content-sha256 header value, determine the appropriate one
var fixedBodyHash = request.TrailingHeaders?.Count > 0
? AWS4Signer.V4aStreamingBodySha256WithTrailer
: AWS4Signer.V4aStreamingBodySha256;
signingConfig.SignedBodyValue = AWS4Signer.SetRequestBodyHash(request, SignPayload, fixedBodyHash, ChunkedUploadWrapperStream.V4A_SIGNATURE_LENGTH);
var crtRequest = CrtHttpRequestConverter.ConvertToCrtRequest(request);
var signingResult = AwsSigner.SignHttpRequest(crtRequest, signingConfig);
var authorizationValue = Encoding.Default.GetString(signingResult.Get().Signature);
var signedCrtRequest = signingResult.Get().SignedRequest;
CrtHttpRequestConverter.CopyHeadersFromCrtRequest(request, signedCrtRequest);
var dateStamp = AWS4Signer.FormatDateTime(signedAt, AWSSDKUtils.ISO8601BasicDateFormat);
var scope = string.Format(CultureInfo.InvariantCulture, "{0}/{1}/{2}", dateStamp, serviceSigningName, AWS4Signer.Terminator);
AWS4aSigningResult result = new AWS4aSigningResult(
credentials.AccessKey,
signedAt,
CrtHttpRequestConverter.ExtractSignedHeaders(signedCrtRequest),
scope,
regionSet,
authorizationValue,
serviceSigningName,
"",
credentials);
return result;
}
/// ///
/// Calculates the signature for the specified request using the Asymmetric SigV4
/// signing protocol in preparation for generating a presigned URL.
///
///
/// The request to compute the signature for. Additional headers mandated by the
/// SigV4a protocol 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
/// Service to sign the request for
/// Region to sign the request for
/// AWS4aSigningResult for the given request
public AWS4aSigningResult Presign4a(IRequest request,
IClientConfig clientConfig,
RequestMetrics metrics,
ImmutableCredentials credentials,
string serviceSigningName,
string overrideSigningRegion)
{
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;
}
var signedAt = AWS4Signer.InitializeHeaders(request.Headers, request.Endpoint);
var regionSet = overrideSigningRegion ?? AWS4Signer.DetermineSigningRegion(clientConfig, clientConfig.RegionEndpointServiceName, request.AlternateEndpoint, request);
var signingConfig = PrepareCRTSigningConfig(
AwsSignatureType.HTTP_REQUEST_VIA_QUERY_PARAMS,
regionSet,
serviceSigningName,
signedAt,
credentials,
request.UseDoubleEncoding);
if (AWS4PreSignedUrlSigner.ServicesUsingUnsignedPayload.Contains(serviceSigningName))
{
signingConfig.SignedBodyValue = AWS4Signer.UnsignedPayload;
}
else
{
signingConfig.SignedBodyValue = AWS4Signer.EmptyBodySha256;
}
// The expiration may have already be set in a header when marshalling the GetPreSignedUrlRequest -> IRequest
if (request.Parameters != null && request.Parameters.ContainsKey(HeaderKeys.XAmzExpires))
{
signingConfig.ExpirationInSeconds = Convert.ToUInt64(request.Parameters[HeaderKeys.XAmzExpires]);
}
var crtRequest = CrtHttpRequestConverter.ConvertToCrtRequest(request);
var signingResult = AwsSigner.SignHttpRequest(crtRequest, signingConfig);
string authorizationValue = Encoding.Default.GetString(signingResult.Get().Signature);
var dateStamp = AWS4Signer.FormatDateTime(signedAt, AWSSDKUtils.ISO8601BasicDateFormat);
var scope = string.Format(CultureInfo.InvariantCulture, "{0}/{1}/{2}", dateStamp, serviceSigningName, AWS4Signer.Terminator);
AWS4aSigningResult result = new AWS4aSigningResult(
credentials.AccessKey,
signedAt,
CrtHttpRequestConverter.ExtractSignedHeaders(signingResult.Get().SignedRequest),
scope,
regionSet,
authorizationValue,
serviceSigningName,
signingResult.Get().SignedRequest.Uri,
credentials);
return result;
}
///
/// Signs one chunk of a request when transferring a payload using multiple chunks
///
/// Content of the current chunk to sign
/// Signature of the previously signed chunk
/// Signing result for the "seed" signature consisting of headers
/// Signature of the current chunk
public string SignChunk(Stream chunkBody, string previousSignature, AWS4aSigningResult headerSigningResult)
{
var signingConfig = PrepareCRTSigningConfig(AwsSignatureType.HTTP_REQUEST_CHUNK, headerSigningResult);
signingConfig.SignedBodyHeader = AwsSignedBodyHeaderType.NONE;
// The previous signature may be padded with '*' up to 144 characters, which is used
// when actually sending a chunk but not when calculating the next chunk's signature.
previousSignature = previousSignature.TrimEnd('*');
var signingResult = AwsSigner.SignChunk(chunkBody, Encoding.UTF8.GetBytes(previousSignature), signingConfig);
return Encoding.UTF8.GetString(signingResult.Get().Signature);
}
///
/// Signs the final chunk containing trailing headers
///
/// Trailing header keys and values
/// Signature of the previously signed chunk
/// Signing result for the "seed" signature consisting of headers
/// Signature of the trailing header chunk
public string SignTrailingHeaderChunk(IDictionary trailingHeaders, string previousSignature, AWS4aSigningResult headerSigningResult)
{
var signingConfig = PrepareCRTSigningConfig(AwsSignatureType.HTTP_REQUEST_TRAILING_HEADERS, headerSigningResult);
signingConfig.SignedBodyHeader = AwsSignedBodyHeaderType.NONE;
var headerArray = trailingHeaders.Select(kvp => new HttpHeader(kvp.Key, kvp.Value)).ToArray();
// The previous signature may be padded with '*' up to 144 characters, which is used
// when actually sending a chunk but not when calculating the next chunk's signature.
previousSignature = previousSignature.TrimEnd('*');
var signingResult = AwsSigner.SignTrailingHeaders(headerArray, Encoding.UTF8.GetBytes(previousSignature), signingConfig);
return Encoding.UTF8.GetString(signingResult.Get().Signature);
}
///
/// Helper function to set up an Aws.Crt.Auth.SigningConfig
///
/// Signature type
/// Signing result for the request's headers
/// Prepared CRT signing configuration
public AwsSigningConfig PrepareCRTSigningConfig(AwsSignatureType signatureType, AWS4aSigningResult headerSigningResult)
{
return PrepareCRTSigningConfig(signatureType,
headerSigningResult.RegionSet,
headerSigningResult.Service,
headerSigningResult.DateTime,
headerSigningResult.Credentials,
useDoubleEncoding: true);
}
///
/// Helper function to set up an Aws.Crt.Auth.SigningConfig
///
/// Signature type
/// Signing region
/// Service to sign the request for
/// Timestamp to sign at
/// The AWS credentials for the account making the service call
/// Use double uri encoding when required
/// Prepared CRT signing configuration
public AwsSigningConfig PrepareCRTSigningConfig(AwsSignatureType signatureType, string region, string service, DateTime signedAt, ImmutableCredentials credentials, bool useDoubleEncoding)
{
var signingConfig = new AwsSigningConfig
{
Algorithm = AwsSigningAlgorithm.SIGV4A,
SignedBodyHeader = AwsSignedBodyHeaderType.X_AMZ_CONTENT_SHA256,
SignatureType = signatureType,
Region = region,
Service = service,
Timestamp = new DateTimeOffset(signedAt),
Credentials = new Credentials(credentials.AccessKey, credentials.SecretKey, credentials.Token)
};
signingConfig.UseDoubleUriEncode = useDoubleEncoding;
signingConfig.ShouldNormalizeUriPath = useDoubleEncoding;
return signingConfig;
}
}
}