// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 using AWSSignatureV4_S3_Sample.Util; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Security.Cryptography; using System.Text; namespace AWSSignatureV4_S3_Sample.Signers { /// /// Sample AWS4 signer demonstrating how to sign requests to Amazon S3 /// using query string parameters. /// public class AWS4SignerForQueryParameterAuth : AWS4SignerBase { /// /// Computes an AWS4 authorization for a request, suitable for embedding /// in query parameters. /// /// /// The request headers; 'Host' and 'X-Amz-Date' will be added to this set. /// /// /// Any query parameters that will be added to the endpoint. The parameters /// should be specified in canonical format. /// /// /// Precomputed SHA256 hash of the request body content; this value should also /// be set as the header 'X-Amz-Content-SHA256' for non-streaming uploads. /// /// /// The user's AWS Access Key. /// /// /// The user's AWS Secret Key. /// /// /// The string expressing the Signature V4 components to add to query parameters. /// public string ComputeSignature(IDictionary headers, string queryParameters, string bodyHash, string awsAccessKey, string awsSecretKey) { // first get the date and time for the subsequent request, and convert to ISO 8601 format // for use in signature generation var requestDateTime = DateTime.UtcNow; var dateTimeStamp = requestDateTime.ToString(ISO8601BasicFormat, CultureInfo.InvariantCulture); // extract the host portion of the endpoint to include in the signature calculation, // unless already set if (!headers.ContainsKey("Host")) { var hostHeader = EndpointUri.Host; if (!EndpointUri.IsDefaultPort) hostHeader += ":" + EndpointUri.Port; headers.Add("Host", hostHeader); } var dateStamp = requestDateTime.ToString(DateStringFormat, CultureInfo.InvariantCulture); var scope = string.Format("{0}/{1}/{2}/{3}", dateStamp, Region, Service, TERMINATOR); // canonicalized headers need to be expressed in the query // parameters processed in the signature var canonicalizedHeaderNames = CanonicalizeHeaderNames(headers); var canonicalizedHeaders = CanonicalizeHeaders(headers); // reform the query parameters to (a) add the parameters required for // Signature V4 and (b) canonicalize the set before they go into the // signature calculation. Note that this assumes parameter names and // values added outside this routine are already url encoded var paramDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); if (!string.IsNullOrEmpty(queryParameters)) { paramDictionary = queryParameters.Split('&').Select(p => p.Split('=')) .ToDictionary(nameval => nameval[0], nameval => nameval.Length > 1 ? nameval[1] : ""); } // add the fixed authorization params required by Signature V4 paramDictionary.Add(X_Amz_Algorithm, HttpHelpers.UrlEncode(string.Format("{0}-{1}", SCHEME, ALGORITHM))); paramDictionary.Add(X_Amz_Credential, HttpHelpers.UrlEncode(string.Format("{0}/{1}", awsAccessKey, scope))); paramDictionary.Add(X_Amz_SignedHeaders, HttpHelpers.UrlEncode(canonicalizedHeaderNames)); // x-amz-date is now added as a query parameter, not a header, but still needs to be in ISO8601 basic form paramDictionary.Add(X_Amz_Date, HttpHelpers.UrlEncode(dateTimeStamp)); // build the expanded canonical query parameter string that will go into the // signature computation var sb = new StringBuilder(); var paramKeys = new List(paramDictionary.Keys); paramKeys.Sort(StringComparer.Ordinal); foreach (var p in paramKeys) { if (sb.Length > 0) sb.Append("&"); sb.AppendFormat("{0}={1}", p, paramDictionary[p]); } var canonicalizedQueryParameters = sb.ToString(); // express all the header and query parameter data as a canonical request string var canonicalRequest = CanonicalizeRequest(EndpointUri, HttpMethod, canonicalizedQueryParameters, canonicalizedHeaderNames, canonicalizedHeaders, bodyHash); //Console.WriteLine("\nCanonicalRequest:\n"+canonicalRequest); byte[] canonicalRequestHashBytes = CanonicalRequestHashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(canonicalRequest)); // construct the string to be signed var stringToSign = new StringBuilder(); stringToSign.AppendFormat("{0}-{1}\n{2}\n{3}\n", SCHEME, ALGORITHM, dateTimeStamp, scope); stringToSign.Append(ToHexString(canonicalRequestHashBytes, true)); //Console.WriteLine("\nStringToSign:\n{0}", stringToSign); // compute the multi-stage signing key KeyedHashAlgorithm kha = KeyedHashAlgorithm.Create(HMACSHA256); kha.Key = DeriveSigningKey(HMACSHA256, awsSecretKey, Region, dateStamp, Service); // compute the final signature for the request, place into the result and return to the // user to be embedded in the request as needed var signature = kha.ComputeHash(Encoding.UTF8.GetBytes(stringToSign.ToString())); var signatureString = ToHexString(signature, true); //Console.WriteLine("\nSignature:\n{0}", signatureString); // form up the authorization parameters for the caller to place in the query string var authString = new StringBuilder(); var authParams = new string[] { X_Amz_Algorithm, X_Amz_Credential, X_Amz_Date, X_Amz_SignedHeaders }; foreach (var p in authParams) { if (authString.Length > 0) authString.Append("&"); authString.AppendFormat("{0}={1}", p, paramDictionary[p]); } authString.AppendFormat("&{0}={1}", X_Amz_Signature, signatureString); var authorization = authString.ToString(); //Console.WriteLine("\nAuthorization:\n{0}", authorization); return authorization; } } }