From f6c7cbebcd852addc7e06a5fa6210d6da292f2cb Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Sun, 8 Jun 2025 03:24:59 +0300 Subject: [PATCH 01/13] Add managed Sigv4a signer. --- .../Credentials/Internal/AwsV4aAuthScheme.cs | 2 +- .../Internal/Auth/AWS4Signer.cs | 19 +- .../Internal/Auth/AWS4aSigner.cs | 574 ++++++++++++++++++ .../Auth/AWSEndpointAuthSchemeSigner.cs | 2 +- .../Internal/Auth/AbstractAWSSigner.cs | 14 +- .../Amazon.Runtime/Internal/Auth/S3Signer.cs | 3 +- .../Util/ChunkedUploadWrapperStream.cs | 52 +- sdk/src/Core/GlobalSuppressions.cs | 2 +- .../S3/Custom/AmazonS3Client.Extensions.cs | 4 +- 9 files changed, 615 insertions(+), 57 deletions(-) create mode 100644 sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigner.cs diff --git a/sdk/src/Core/Amazon.Runtime/Credentials/Internal/AwsV4aAuthScheme.cs b/sdk/src/Core/Amazon.Runtime/Credentials/Internal/AwsV4aAuthScheme.cs index cbd7c8dadda3..54bdd3809433 100644 --- a/sdk/src/Core/Amazon.Runtime/Credentials/Internal/AwsV4aAuthScheme.cs +++ b/sdk/src/Core/Amazon.Runtime/Credentials/Internal/AwsV4aAuthScheme.cs @@ -39,7 +39,7 @@ public ISigner Signer() { if (_signer == null) { - Interlocked.Exchange(ref _signer, new AWS4aSignerCRTWrapper()); + Interlocked.Exchange(ref _signer, new AWS4aSigner()); } return _signer; diff --git a/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4Signer.cs b/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4Signer.cs index 3753da711ea2..0db77aed030d 100644 --- a/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4Signer.cs +++ b/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4Signer.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). @@ -31,7 +31,6 @@ namespace Amazon.Runtime.Internal.Auth /// public class AWS4Signer : AbstractAWSSigner { - public const string Scheme = "AWS4"; public const string Algorithm = "HMAC-SHA256"; public const string Sigv4aAlgorithm = "ECDSA-P256-SHA256"; @@ -299,7 +298,7 @@ private static void CleanHeaders(IDictionary headers) } } - private static void ValidateRequest(IRequest request) + internal static void ValidateRequest(IRequest request) { Uri url = request.Endpoint; @@ -630,7 +629,7 @@ public static byte[] ComputeHash(byte[] data) #endregion #region Private Signing Helpers - static string SetPayloadSignatureHeader(IRequest request, string payloadHash) + internal static string SetPayloadSignatureHeader(IRequest request, string payloadHash) { if (request.Headers.ContainsKey(HeaderKeys.XAmzContentSha256Header)) request.Headers[HeaderKeys.XAmzContentSha256Header] = payloadHash; @@ -704,7 +703,7 @@ public static string DetermineService(IClientConfig clientConfig, IRequest reque /// will look for the hash as a header on the request. /// /// Canonicalised request as a string - protected static string CanonicalizeRequest(Uri endpoint, + protected internal static string CanonicalizeRequest(Uri endpoint, string resourcePath, string httpMethod, IDictionary sortedHeaders, @@ -728,7 +727,7 @@ protected static string CanonicalizeRequest(Uri endpoint, /// will look for the hash as a header on the request. /// /// Canonicalised request as a string - protected static string CanonicalizeRequest(Uri endpoint, + protected internal static string CanonicalizeRequest(Uri endpoint, string resourcePath, string httpMethod, IDictionary sortedHeaders, @@ -761,7 +760,7 @@ protected static string CanonicalizeRequest(Uri endpoint, /// /// Encode "/" when canonicalize resource path /// Canonicalised request as a string - protected static string CanonicalizeRequest(Uri endpoint, + protected internal static string CanonicalizeRequest(Uri endpoint, string resourcePath, string httpMethod, IDictionary sortedHeaders, @@ -866,7 +865,7 @@ protected internal static string CanonicalizeHeaders(IEnumerable /// The headers included in the signature /// Formatted string of header names - protected static string CanonicalizeHeaderNames(IEnumerable> sortedHeaders) + protected internal static string CanonicalizeHeaderNames(IEnumerable> sortedHeaders) { var builder = new ValueStringBuilder(512); @@ -886,7 +885,7 @@ protected static string CanonicalizeHeaderNames(IEnumerable /// The in-flight request being signed /// The fused set of parameters - protected static List> GetParametersToCanonicalize(IRequest request) + protected internal static List> GetParametersToCanonicalize(IRequest request) { var parametersToCanonicalize = new List>(); @@ -968,7 +967,7 @@ protected static string CanonicalizeQueryParameters(string queryString, bool uri return CanonicalizeQueryParameters(queryParams, uriEncodeParameters: uriEncodeParameters); } - protected static string CanonicalizeQueryParameters(IEnumerable> parameters) + protected internal static string CanonicalizeQueryParameters(IEnumerable> parameters) { return CanonicalizeQueryParameters(parameters, true); } diff --git a/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigner.cs b/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigner.cs new file mode 100644 index 000000000000..799d96ee3391 --- /dev/null +++ b/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigner.cs @@ -0,0 +1,574 @@ +/* + * 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.Util; +using Amazon.Runtime.Internal.Util; +using Amazon.Runtime.Identity; +using System.Security.Cryptography; +using System.Runtime.CompilerServices; + +using static Amazon.Runtime.Internal.Auth.AWS4Signer; + +namespace Amazon.Runtime.Internal.Auth +{ + /// + /// AWS4a protocol signer for service calls that transmit authorization in the header field "Authorization". + /// + public class AWS4aSigner : AbstractAWSSigner + { + internal const string Scheme = "AWS4A"; + + /// + /// Represents the elliptic curve used for signing requests with the AWS4a protocol. + /// + private static readonly ECCurve SigningCurve = ECCurve.NamedCurves.nistP256; + /// + /// Represents the value of N-2, where N is the order of the + /// NIST P-256 curve group. + /// + private static readonly byte[] NMinus2 = new byte[] { + 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xBC, 0xE6, 0xFA, 0xAD, 0xA7, 0x17, 0x9E, 0x84, + 0xF3, 0xB9, 0xCA, 0xC2, 0xFC, 0x63, 0x25, 0x4F, + }; + + internal const SigningAlgorithm SignerAlgorithm = SigningAlgorithm.HmacSHA256; + + public AWS4aSigner() + : this(true) + { + } + + public AWS4aSigner(bool signPayload) + { + SignPayload = signPayload; + } + + public bool SignPayload { get; } + + public override ClientProtocol Protocol + { + get { return ClientProtocol.RestProtocol; } + } + + /// + /// Calculates and signs the specified request using the AWS4a 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 AWS4a 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, + BaseIdentity identity) + { + if (identity is not AWSCredentials credentials) + { + throw new AmazonClientException($"The identity parameter must be of type AWSCredentials for the signer {nameof(AWS4aSigner)}."); + } + + var immutableCredentials = credentials.GetCredentials(); + if (immutableCredentials is null) + { + return; + } + + var signingResult = SignRequest(request, clientConfig, metrics, immutableCredentials); + request.AWS4aSignerResult = signingResult; + request.Headers[HeaderKeys.AuthorizationHeader] = signingResult.ForAuthorizationHeader; + } + + /// + /// Calculates and signs the specified request using the AWS4a 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 AWS4a 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. + /// + /// + /// 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 AWS4aSigningResult SignRequest(IRequest request, + IClientConfig clientConfig, + RequestMetrics metrics, + ImmutableCredentials credentials) + { + ValidateRequest(request); + var signedAt = InitializeHeaders(request.Headers, request.Endpoint); + + var serviceSigningName = !string.IsNullOrEmpty(request.OverrideSigningServiceName) + ? request.OverrideSigningServiceName + : DetermineService(clientConfig, request); + + 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 region = DetermineSigningRegion(clientConfig, clientConfig.RegionEndpointServiceName, request.AlternateEndpoint, request); + request.DeterminedSigningRegion = region; + request.Headers[HeaderKeys.XAmzRegionSetHeader] = region; + 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 + ? V4aStreamingBodySha256WithTrailer + : V4aStreamingBodySha256; + + var bodyHash = SetRequestBodyHash(request, SignPayload, bodySha, ChunkedUploadWrapperStream.V4A_SIGNATURE_LENGTH); + var sortedHeaders = SortAndPruneHeaders(request.Headers); + + var canonicalRequest = CanonicalizeRequest(request.Endpoint, + request.ResourcePath, + request.HttpMethod, + sortedHeaders, + canonicalParameters, + bodyHash, + request.PathResources, + request.UseDoubleEncoding); + metrics?.AddProperty(Metric.CanonicalRequest, canonicalRequest); + request.SignatureVersion = SignatureVersion.SigV4a; + return ComputeSignature(credentials, + request.DeterminedSigningRegion, + signedAt, + serviceSigningName, + CanonicalizeHeaderNames(sortedHeaders), + canonicalRequest, + metrics); + } + + #region Public Signing Helpers + + /// + /// Computes and returns an AWS4a signature for the specified canonicalized request + /// + /// + /// + /// + /// + /// + /// + /// + public static AWS4aSigningResult ComputeSignature(ImmutableCredentials credentials, + string regionSet, + DateTime signedAt, + string service, + string signedHeaders, + string canonicalRequest) + { + return ComputeSignature(credentials, regionSet, signedAt, service, signedHeaders, canonicalRequest, null); + } + + /// + /// Computes and returns an AWS4a signature for the specified canonicalized request + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static AWS4aSigningResult ComputeSignature(ImmutableCredentials credentials, + string regionSet, + 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}", dateStamp, service, Terminator); + + var stringToSignBuilder = new StringBuilder(); + stringToSignBuilder.AppendFormat(CultureInfo.InvariantCulture, "{0}\n{1}\n{2}\n", + AWS4aAlgorithmTag, + FormatDateTime(signedAt, AWSSDKUtils.ISO8601BasicDateTimeFormat), + scope); + + var canonicalRequestHashBytes = AWS4Signer.ComputeHash(canonicalRequest); + stringToSignBuilder.Append(AWSSDKUtils.ToHex(canonicalRequestHashBytes, true)); + + metrics?.AddProperty(Metric.StringToSign, stringToSignBuilder); + + using var key = ComputeSigningKey(credentials); + + var stringToSign = stringToSignBuilder.ToString(); + var signature = AWSSDKUtils.ToHex(SignBlob(key, stringToSign), true); + return new AWS4aSigningResult(credentials.AccessKey, signedAt, signedHeaders, scope, regionSet, signature, service, "", credentials); + } + + /// + /// Adds one to a large unsigned integer represented by a sequence of bytes in constant time. + /// + /// + /// Implementation adapted from . + /// + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private static void AddOneConstantTime(byte[] data) + { + uint carry = 1; + + for (int i = 0; i < data.Length; i++) + { + int index = data.Length - i - 1; + uint current_digit = data[index] + carry; + carry = (current_digit >> 8) & 1; + data[index] = (byte)(current_digit & 0xFF); + } + } + + /// + /// Compares two byte arrays in constant time. + /// + /// The first byte array to compare. + /// The second byte array to compare. + /// and have a different length. + /// + /// Implementation adapted from . + /// + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private static int CompareConstantTime(byte[] lhs, byte[] rhs) + { + if (lhs.Length != rhs.Length) + throw new ArgumentException("Arrays must be of equal length for constant time comparison."); + + byte gt = 0, eq = 0; + + for (int i = 0; i < lhs.Length; i++) + { + int lhs_digit = lhs[i]; + int rhs_digit = rhs[i]; + + gt |= (byte)(((rhs_digit - lhs_digit) >> 31) & eq); + eq &= (byte)((((lhs_digit ^ rhs_digit) - 1) >> 31) & 1); + } + + return gt + gt + eq - 1; + } + + /// + /// Compute and return the signing key for the request. + /// + /// The credentials. + /// Computed signing key + public static ECDsa ComputeSigningKey(ImmutableCredentials credentials) + { + byte[] kvalue = null; + byte[] ksecret = null; + + try + { + ksecret = Encoding.UTF8.GetBytes(Scheme + awsSecretAccessKey); + + // The key value is constructed as follows: + // 0x00000001 || "AWS4-ECDSA-P256-SHA256" || 0x00 || AccessKeyId || CounterValue as uint8_t || 0x00000100(Length) + kvalue = new byte[sizeof(uint) + AWS4aAlgorithmTag.Length + 1 + Encoding.UTF8.GetByteCount(awsAccessKey) + 1 + sizeof(uint)]; + int idx = 0; + kvalue[idx + 3] = 1; + idx += 4; + idx += Encoding.UTF8.GetBytes(AWS4aAlgorithmTag, 0, AWS4aAlgorithmTag.Length, kvalue, idx); + idx++; + idx += Encoding.UTF8.GetBytes(credentials.AccessKey, 0, credentials.AccessKey.Length, kvalue, idx); + ref byte counterValue = ref kvalue[idx++]; + kvalue[idx + 2] = 1; + + counterValue = 1; + while (counterValue < 0xFF) + { + byte[] kDerived = ComputeKeyedHash(SignerAlgorithm, ksecret, kvalue); + if (CompareConstantTime(kDerived, NMinus2) > 0) + { + // increment the counter value + counterValue++; + continue; + } + + AddOneConstantTime(kDerived); + var ecdsa = ECDsa.Create(new ECParameters + { + Curve = SigningCurve, + D = kDerived, + }); + Array.Clear(kDerived, 0, kDerived.Length); + return ecdsa; + } + + throw new AmazonClientException("Failed to derive a SigV4a key for the request."); + } + finally + { + // clean up all secrets, regardless of how initially seeded (for simplicity) + if (ksecret != null) + Array.Clear(ksecret, 0, ksecret.Length); + if (kvalue != null) + Array.Clear(kvalue, 0, kvalue.Length); + } + } + + /// + /// Returns the ECDSA signature for an arbitrary blob using the specified key + /// + /// The key to use. + /// The data to sign. + public static byte[] SignBlob(ECDsa key, string data) + { + return SignBlob(key, Encoding.UTF8.GetBytes(data)); + } + + /// + /// Returns the ECDSA signature for an arbitrary blob using the specified key + /// + /// The key to use. + /// The data to sign. + public static byte[] SignBlob(ECDsa key, byte[] data) + { + return key.SignData(data, HashAlgorithmName.SHA256); + } + #endregion + } + + /// + /// AWS4a protocol signer for Amazon S3 presigned urls. + /// + public class AWS4aPreSignedUrlSigner : AWS4aSigner + { + // 7 days is the maximum period for presigned url expiry with AWS4a + public const Int64 MaxAWS4aPreSignedUrlExpiry = 7 * 24 * 60 * 60; + + public static readonly IEnumerable ServicesUsingUnsignedPayload = AWS4PreSignedUrlSigner.ServicesUsingUnsignedPayload; + + /// + /// Calculates and signs the specified request using the AWS4a 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 AWS4a 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, + BaseIdentity identity) + { + throw new InvalidOperationException("PreSignedUrl signature computation is not supported by this method; use SignRequest instead."); + } + + /// + /// Calculates the AWS4a signature for a presigned url. + /// + /// + /// The request to compute the signature for. Additional headers mandated by the AWS4a 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 credentials for the account making the service call. + /// + /// + /// 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 static new AWS4aSigningResult SignRequest(IRequest request, + IClientConfig clientConfig, + RequestMetrics metrics, + ImmutableCredentials credentials) + { + var service = "s3"; + if (!string.IsNullOrEmpty(request.OverrideSigningServiceName)) + service = request.OverrideSigningServiceName; + return SignRequest(request, clientConfig, metrics, credentials, service, null); + } + + /// + /// Calculates the AWS4a signature for a presigned url. + /// + /// + /// The request to compute the signature for. Additional headers mandated by the AWS4a 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 credentials for the account making the service call. + /// + /// + /// 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 AWS4aSigningResult SignRequest(IRequest request, + IClientConfig clientConfig, + RequestMetrics metrics, + ImmutableCredentials credentials, + string service, + string overrideSigningRegion) + { + if (service == "s3" || service == "s3express") + { + // 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()); + request.SignedAt = signedAt; + var regionSet = overrideSigningRegion ?? DetermineSigningRegion(clientConfig, clientConfig.RegionEndpointServiceName, request.AlternateEndpoint, request); + + // AWS4a presigned urls got S3 are expected to contain a 'UNSIGNED-PAYLOAD' magic string + // during signing (other services use the empty-body sha) + request.Headers.Remove(HeaderKeys.XAmzContentSha256Header); + + var sortedHeaders = SortAndPruneHeaders(request.Headers); + var canonicalizedHeaderNames = CanonicalizeHeaderNames(sortedHeaders); + + var parametersToCanonicalize = GetParametersToCanonicalize(request); + parametersToCanonicalize.Add(new KeyValuePair(HeaderKeys.XAmzAlgorithm, AWS4aAlgorithmTag)); + parametersToCanonicalize.Add(new KeyValuePair(HeaderKeys.XAmzRegionSetHeader, regionSet)); + var xAmzCredentialValue = string.Format(CultureInfo.InvariantCulture, "{0}/{1}/{2}/{3}", + credentials.AccessKey, + FormatDateTime(signedAt, AWSSDKUtils.ISO8601BasicDateFormat), + 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); + metrics?.AddProperty(Metric.CanonicalRequest, canonicalRequest); + + return ComputeSignature(credentials, + regionSet, + signedAt, + service, + canonicalizedHeaderNames, + canonicalRequest, + metrics); + } + } +} diff --git a/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWSEndpointAuthSchemeSigner.cs b/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWSEndpointAuthSchemeSigner.cs index e9cd7d5d5d5d..93ff7a68f72d 100644 --- a/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWSEndpointAuthSchemeSigner.cs +++ b/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWSEndpointAuthSchemeSigner.cs @@ -34,7 +34,7 @@ public override void Sign(IRequest request, IClientConfig clientConfig, RequestM { var useSigV4 = request.SignatureVersion == SignatureVersion.SigV4; var signer = SelectSigner(this, useSigV4, request, clientConfig); - var aws4aSigner = signer as AWS4aSignerCRTWrapper; + var aws4aSigner = signer as AWS4aSigner; var aws4Signer = signer as AWS4Signer; var useV4a = aws4aSigner != null; var useV4 = aws4Signer != null; diff --git a/sdk/src/Core/Amazon.Runtime/Internal/Auth/AbstractAWSSigner.cs b/sdk/src/Core/Amazon.Runtime/Internal/Auth/AbstractAWSSigner.cs index 7afd8fe2286e..7bfae5ce9fca 100644 --- a/sdk/src/Core/Amazon.Runtime/Internal/Auth/AbstractAWSSigner.cs +++ b/sdk/src/Core/Amazon.Runtime/Internal/Auth/AbstractAWSSigner.cs @@ -44,21 +44,21 @@ private AWS4Signer AWS4SignerInstance } } - private AWS4aSignerCRTWrapper _aws4aSignerCRTWrapper; - private AWS4aSignerCRTWrapper AWS4aSignerCRTWrapperInstance + private AWS4aSigner _aws4aSigner; + private AWS4aSigner AWS4aSignerInstance { get { - if (_aws4aSignerCRTWrapper == null) + if (_aws4aSigner == null) { lock (_lock) { - if (_aws4aSignerCRTWrapper == null) - _aws4aSignerCRTWrapper = new AWS4aSignerCRTWrapper(); + if (_aws4aSigner == null) + _aws4aSigner = new AWS4aSigner(); } } - return _aws4aSignerCRTWrapper; + return _aws4aSigner; } } @@ -171,7 +171,7 @@ protected AbstractAWSSigner SelectSigner(AbstractAWSSigner defaultSigner,bool us IRequest request, IClientConfig config) { if (request.SignatureVersion == SignatureVersion.SigV4a) - return AWS4aSignerCRTWrapperInstance; + return AWS4aSignerInstance; else if (UseV4Signing(useSigV4Setting, request, config)) return AWS4SignerInstance; else diff --git a/sdk/src/Core/Amazon.Runtime/Internal/Auth/S3Signer.cs b/sdk/src/Core/Amazon.Runtime/Internal/Auth/S3Signer.cs index 798fb0393ebc..3531f4b286ae 100644 --- a/sdk/src/Core/Amazon.Runtime/Internal/Auth/S3Signer.cs +++ b/sdk/src/Core/Amazon.Runtime/Internal/Auth/S3Signer.cs @@ -55,7 +55,7 @@ public override void Sign(IRequest request, IClientConfig clientConfig, RequestM { var signer = SelectSigner(this, true, request, clientConfig); var aws4Signer = signer as AWS4Signer; - var aws4aSigner = signer as AWS4aSignerCRTWrapper; + var aws4aSigner = signer as AWS4aSigner; var useV4 = aws4Signer != null; var useV4a = aws4aSigner != null; @@ -73,6 +73,7 @@ public override void Sign(IRequest request, IClientConfig clientConfig, RequestM if (useV4a) { var signingResult = aws4aSigner.SignRequest(request, clientConfig, metrics, immutableCredentials); + request.Headers[HeaderKeys.AuthorizationHeader] = signingResult.ForAuthorizationHeader; if (request.UseChunkEncoding) { request.AWS4aSignerResult = signingResult; diff --git a/sdk/src/Core/Amazon.Runtime/Internal/Util/ChunkedUploadWrapperStream.cs b/sdk/src/Core/Amazon.Runtime/Internal/Util/ChunkedUploadWrapperStream.cs index 01ab2da21cae..24068edb3048 100644 --- a/sdk/src/Core/Amazon.Runtime/Internal/Util/ChunkedUploadWrapperStream.cs +++ b/sdk/src/Core/Amazon.Runtime/Internal/Util/ChunkedUploadWrapperStream.cs @@ -93,10 +93,6 @@ internal ChunkedUploadWrapperStream(Stream stream, int wrappedStreamBufferSize, { throw new AmazonClientException($"{nameof(ChunkedUploadWrapperStream)} was initialized without a SigV4 or SigV4a signing result."); } - else if (headerSigningResult is AWS4aSigningResult) - { - Sigv4aSigner = new AWS4aSignerCRTWrapper(); - } HeaderSigningResult = headerSigningResult; PreviousChunkSignature = headerSigningResult?.Signature; @@ -286,11 +282,6 @@ private async Task FillInputBufferAsync(CancellationToken cancellationToken /// private AWSSigningResultBase HeaderSigningResult { get; set; } - /// - /// SigV4a signer - /// - private AWS4aSignerCRTWrapper Sigv4aSigner { get; set; } - /// /// Computed signature of the chunk prior to the one in-flight in hex, /// for either SigV4 or SigV4a @@ -321,23 +312,17 @@ private void ConstructOutputBufferChunk(int dataLen) // variable-length size of the embedded chunk data in hex chunkHeader.Append(dataLen.ToString("X", CultureInfo.InvariantCulture)); + var chunkStringToSign = BuildChunkedStringToSign(CHUNK_STRING_TO_SIGN_PREFIX, HeaderSigningResult.ISO8601DateTime, + HeaderSigningResult.Scope, PreviousChunkSignature, dataLen, _inputBuffer); + string chunkSignature = ""; if (HeaderSigningResult is AWS4aSigningResult v4aHeaderSigningResult) { - if (isFinalDataChunk) // _inputBuffer still contains previous chunk, but this is the final 0 content chunk so sign null - { - chunkSignature = Sigv4aSigner.SignChunk(null, PreviousChunkSignature, v4aHeaderSigningResult); - } - else - { - chunkSignature = Sigv4aSigner.SignChunk(new MemoryStream(_inputBuffer), PreviousChunkSignature, v4aHeaderSigningResult); - } + using var signingKey = AWS4aSigner.ComputeSigningKey(v4aHeaderSigningResult.Credentials); + chunkSignature = AWSSDKUtils.ToHex(AWS4aSigner.SignBlob(signingKey, chunkStringToSign), true); } else if (HeaderSigningResult is AWS4SigningResult v4HeaderSingingResult) // SigV4 { - var chunkStringToSign = BuildChunkedStringToSign(CHUNK_STRING_TO_SIGN_PREFIX, v4HeaderSingingResult.ISO8601DateTime, - v4HeaderSingingResult.Scope, PreviousChunkSignature, dataLen, _inputBuffer); - chunkSignature = AWSSDKUtils.ToHex(AWS4Signer.SignBlob(v4HeaderSingingResult.GetSigningKey(), chunkStringToSign), true); } @@ -408,24 +393,25 @@ private string ConstructSignedTrailersChunk() _trailingHeaders[ChecksumUtils.GetChecksumHeaderKey(_trailingChecksum)] = Convert.ToBase64String(_hashAlgorithm.Hash); } - string chunkSignature; - if (HeaderSigningResult is AWS4SigningResult) - { - var sortedTrailingHeaders = AWS4Signer.SortAndPruneHeaders(_trailingHeaders); - var canonicalizedTrailingHeaders = AWS4Signer.CanonicalizeHeaders(sortedTrailingHeaders); + var sortedTrailingHeaders = AWS4Signer.SortAndPruneHeaders(_trailingHeaders); + var canonicalizedTrailingHeaders = AWS4Signer.CanonicalizeHeaders(sortedTrailingHeaders); - var chunkStringToSign = - TRAILING_HEADER_STRING_TO_SIGN_PREFIX + "\n" + - HeaderSigningResult.ISO8601DateTime + "\n" + - HeaderSigningResult.Scope + "\n" + - PreviousChunkSignature + "\n" + - AWSSDKUtils.ToHex(AWS4Signer.ComputeHash(canonicalizedTrailingHeaders), true); + var chunkStringToSign = + TRAILING_HEADER_STRING_TO_SIGN_PREFIX + "\n" + + HeaderSigningResult.ISO8601DateTime + "\n" + + HeaderSigningResult.Scope + "\n" + + PreviousChunkSignature + "\n" + + AWSSDKUtils.ToHex(AWS4Signer.ComputeHash(canonicalizedTrailingHeaders), true); - chunkSignature = AWSSDKUtils.ToHex(AWS4Signer.SignBlob(((AWS4SigningResult)HeaderSigningResult).GetSigningKey(), chunkStringToSign), true); + string chunkSignature; + if (HeaderSigningResult is AWS4SigningResult result) + { + chunkSignature = AWSSDKUtils.ToHex(AWS4Signer.SignBlob(result.GetSigningKey(), chunkStringToSign), true); } else // SigV4a { - chunkSignature = Sigv4aSigner.SignTrailingHeaderChunk(_trailingHeaders, PreviousChunkSignature, (AWS4aSigningResult)HeaderSigningResult).PadRight(V4A_SIGNATURE_LENGTH, '*'); + using var signingKey = AWS4aSigner.ComputeSigningKey(((AWS4aSigningResult)HeaderSigningResult).Credentials); + chunkSignature = AWSSDKUtils.ToHex(AWS4aSigner.SignBlob(signingKey, chunkStringToSign), true).PadRight(V4A_SIGNATURE_LENGTH, '*'); } var chunk = new StringBuilder(); diff --git a/sdk/src/Core/GlobalSuppressions.cs b/sdk/src/Core/GlobalSuppressions.cs index 0eac98387a68..0aa19d9c8dd7 100644 --- a/sdk/src/Core/GlobalSuppressions.cs +++ b/sdk/src/Core/GlobalSuppressions.cs @@ -196,7 +196,7 @@ [module: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Scope = "member", Target = "Amazon.Runtime.Internal.Auth.AWS4Signer.#DetermineSigningRegion(Amazon.Runtime.IClientConfig,System.String,Amazon.RegionEndpoint,Amazon.Runtime.Internal.IRequest)")] [module: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Scope = "member", Target = "Amazon.Runtime.Internal.Auth.AWS4Signer.#CanonicalizeHeaders(System.Collections.Generic.IDictionary`2)")] [module: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Scope = "member", Target = "Amazon.Runtime.Internal.Auth.AWS4Signer.#CanonicalizeHeaderNames(System.Collections.Generic.IDictionary`2)")] -[module: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Scope = "member", Target = "Amazon.Runtime.Internal.Marshaller.#ToUserAgentHeaderString(Amazon.Runtime.RequestRetryMode)")] +[module: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Scope = "member", Target = "Amazon.Runtime.Internal.Auth.AWS4aSigner")] [assembly: SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Scope = "member", Target = "~M:Amazon.Runtime.Internal.Util.IniFile.SeekProperty(System.Int32@,System.String@,System.String@,Amazon.Runtime.Internal.Util.NestedProperty@)~System.Boolean")] [assembly: SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Scope = "member", Target = "~M:Amazon.Runtime.Internal.Util.IniFile.TryParseSubproperties(System.Int32@,System.String,Amazon.Runtime.Internal.Util.NestedProperty@)~System.Boolean")] diff --git a/sdk/src/Services/S3/Custom/AmazonS3Client.Extensions.cs b/sdk/src/Services/S3/Custom/AmazonS3Client.Extensions.cs index f05f900342f9..b05d6c9395b7 100644 --- a/sdk/src/Services/S3/Custom/AmazonS3Client.Extensions.cs +++ b/sdk/src/Services/S3/Custom/AmazonS3Client.Extensions.cs @@ -382,9 +382,7 @@ private static SigningResult ReturnSigningResult(SignatureVersion signatureVersi switch (signatureVersionToUse) { case SignatureVersion.SigV4a: - var aws4aSigner = new AWS4aSignerCRTWrapper(); - - var signingResult4a = aws4aSigner.Presign4a(iRequest, + var signingResult4a = AWS4aPreSignedUrlSigner.SignRequest(iRequest, config, metrics, immutableCredentials, From 56db341d9a2518f4607d3650141572bf3855d584 Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Sun, 22 Jun 2025 00:09:47 +0300 Subject: [PATCH 02/13] Cache SigV4a key in the `ImmutableCredentials` object. --- .../Credentials/ImmutableCredentials.cs | 9 +++++++ .../Internal/Auth/AWS4aSigner.cs | 26 +++++++++---------- .../Util/ChunkedUploadWrapperStream.cs | 11 ++++---- .../SharedInterfaces/IAWSSigV4aProvider.cs | 2 +- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/sdk/src/Core/Amazon.Runtime/Credentials/ImmutableCredentials.cs b/sdk/src/Core/Amazon.Runtime/Credentials/ImmutableCredentials.cs index 4bb0e2f0bbd5..3b7cd3175c87 100644 --- a/sdk/src/Core/Amazon.Runtime/Credentials/ImmutableCredentials.cs +++ b/sdk/src/Core/Amazon.Runtime/Credentials/ImmutableCredentials.cs @@ -15,6 +15,7 @@ using Amazon.Runtime.Internal.Util; using Amazon.Util; using System; +using System.Security.Cryptography; namespace Amazon.Runtime { @@ -55,6 +56,14 @@ public class ImmutableCredentials /// Account-based endpoints take the form https://accountid.ddb.region.amazonaws.com /// public string AccountId { get; private set; } + + /// + /// Gets a cached AWS4a signing key for the current credentials. + /// + /// + /// The key must not be disposed, and must not be returned to the user. + /// + internal ECDsa AWS4aSigningKey { get; set; } #endregion diff --git a/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigner.cs b/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigner.cs index 799d96ee3391..81c8bb049996 100644 --- a/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigner.cs +++ b/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigner.cs @@ -250,10 +250,8 @@ public static AWS4aSigningResult ComputeSignature(ImmutableCredentials credentia metrics?.AddProperty(Metric.StringToSign, stringToSignBuilder); - using var key = ComputeSigningKey(credentials); - var stringToSign = stringToSignBuilder.ToString(); - var signature = AWSSDKUtils.ToHex(SignBlob(key, stringToSign), true); + var signature = AWSSDKUtils.ToHex(SignBlob(credentials, stringToSign), true); return new AWS4aSigningResult(credentials.AccessKey, signedAt, signedHeaders, scope, regionSet, signature, service, "", credentials); } @@ -309,9 +307,10 @@ private static int CompareConstantTime(byte[] lhs, byte[] rhs) /// /// Compute and return the signing key for the request. /// - /// The credentials. + /// Access key credential. + /// Secret access key credential. /// Computed signing key - public static ECDsa ComputeSigningKey(ImmutableCredentials credentials) + public static ECDsa ComputeSigningKey(string awsAccessKey, string awsSecretAccessKey) { byte[] kvalue = null; byte[] ksecret = null; @@ -328,7 +327,7 @@ public static ECDsa ComputeSigningKey(ImmutableCredentials credentials) idx += 4; idx += Encoding.UTF8.GetBytes(AWS4aAlgorithmTag, 0, AWS4aAlgorithmTag.Length, kvalue, idx); idx++; - idx += Encoding.UTF8.GetBytes(credentials.AccessKey, 0, credentials.AccessKey.Length, kvalue, idx); + idx += Encoding.UTF8.GetBytes(awsAccessKey, 0, awsAccessKey.Length, kvalue, idx); ref byte counterValue = ref kvalue[idx++]; kvalue[idx + 2] = 1; @@ -366,22 +365,23 @@ public static ECDsa ComputeSigningKey(ImmutableCredentials credentials) } /// - /// Returns the ECDSA signature for an arbitrary blob using the specified key + /// Returns the ECDSA signature for an arbitrary blob using the specified credentials. /// - /// The key to use. + /// The credentials to use. /// The data to sign. - public static byte[] SignBlob(ECDsa key, string data) + public static byte[] SignBlob(ImmutableCredentials credentials, string data) { - return SignBlob(key, Encoding.UTF8.GetBytes(data)); + return SignBlob(credentials, Encoding.UTF8.GetBytes(data)); } /// - /// Returns the ECDSA signature for an arbitrary blob using the specified key + /// Returns the ECDSA signature for an arbitrary blob using the specified credentials. /// - /// The key to use. + /// The credentials to use. /// The data to sign. - public static byte[] SignBlob(ECDsa key, byte[] data) + public static byte[] SignBlob(ImmutableCredentials credentials, byte[] data) { + var key = credentials.AWS4aSigningKey ??= ComputeSigningKey(credentials.AccessKey, credentials.SecretKey); return key.SignData(data, HashAlgorithmName.SHA256); } #endregion diff --git a/sdk/src/Core/Amazon.Runtime/Internal/Util/ChunkedUploadWrapperStream.cs b/sdk/src/Core/Amazon.Runtime/Internal/Util/ChunkedUploadWrapperStream.cs index 24068edb3048..e4d5e514c716 100644 --- a/sdk/src/Core/Amazon.Runtime/Internal/Util/ChunkedUploadWrapperStream.cs +++ b/sdk/src/Core/Amazon.Runtime/Internal/Util/ChunkedUploadWrapperStream.cs @@ -318,8 +318,7 @@ private void ConstructOutputBufferChunk(int dataLen) string chunkSignature = ""; if (HeaderSigningResult is AWS4aSigningResult v4aHeaderSigningResult) { - using var signingKey = AWS4aSigner.ComputeSigningKey(v4aHeaderSigningResult.Credentials); - chunkSignature = AWSSDKUtils.ToHex(AWS4aSigner.SignBlob(signingKey, chunkStringToSign), true); + chunkSignature = AWSSDKUtils.ToHex(AWS4aSigner.SignBlob(v4aHeaderSigningResult.Credentials, chunkStringToSign), true); } else if (HeaderSigningResult is AWS4SigningResult v4HeaderSingingResult) // SigV4 { @@ -404,14 +403,14 @@ private string ConstructSignedTrailersChunk() AWSSDKUtils.ToHex(AWS4Signer.ComputeHash(canonicalizedTrailingHeaders), true); string chunkSignature; - if (HeaderSigningResult is AWS4SigningResult result) + if (HeaderSigningResult is AWS4SigningResult aws4Result) { - chunkSignature = AWSSDKUtils.ToHex(AWS4Signer.SignBlob(result.GetSigningKey(), chunkStringToSign), true); + chunkSignature = AWSSDKUtils.ToHex(AWS4Signer.SignBlob(aws4Result.GetSigningKey(), chunkStringToSign), true); } else // SigV4a { - using var signingKey = AWS4aSigner.ComputeSigningKey(((AWS4aSigningResult)HeaderSigningResult).Credentials); - chunkSignature = AWSSDKUtils.ToHex(AWS4aSigner.SignBlob(signingKey, chunkStringToSign), true).PadRight(V4A_SIGNATURE_LENGTH, '*'); + var aws4aResult = (AWS4aSigningResult)HeaderSigningResult; + chunkSignature = AWSSDKUtils.ToHex(AWS4aSigner.SignBlob(aws4aResult.Credentials, chunkStringToSign), true).PadRight(V4A_SIGNATURE_LENGTH, '*'); } var chunk = new StringBuilder(); diff --git a/sdk/src/Core/Amazon.Runtime/SharedInterfaces/IAWSSigV4aProvider.cs b/sdk/src/Core/Amazon.Runtime/SharedInterfaces/IAWSSigV4aProvider.cs index 2e20b21daa2e..ec90499c8cb9 100644 --- a/sdk/src/Core/Amazon.Runtime/SharedInterfaces/IAWSSigV4aProvider.cs +++ b/sdk/src/Core/Amazon.Runtime/SharedInterfaces/IAWSSigV4aProvider.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). From 5bcabbe17fa05fdec9e6fe2c82104988acb47638 Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Sun, 22 Jun 2025 00:21:27 +0300 Subject: [PATCH 03/13] Obsolete `AWS4aSignerCRTWrapper` and related APIs. --- .../CrtAWS4aSigner.cs | 2 ++ .../Internal/Auth/AWS4aSignerCRTWrapper.cs | 2 ++ .../Pipeline/Handlers/BaseEndpointResolver.cs | 29 ------------------- .../SharedInterfaces/IAWSSigV4aProvider.cs | 3 ++ .../GlobalRuntimeDependencyRegistry.cs | 5 +++- 5 files changed, 11 insertions(+), 30 deletions(-) diff --git a/extensions/src/AWSSDK.Extensions.CrtIntegration/CrtAWS4aSigner.cs b/extensions/src/AWSSDK.Extensions.CrtIntegration/CrtAWS4aSigner.cs index ad3361423e93..297760b72aa8 100644 --- a/extensions/src/AWSSDK.Extensions.CrtIntegration/CrtAWS4aSigner.cs +++ b/extensions/src/AWSSDK.Extensions.CrtIntegration/CrtAWS4aSigner.cs @@ -24,6 +24,7 @@ using AWSSDK.Extensions.CrtIntegration; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.IO; using System.Linq; @@ -34,6 +35,7 @@ namespace Amazon.Extensions.CrtIntegration /// /// Asymmetric Sigv4 (SigV4a) protocol signer using the implementation provided by Aws.Crt.Auth /// + [Obsolete("Use Amazon.Runtime.Internal.Auth.AWS4aSigner instead."), EditorBrowsable(EditorBrowsableState.Never)] public class CrtAWS4aSigner : IAWSSigV4aProvider { /// diff --git a/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSignerCRTWrapper.cs b/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSignerCRTWrapper.cs index 03f5c76cd930..a3f9159563e1 100644 --- a/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSignerCRTWrapper.cs +++ b/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSignerCRTWrapper.cs @@ -19,6 +19,7 @@ using Amazon.Util.Internal; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; @@ -29,6 +30,7 @@ namespace Amazon.Runtime.Internal.Auth /// /// Asymmetric SigV4 signer using a the AWS Common Runtime implementation of SigV4a via AWSSDK.Extensions.CrtIntegration /// + [Obsolete("Use AWS4aSigner instead."), EditorBrowsable(EditorBrowsableState.Never)] public class AWS4aSignerCRTWrapper : AbstractAWSSigner { internal const string CRT_WRAPPER_ASSEMBLY_NAME = "AWSSDK.Extensions.CrtIntegration"; diff --git a/sdk/src/Core/Amazon.Runtime/Pipeline/Handlers/BaseEndpointResolver.cs b/sdk/src/Core/Amazon.Runtime/Pipeline/Handlers/BaseEndpointResolver.cs index 5991a79634aa..87b66c5f2ee5 100644 --- a/sdk/src/Core/Amazon.Runtime/Pipeline/Handlers/BaseEndpointResolver.cs +++ b/sdk/src/Core/Amazon.Runtime/Pipeline/Handlers/BaseEndpointResolver.cs @@ -162,16 +162,6 @@ private static void SetAuthenticationAndHeaders(IRequest request, Endpoint endpo } case "sigv4a": { - // If there are multiple authentication schemes but the CRT dependency is not available, - // we will proceed to check the next value in authSchemes. - if (hasMultipleSchemes) - { - if (!IsCrtDependencyAvailable()) - { - continue; - } - } - request.SignatureVersion = SignatureVersion.SigV4a; var signingRegions = ((List)schema["signingRegionSet"]).OfType().ToArray(); @@ -220,25 +210,6 @@ private static void ApplyCommonSchema(IRequest request, PropertyBag schema) } } - /// - /// Validates whether the CRT dependency is available by trying to create an instance. - /// - /// - /// True if the CRT package is available at runtime, false otherwise. - /// - private static bool IsCrtDependencyAvailable() - { - try - { - var signer = new AWS4aSignerCRTWrapper(); - return signer != null; - } - catch (AWSCommonRuntimeException) - { - return false; - } - } - /// /// Inject host prefix into request endpoint. /// diff --git a/sdk/src/Core/Amazon.Runtime/SharedInterfaces/IAWSSigV4aProvider.cs b/sdk/src/Core/Amazon.Runtime/SharedInterfaces/IAWSSigV4aProvider.cs index ec90499c8cb9..19c4baaa0557 100644 --- a/sdk/src/Core/Amazon.Runtime/SharedInterfaces/IAWSSigV4aProvider.cs +++ b/sdk/src/Core/Amazon.Runtime/SharedInterfaces/IAWSSigV4aProvider.cs @@ -16,7 +16,9 @@ using Amazon.Runtime.Internal; using Amazon.Runtime.Internal.Auth; using Amazon.Runtime.Internal.Util; +using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; namespace Amazon.Runtime.SharedInterfaces @@ -24,6 +26,7 @@ namespace Amazon.Runtime.SharedInterfaces /// /// Interface for an asymmetric SigV4 (SigV4a) signer /// + [Obsolete("SigV4a provider is now built-in; this interface is no longer necessary to implement."), EditorBrowsable(EditorBrowsableState.Never)] public interface IAWSSigV4aProvider { /// diff --git a/sdk/src/Core/RuntimeDependencies/GlobalRuntimeDependencyRegistry.cs b/sdk/src/Core/RuntimeDependencies/GlobalRuntimeDependencyRegistry.cs index 443db2a10c46..e796b9f28be3 100644 --- a/sdk/src/Core/RuntimeDependencies/GlobalRuntimeDependencyRegistry.cs +++ b/sdk/src/Core/RuntimeDependencies/GlobalRuntimeDependencyRegistry.cs @@ -15,6 +15,8 @@ using Amazon.Runtime.Internal; using Amazon.Runtime.Internal.Auth; using AWSSDK.Runtime.Internal.Util; +using System; +using System.ComponentModel; namespace Amazon.RuntimeDependencies { @@ -60,10 +62,11 @@ public void RegisterChecksumProvider(object instance) /// GlobalRuntimeDependencyRegistry.Instance.RegisterSigV4aProvider((context) => /// { /// return new Amazon.Extensions.CrtIntegration.CrtAWS4aSigner(context.SigV4aCrtSignerContextData.Payload); - /// } + /// }); /// /// /// + [Obsolete("SigV4a provider is now built-in; this method is no longer necessary to call."), EditorBrowsable(EditorBrowsableState.Never)] public void RegisterSigV4aProvider(RuntimeDependencyFactory factory) { RegisterInstance(AWS4aSignerCRTWrapper.CRT_WRAPPER_ASSEMBLY_NAME, AWS4aSignerCRTWrapper.CRT_WRAPPER_CLASS_NAME, factory); From 5daa0c1d5549837459b2f3dc4ea28050964bc80e Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Sat, 19 Jul 2025 15:14:11 +0300 Subject: [PATCH 04/13] Add debugger display for `ValueStringBuilder`. --- .../Core/ThirdParty/RuntimeBackports/ValueStringBuilder.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sdk/src/Core/ThirdParty/RuntimeBackports/ValueStringBuilder.cs b/sdk/src/Core/ThirdParty/RuntimeBackports/ValueStringBuilder.cs index 2e421a7222f5..a9e28037048d 100644 --- a/sdk/src/Core/ThirdParty/RuntimeBackports/ValueStringBuilder.cs +++ b/sdk/src/Core/ThirdParty/RuntimeBackports/ValueStringBuilder.cs @@ -12,6 +12,7 @@ namespace ThirdParty.RuntimeBackports { + [DebuggerDisplay("{DebuggerDisplay,nq}")] #pragma warning disable CA1815 internal ref struct ValueStringBuilder #pragma warning restore CA1815 @@ -45,6 +46,9 @@ public int Length } } + // ToString() has side effects, so we don't want to call it from the debugger display. + private string DebuggerDisplay => AsSpan().ToString(); + public int Capacity => _chars.Length; public void EnsureCapacity(int capacity) From c54ed3024fb449eaaef536647ab57bdc250fd3c0 Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Sat, 19 Jul 2025 16:21:41 +0300 Subject: [PATCH 05/13] Encode ECDSA signature based on RFC 3279. --- sdk/src/Core/AWSSDK.Core.NetFramework.csproj | 1 + sdk/src/Core/AWSSDK.Core.NetStandard.csproj | 7 ++++- .../Internal/Auth/AWS4aSigner.cs | 26 ++++++++++++++++--- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/sdk/src/Core/AWSSDK.Core.NetFramework.csproj b/sdk/src/Core/AWSSDK.Core.NetFramework.csproj index 5e8f36044fc1..2e6b45ea1e56 100644 --- a/sdk/src/Core/AWSSDK.Core.NetFramework.csproj +++ b/sdk/src/Core/AWSSDK.Core.NetFramework.csproj @@ -70,6 +70,7 @@ + - + + + + + \ No newline at end of file diff --git a/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigner.cs b/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigner.cs index 81c8bb049996..23efc47c0ab3 100644 --- a/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigner.cs +++ b/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigner.cs @@ -18,11 +18,15 @@ using System.Linq; using System.Text; using System.Globalization; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; using Amazon.Util; using Amazon.Runtime.Internal.Util; using Amazon.Runtime.Identity; -using System.Security.Cryptography; -using System.Runtime.CompilerServices; + +#if !NET7_0_OR_GREATER +using System.Formats.Asn1; +#endif using static Amazon.Runtime.Internal.Auth.AWS4Signer; @@ -382,8 +386,24 @@ public static byte[] SignBlob(ImmutableCredentials credentials, string data) public static byte[] SignBlob(ImmutableCredentials credentials, byte[] data) { var key = credentials.AWS4aSigningKey ??= ComputeSigningKey(credentials.AccessKey, credentials.SecretKey); - return key.SignData(data, HashAlgorithmName.SHA256); +#if NET7_0_OR_GREATER + return key.SignData(data, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence); +#else + return ConvertToRfc3279DerSequence(key.SignData(data, HashAlgorithmName.SHA256)); +#endif + } + +#if !NET7_0_OR_GREATER + private static byte[] ConvertToRfc3279DerSequence(byte[] signature) + { + var writer = new AsnWriter(AsnEncodingRules.DER); + writer.PushSequence(); + writer.WriteIntegerUnsigned(signature.AsSpan(0, signature.Length / 2)); // R value + writer.WriteIntegerUnsigned(signature.AsSpan(signature.Length / 2)); // S value + writer.PopSequence(); + return writer.Encode(); } +#endif #endregion } From 14243ac223a9dafc3e390a8cfa5f489cbdeb7b1e Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Sat, 19 Jul 2025 16:35:49 +0300 Subject: [PATCH 06/13] Add test. --- .../UnitTests/Core/AWS4aSignerTests.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 sdk/test/NetStandard/UnitTests/Core/AWS4aSignerTests.cs diff --git a/sdk/test/NetStandard/UnitTests/Core/AWS4aSignerTests.cs b/sdk/test/NetStandard/UnitTests/Core/AWS4aSignerTests.cs new file mode 100644 index 000000000000..254438a11600 --- /dev/null +++ b/sdk/test/NetStandard/UnitTests/Core/AWS4aSignerTests.cs @@ -0,0 +1,40 @@ +/* + * 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.Internal.Auth; +using Amazon.Util; +using Xunit; + +namespace UnitTests.NetStandard.Core +{ + public class AWS4aSignerTests + { + private const string SigningTestAccessKeyId = "AKIDEXAMPLE"; + private const string SigningTestSecretAccessKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"; + + /* The public coordinates of the ecc key derived from the above credentials pair */ + private const string SigningTestEccPubX = "b6618f6a65740a99e650b33b6b4b5bd0d43b176d721a3edfea7e7d2d56d936b1"; + private const string SigningTestEccPubY = "865ed22a7eadc9c5cb9d2cbaca1b3699139fedc5043dc6661864218330c8e518"; + + [Fact] + public void DeriveSigningKey() + { + using var key = AWS4aSigner.ComputeSigningKey(SigningTestAccessKeyId, SigningTestSecretAccessKey); + var parameters = key.ExportParameters(false); + Assert.Equal(AWSSDKUtils.HexStringToBytes(SigningTestEccPubX), parameters.Q.X); + Assert.Equal(AWSSDKUtils.HexStringToBytes(SigningTestEccPubY), parameters.Q.Y); + } + } +} From feeb0f442b639d1cc280a74f8bd282501d67c21c Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Sat, 19 Jul 2025 21:02:17 +0300 Subject: [PATCH 07/13] Fix presigned URL support. --- .../Internal/Auth/AWS4aSigningResult.cs | 24 ++++++++++++++++++- .../S3/Custom/AmazonS3Client.Extensions.cs | 3 ++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigningResult.cs b/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigningResult.cs index c5130dc4acdf..fd4d331e9657 100644 --- a/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigningResult.cs +++ b/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigningResult.cs @@ -13,7 +13,9 @@ * permissions and limitations under the License. */ +using Amazon.Util; using System; +using System.Globalization; using System.Text; namespace Amazon.Runtime.Internal.Auth @@ -38,7 +40,7 @@ public class AWS4aSigningResult : AWSSigningResultBase /// The access key that was included in the signature /// Date/time (UTC) that the signature was computed /// The collection of headers names that were included in the signature - /// Formatted 'scope' value for signing (YYYYMMDD/region/service/aws4_request) + /// Formatted 'scope' value for signing (YYYYMMDD/service/aws4_request) /// The set of AWS regions this signature is valid for /// Computed signature /// Service the request was signed for @@ -87,6 +89,25 @@ public override string ForAuthorizationHeader } } + /// + /// Returns the signature in a form usable as a set of query string parameters. + /// + public string ForQueryParameters + { + get + { + var authParams = new StringBuilder() + .AppendFormat("{0}={1}", AWSSDKUtils.UrlEncode(HeaderKeys.XAmzAlgorithm, false), AWSSDKUtils.UrlEncode(AWS4Signer.AWS4aAlgorithmTag, false)) + .AppendFormat("&{0}={1}", AWSSDKUtils.UrlEncode(HeaderKeys.XAmzRegionSetHeader, false), AWSSDKUtils.UrlEncode(RegionSet, false)) + .AppendFormat("&{0}={1}", AWSSDKUtils.UrlEncode(HeaderKeys.XAmzCredential, false), AWSSDKUtils.UrlEncode(string.Format(CultureInfo.InvariantCulture, "{0}/{1}", AccessKeyId, Scope), false)) + .AppendFormat("&{0}={1}", AWSSDKUtils.UrlEncode(HeaderKeys.XAmzDateHeader, false), AWSSDKUtils.UrlEncode(ISO8601DateTime, false)) + .AppendFormat("&{0}={1}", AWSSDKUtils.UrlEncode(HeaderKeys.XAmzSignedHeadersHeader, false), AWSSDKUtils.UrlEncode(SignedHeaders, false)) + .AppendFormat("&{0}={1}", AWSSDKUtils.UrlEncode(HeaderKeys.XAmzSignature, false), AWSSDKUtils.UrlEncode(Signature, false)); + + return authParams.ToString(); + } + } + /// /// Returns the set of regions this signature is valid for /// @@ -99,6 +120,7 @@ public string RegionSet /// /// Returns the full presigned Uri /// + [Obsolete("This property is always empty in objects returned by AWS4aSigner. Use the ForQueryParameters property instead, to get the query parameters for a presigned URL.")] public string PresignedUri { get { return _presignedUri; } diff --git a/sdk/src/Services/S3/Custom/AmazonS3Client.Extensions.cs b/sdk/src/Services/S3/Custom/AmazonS3Client.Extensions.cs index b05d6c9395b7..2474e5e6fff3 100644 --- a/sdk/src/Services/S3/Custom/AmazonS3Client.Extensions.cs +++ b/sdk/src/Services/S3/Custom/AmazonS3Client.Extensions.cs @@ -388,7 +388,8 @@ private static SigningResult ReturnSigningResult(SignatureVersion signatureVersi immutableCredentials, "s3", arn.IsMRAPArn() ? "*" : ""); - signingResult.Result = signingResult4a.PresignedUri; + signingResult.Authorization = "&" + signingResult4a.ForQueryParameters; + signingResult.Result = ComposeUrl(iRequest).AbsoluteUri + signingResult.Authorization; break; case SignatureVersion.SigV4: var aws4Signer = new AWS4PreSignedUrlSigner(); From 13934d1b0e7a58d1c3b5fed1f0ab98b21bbc2f90 Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Sat, 19 Jul 2025 21:16:17 +0300 Subject: [PATCH 08/13] Update README. --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 9e6d2f4a02a8..ca13af630a98 100644 --- a/README.md +++ b/README.md @@ -71,11 +71,9 @@ You can find the archive for _**legacy**_ Unity support at https://github.com/aw This SDK has optional functionality that requires the [AWS Common Runtime (CRT)](https://docs.aws.amazon.com/sdkref/latest/guide/common-runtime.html) bindings to be included as a dependency with your application. This functionality includes: -* [Amazon S3 Multi-Region Access Points](https://docs.aws.amazon.com/AmazonS3/latest/userguide/MultiRegionAccessPoints.html) * [Amazon S3 Object Integrity](https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html) -* Amazon EventBridge Global Endpoints -If the required AWS Common Runtime components are not installed you will receive an error like `Attempting to make a request that requires an implementation of AWS Signature V4a. Add a reference to the AWSSDK.Extensions.CrtIntegration NuGet package to your project to include the AWS Signature V4a signer.`, +If the required AWS Common Runtime components are not installed you will receive an error like `Attempting to handle a request that requires additional checksums. Add a reference to the AWSSDK.Extensions.CrtIntegration NuGet package to your project to include the AWS Common Runtime checksum implementation.`, indicating that the required dependency is missing to use the associated functionality. To install this dependency follow the provided [instructions](#installing-the-aws-common-runtime-crt-dependency). From 8e4c132e205d90e7e89a172d95a5d83ba9b7d560 Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Sat, 19 Jul 2025 21:53:27 +0300 Subject: [PATCH 09/13] Add DevConfig. --- .../b495f893-31aa-4e17-866c-af78e4da6e8b.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 generator/.DevConfigs/b495f893-31aa-4e17-866c-af78e4da6e8b.json diff --git a/generator/.DevConfigs/b495f893-31aa-4e17-866c-af78e4da6e8b.json b/generator/.DevConfigs/b495f893-31aa-4e17-866c-af78e4da6e8b.json new file mode 100644 index 000000000000..df3bceb7e824 --- /dev/null +++ b/generator/.DevConfigs/b495f893-31aa-4e17-866c-af78e4da6e8b.json @@ -0,0 +1,10 @@ +{ + "core": { + "updateMinimum": false, + "type": "patch", + "changeLogMessages": [ + "Added AWS4aSigner class that implements SigV4a signing in managed code. AWS CRT is no longer used by the SDK for this purpose, and the related APIs were deprecated.", + "The AWS4aSigningResult.PresignedUri property was deprecated and will always be empty in objects returned by AWS4aSigner. Use the ForQueryParameters property instead, to get the query parameters for a presigned URL." + ] + } +} \ No newline at end of file From f44257adc13b30bdb5e7ca2b17054cf735ecea63 Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Sat, 19 Jul 2025 23:11:41 +0300 Subject: [PATCH 10/13] Simplify `AwsV4aAuthScheme`. Now that the signer is managed, we create it at static initialization. --- .../Credentials/Internal/AwsV4aAuthScheme.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/sdk/src/Core/Amazon.Runtime/Credentials/Internal/AwsV4aAuthScheme.cs b/sdk/src/Core/Amazon.Runtime/Credentials/Internal/AwsV4aAuthScheme.cs index 54bdd3809433..784fcd7c7030 100644 --- a/sdk/src/Core/Amazon.Runtime/Credentials/Internal/AwsV4aAuthScheme.cs +++ b/sdk/src/Core/Amazon.Runtime/Credentials/Internal/AwsV4aAuthScheme.cs @@ -13,7 +13,6 @@ * permissions and limitations under the License. */ -using System.Threading; using Amazon.Runtime.Identity; using Amazon.Runtime.Internal.Auth; @@ -25,7 +24,7 @@ namespace Amazon.Runtime.Credentials.Internal /// public class AwsV4aAuthScheme : IAuthScheme { - private static ISigner _signer; + private static readonly ISigner _signer = new AWS4aSigner(); /// public string SchemeId => AuthSchemeOption.SigV4A; @@ -35,14 +34,6 @@ public IIdentityResolver GetIdentityResolver(IIdentityResolverConfiguration conf => configuration.GetIdentityResolver(); /// - public ISigner Signer() - { - if (_signer == null) - { - Interlocked.Exchange(ref _signer, new AWS4aSigner()); - } - - return _signer; - } + public ISigner Signer() => _signer; } } From d5d2aa90290f27d60f19a46e1d8d90b46b4e286e Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Sun, 20 Jul 2025 00:19:43 +0300 Subject: [PATCH 11/13] Make `AWS4aPreSignedUrlSigner` a static class. --- .../Internal/Auth/AWS4aSigner.cs | 48 ++++--------------- 1 file changed, 9 insertions(+), 39 deletions(-) diff --git a/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigner.cs b/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigner.cs index 23efc47c0ab3..e21044bfc9ee 100644 --- a/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigner.cs +++ b/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigner.cs @@ -410,43 +410,13 @@ private static byte[] ConvertToRfc3279DerSequence(byte[] signature) /// /// AWS4a protocol signer for Amazon S3 presigned urls. /// - public class AWS4aPreSignedUrlSigner : AWS4aSigner + public static class AWS4aPreSignedUrlSigner { // 7 days is the maximum period for presigned url expiry with AWS4a public const Int64 MaxAWS4aPreSignedUrlExpiry = 7 * 24 * 60 * 60; public static readonly IEnumerable ServicesUsingUnsignedPayload = AWS4PreSignedUrlSigner.ServicesUsingUnsignedPayload; - /// - /// Calculates and signs the specified request using the AWS4a 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 AWS4a 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, - BaseIdentity identity) - { - throw new InvalidOperationException("PreSignedUrl signature computation is not supported by this method; use SignRequest instead."); - } - /// /// Calculates the AWS4a signature for a presigned url. /// @@ -474,7 +444,7 @@ public override void Sign(IRequest request, /// be not be encoded; encoding will be done for these parameters as part of the /// construction of the canonical request. /// - public static new AWS4aSigningResult SignRequest(IRequest request, + public static AWS4aSigningResult SignRequest(IRequest request, IClientConfig clientConfig, RequestMetrics metrics, ImmutableCredentials credentials) @@ -582,13 +552,13 @@ public static AWS4aSigningResult SignRequest(IRequest request, request.UseDoubleEncoding); metrics?.AddProperty(Metric.CanonicalRequest, canonicalRequest); - return ComputeSignature(credentials, - regionSet, - signedAt, - service, - canonicalizedHeaderNames, - canonicalRequest, - metrics); + return AWS4aSigner.ComputeSignature(credentials, + regionSet, + signedAt, + service, + canonicalizedHeaderNames, + canonicalRequest, + metrics); } } } From 14dd45dc21db2e30cfad6cefa1f3ecac31c867e1 Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Sun, 20 Jul 2025 00:34:54 +0300 Subject: [PATCH 12/13] Fix spaces. --- sdk/src/Core/AWSSDK.Core.NetStandard.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/src/Core/AWSSDK.Core.NetStandard.csproj b/sdk/src/Core/AWSSDK.Core.NetStandard.csproj index 14d1e4f07310..c17436064712 100644 --- a/sdk/src/Core/AWSSDK.Core.NetStandard.csproj +++ b/sdk/src/Core/AWSSDK.Core.NetStandard.csproj @@ -89,8 +89,8 @@ - + From f3cf72d94de8bd8a4704db465c7840dc1b6df456 Mon Sep 17 00:00:00 2001 From: Theodore Tsirpanis Date: Sat, 26 Jul 2025 23:25:55 +0300 Subject: [PATCH 13/13] Refactor caching the signing key. The logic is moved to a separate function, and we guard from temporary resource leaks if multiple threads try to populate the cache. --- .../Credentials/ImmutableCredentials.cs | 5 ++-- .../Internal/Auth/AWS4aSigner.cs | 23 ++++++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/sdk/src/Core/Amazon.Runtime/Credentials/ImmutableCredentials.cs b/sdk/src/Core/Amazon.Runtime/Credentials/ImmutableCredentials.cs index 3b7cd3175c87..76ebd4ce8e4a 100644 --- a/sdk/src/Core/Amazon.Runtime/Credentials/ImmutableCredentials.cs +++ b/sdk/src/Core/Amazon.Runtime/Credentials/ImmutableCredentials.cs @@ -58,12 +58,13 @@ public class ImmutableCredentials public string AccountId { get; private set; } /// - /// Gets a cached AWS4a signing key for the current credentials. + /// Contains a cached AWS4a signing key for the current credentials. Do + /// not access this property directly; use instead. /// /// /// The key must not be disposed, and must not be returned to the user. /// - internal ECDsa AWS4aSigningKey { get; set; } + internal ECDsa AWS4aSigningKey; #endregion diff --git a/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigner.cs b/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigner.cs index e21044bfc9ee..2fd4e32fc1a8 100644 --- a/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigner.cs +++ b/sdk/src/Core/Amazon.Runtime/Internal/Auth/AWS4aSigner.cs @@ -23,6 +23,7 @@ using Amazon.Util; using Amazon.Runtime.Internal.Util; using Amazon.Runtime.Identity; +using System.Threading; #if !NET7_0_OR_GREATER using System.Formats.Asn1; @@ -368,6 +369,26 @@ public static ECDsa ComputeSigningKey(string awsAccessKey, string awsSecretAcces } } + private static ECDsa GetCachedSigningKey(ImmutableCredentials credentials) + { + // First, check if the credentials already have a cached signing key. + if (credentials.AWS4aSigningKey is { } key) + { + return key; + } + + // Otherwise, compute one and try setting it in a thread-safe manner. + ECDsa newKey = ComputeSigningKey(credentials.AccessKey, credentials.SecretKey); + ECDsa existingKey = Interlocked.CompareExchange(ref credentials.AWS4aSigningKey, newKey, null); + if (existingKey != null) + { + // If another thread beat us to setting the key, use that, and dispose the one we generated, to save resources. + newKey.Dispose(); + return existingKey; + } + return newKey; + } + /// /// Returns the ECDSA signature for an arbitrary blob using the specified credentials. /// @@ -385,7 +406,7 @@ public static byte[] SignBlob(ImmutableCredentials credentials, string data) /// The data to sign. public static byte[] SignBlob(ImmutableCredentials credentials, byte[] data) { - var key = credentials.AWS4aSigningKey ??= ComputeSigningKey(credentials.AccessKey, credentials.SecretKey); + var key = GetCachedSigningKey(credentials); #if NET7_0_OR_GREATER return key.SignData(data, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence); #else