/*
* MinIO .NET Library for Amazon S3 Compatible Cloud Storage, (C) 2017, 2020 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Minio.Helper;
namespace Minio;
///
/// V4Authenticator implements IAuthenticator interface.
///
internal class V4Authenticator
{
//
// Excerpts from @lsegal - https://github.com/aws/aws-sdk-js/issues/659#issuecomment-120477258
//
// User-Agent:
//
// This is ignored from signing because signing this causes problems with generating pre-signed URLs
// (that are executed by other agents) or when customers pass requests through proxies, which may
// modify the user-agent.
//
// Authorization:
//
// Is skipped for obvious reasons
//
private static readonly HashSet ignoredHeaders = new(StringComparer.OrdinalIgnoreCase)
{
"authorization", "user-agent"
};
private readonly string accessKey;
private readonly string region;
private readonly string secretKey;
private readonly string sessionToken;
private readonly string sha256EmptyFileHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
///
/// Authenticator constructor.
///
///
/// Access key id
/// Secret access key
/// Region if specifically set
/// sessionToken
public V4Authenticator(bool secure, string accessKey, string secretKey, string region = "",
string sessionToken = "")
{
IsSecure = secure;
this.accessKey = accessKey;
this.secretKey = secretKey;
IsAnonymous = Utils.IsAnonymousClient(accessKey, secretKey);
this.region = region;
this.sessionToken = sessionToken;
}
internal bool IsAnonymous { get; }
internal bool IsSecure { get; }
private string GetRegion(string endpoint)
{
if (!string.IsNullOrEmpty(region)) return region;
var endpointRegion = RegionHelper.GetRegionFromEndpoint(endpoint);
return string.IsNullOrEmpty(endpointRegion) ? "us-east-1" : endpointRegion;
}
///
/// Implements Authenticate interface method for IAuthenticator.
///
/// Instantiated IRestRequest object
/// boolean; if true role credentials, otherwise IAM user
public string Authenticate(HttpRequestMessageBuilder requestBuilder, bool isSts = false)
{
var signingDate = DateTime.UtcNow;
SetContentSha256(requestBuilder, isSts);
requestBuilder.RequestUri = requestBuilder.Request.RequestUri;
var requestUri = requestBuilder.RequestUri;
if (requestUri.Port is 80 or 443)
SetHostHeader(requestBuilder, requestUri.Host);
else
SetHostHeader(requestBuilder, requestUri.Host + ":" + requestUri.Port);
SetDateHeader(requestBuilder, signingDate);
SetSessionTokenHeader(requestBuilder, sessionToken);
var headersToSign = GetHeadersToSign(requestBuilder);
var signedHeaders = GetSignedHeaders(headersToSign);
var canonicalRequest = GetCanonicalRequest(requestBuilder, headersToSign);
ReadOnlySpan canonicalRequestBytes = Encoding.UTF8.GetBytes(canonicalRequest);
var hash = ComputeSha256(canonicalRequestBytes);
var canonicalRequestHash = BytesToHex(hash);
var endpointRegion = GetRegion(requestUri.Host);
var stringToSign = GetStringToSign(endpointRegion, signingDate, canonicalRequestHash, isSts);
var signingKey = GenerateSigningKey(endpointRegion, signingDate, isSts);
ReadOnlySpan stringToSignBytes = Encoding.UTF8.GetBytes(stringToSign);
var signatureBytes = SignHmac(signingKey, stringToSignBytes);
var signature = BytesToHex(signatureBytes);
return GetAuthorizationHeader(signedHeaders, signature, signingDate, endpointRegion, isSts);
}
///
/// Get credential string of form {ACCESSID}/date/region/serviceKind/aws4_request.
///
/// Signature initiated date
/// Region for the credential string
/// boolean; if true role credentials, otherwise IAM user
/// Credential string for the authorization header
public string GetCredentialString(DateTime signingDate, string region, bool isSts = false)
{
var scope = GetScope(region, signingDate, isSts);
return $"{accessKey}/{scope}";
}
///
/// Constructs an authorization header.
///
/// All signed http headers
/// Hexadecimally encoded computed signature
/// Date for signature to be signed
/// Requested region
/// boolean; if true role credentials, otherwise IAM user
/// Fully formed authorization header
private string GetAuthorizationHeader(string signedHeaders, string signature, DateTime signingDate, string region,
bool isSts = false)
{
var scope = GetScope(region, signingDate, isSts);
return $"AWS4-HMAC-SHA256 Credential={accessKey}/{scope}, SignedHeaders={signedHeaders}, Signature={signature}";
}
///
/// Concatenates sorted list of signed http headers.
///
/// Sorted dictionary of headers to be signed
/// All signed headers
private string GetSignedHeaders(SortedDictionary headersToSign)
{
return string.Join(";", headersToSign.Keys);
}
///
/// Determines and returns the kind of service
///
/// boolean; if true role credentials, otherwise IAM user
/// returns the kind of service as a string
private string GetService(bool isSts)
{
return isSts ? "sts" : "s3";
}
///
/// Generates signing key based on the region and date.
///
/// Requested region
/// Date for signature to be signed
/// boolean; if true role credentials, otherwise IAM user
/// bytes of computed hmac
private ReadOnlySpan GenerateSigningKey(string region, DateTime signingDate, bool isSts = false)
{
ReadOnlySpan dateRegionServiceKey;
ReadOnlySpan requestBytes;
ReadOnlySpan serviceBytes = Encoding.UTF8.GetBytes(GetService(isSts));
ReadOnlySpan formattedDateBytes =
Encoding.UTF8.GetBytes(signingDate.ToString("yyyyMMdd", CultureInfo.InvariantCulture));
ReadOnlySpan formattedKeyBytes = Encoding.UTF8.GetBytes($"AWS4{secretKey}");
var dateKey = SignHmac(formattedKeyBytes, formattedDateBytes);
ReadOnlySpan regionBytes = Encoding.UTF8.GetBytes(region);
var dateRegionKey = SignHmac(dateKey, regionBytes);
dateRegionServiceKey = SignHmac(dateRegionKey, serviceBytes);
requestBytes = Encoding.UTF8.GetBytes("aws4_request");
//var hmac = SignHmac(dateRegionServiceKey, requestBytes);
//var signingKey = Encoding.UTF8.GetString(hmac);
return SignHmac(dateRegionServiceKey, requestBytes);
}
///
/// Compute hmac of input content with key.
///
/// Hmac key
/// Bytes to be hmac computed
/// Computed hmac of input content
private ReadOnlySpan SignHmac(ReadOnlySpan key, ReadOnlySpan content)
{
#if NETSTANDARD
using var hmac = new HMACSHA256(key.ToArray());
hmac.Initialize();
return hmac.ComputeHash(content.ToArray());
#else
return HMACSHA256.HashData(key, content);
#endif
}
///
/// Get string to sign.
///
/// Requested region
/// Date for signature to be signed
/// Hexadecimal encoded sha256 checksum of canonicalRequest
/// boolean; if true role credentials, otherwise IAM user
/// String to sign
private string GetStringToSign(string region, DateTime signingDate,
string canonicalRequestHash, bool isSts = false)
{
var scope = GetScope(region, signingDate, isSts);
return $"AWS4-HMAC-SHA256\n{signingDate:yyyyMMddTHHmmssZ}\n{scope}\n{canonicalRequestHash}";
}
///
/// Get scope.
///
/// Requested region
/// Date for signature to be signed
/// boolean; if true role credentials, otherwise IAM user
/// Scope string
private string GetScope(string region, DateTime signingDate, bool isSts = false)
{
return $"{signingDate:yyyyMMdd}/{region}/{GetService(isSts)}/aws4_request";
}
///
/// Compute sha256 checksum.
///
/// Bytes body
/// Bytes of sha256 checksum
private ReadOnlySpan ComputeSha256(ReadOnlySpan body)
{
#if NETSTANDARD
using var sha = SHA256.Create();
ReadOnlySpan hash
= sha.ComputeHash(body.ToArray());
#else
ReadOnlySpan hash = SHA256.HashData(body);
#endif
return hash;
}
///
/// Convert bytes to hexadecimal string.
///
/// Bytes of any checksum
/// Hexlified string of input bytes
private string BytesToHex(ReadOnlySpan checkSum)
{
return BitConverter.ToString(checkSum.ToArray()).Replace("-", string.Empty, StringComparison.OrdinalIgnoreCase)
.ToLowerInvariant();
}
///
/// Generate signature for post policy.
///
/// Requested region
/// Date for signature to be signed
/// Base64 encoded policy JSON
/// Computed signature
public string PresignPostSignature(string region, DateTime signingDate, string policyBase64)
{
var signingKey = GenerateSigningKey(region, signingDate);
ReadOnlySpan stringToSignBytes = Encoding.UTF8.GetBytes(policyBase64);
var signatureBytes = SignHmac(signingKey, stringToSignBytes);
var signature = BytesToHex(signatureBytes);
return signature;
}
///
/// Presigns any input client object with a requested expiry.
///
/// Instantiated requestBuilder
/// Expiration in seconds
/// Region of storage
/// Value for session token
/// Optional requestBuilder date and time in UTC
/// Presigned url
internal string PresignURL(HttpRequestMessageBuilder requestBuilder, int expires, string region = "",
string sessionToken = "", DateTime? reqDate = null)
{
var signingDate = reqDate ?? DateTime.UtcNow;
if (string.IsNullOrWhiteSpace(region)) region = GetRegion(requestBuilder.RequestUri.Host);
var requestUri = requestBuilder.RequestUri;
var requestQuery = requestUri.Query;
var headersToSign = GetHeadersToSign(requestBuilder);
if (!string.IsNullOrEmpty(sessionToken)) headersToSign["X-Amz-Security-Token"] = sessionToken;
if (requestQuery.Length > 0) requestQuery += "&";
requestQuery += "X-Amz-Algorithm=AWS4-HMAC-SHA256&";
requestQuery += "X-Amz-Credential="
+ Uri.EscapeDataString(accessKey + "/" + GetScope(region, signingDate))
+ "&";
requestQuery += "X-Amz-Date="
+ signingDate.ToString("yyyyMMddTHHmmssZ", CultureInfo.InvariantCulture)
+ "&";
requestQuery += "X-Amz-Expires="
+ expires
+ "&";
requestQuery += "X-Amz-SignedHeaders=host";
var presignUri = new UriBuilder(requestUri) { Query = requestQuery }.Uri;
var canonicalRequest = GetPresignCanonicalRequest(requestBuilder.Method, presignUri, headersToSign);
var headers = string.Concat(headersToSign.Select(p => $"&{p.Key}={Utils.UrlEncode(p.Value)}"));
ReadOnlySpan canonicalRequestBytes = Encoding.UTF8.GetBytes(canonicalRequest);
var canonicalRequestHash = BytesToHex(ComputeSha256(canonicalRequestBytes));
var stringToSign = GetStringToSign(region, signingDate, canonicalRequestHash);
var signingKey = GenerateSigningKey(region, signingDate);
ReadOnlySpan stringToSignBytes = Encoding.UTF8.GetBytes(stringToSign);
var signatureBytes = SignHmac(signingKey, stringToSignBytes);
var signature = BytesToHex(signatureBytes);
// Return presigned url.
var signedUri = new UriBuilder(presignUri) { Query = $"{requestQuery}{headers}&X-Amz-Signature={signature}" };
if (signedUri.Uri.IsDefaultPort) signedUri.Port = -1;
return Convert.ToString(signedUri, CultureInfo.InvariantCulture);
}
///
/// Get presign canonical requestBuilder.
///
/// HTTP method used for this requestBuilder
///
/// Full url for this requestBuilder, including all query parameters except for headers and
/// X-Amz-Signature
///
/// The key-value of headers.
/// Presigned canonical requestBuilder
internal string GetPresignCanonicalRequest(HttpMethod requestMethod, Uri uri,
SortedDictionary headersToSign)
{
var canonicalStringList = new LinkedList();
_ = canonicalStringList.AddLast(requestMethod.ToString());
var path = uri.AbsolutePath;
_ = canonicalStringList.AddLast(path);
var queryParams = uri.Query.TrimStart('?').Split('&').ToList();
queryParams.AddRange(headersToSign.Select(cv =>
$"{Utils.UrlEncode(cv.Key)}={Utils.UrlEncode(cv.Value.Trim())}"));
queryParams.Sort(StringComparer.Ordinal);
var query = string.Join("&", queryParams);
_ = canonicalStringList.AddLast(query);
var canonicalHost = GetCanonicalHost(uri);
_ = canonicalStringList.AddLast($"host:{canonicalHost}");
_ = canonicalStringList.AddLast(string.Empty);
_ = canonicalStringList.AddLast("host");
_ = canonicalStringList.AddLast("UNSIGNED-PAYLOAD");
return string.Join("\n", canonicalStringList);
}
private static string GetCanonicalHost(Uri url)
{
if (url.Port is > 0 and not 80 and not 443)
return $"{url.Host}:{url.Port}";
return url.Host;
}
///
/// Get canonical requestBuilder.
///
/// Instantiated requestBuilder object
/// Dictionary of http headers to be signed
/// Canonical Request
private string GetCanonicalRequest(HttpRequestMessageBuilder requestBuilder,
SortedDictionary headersToSign)
{
var canonicalStringList = new LinkedList();
// METHOD
_ = canonicalStringList.AddLast(requestBuilder.Method.ToString());
var queryParamsDict = new Dictionary(StringComparer.Ordinal);
if (requestBuilder.QueryParameters is not null)
foreach (var kvp in requestBuilder.QueryParameters)
queryParamsDict[kvp.Key] = Uri.EscapeDataString(kvp.Value);
var queryParams = "";
if (queryParamsDict.Count > 0)
{
var sb1 = new StringBuilder();
var queryKeys = new List(queryParamsDict.Keys);
queryKeys.Sort(StringComparer.Ordinal);
foreach (var p in queryKeys)
{
if (sb1.Length > 0)
_ = sb1.Append('&');
_ = sb1.AppendFormat(CultureInfo.InvariantCulture, "{0}={1}", p, queryParamsDict[p]);
}
queryParams = sb1.ToString();
}
var isFormData = false;
if (requestBuilder.Request.Content?.Headers?.ContentType is not null)
isFormData = string.Equals(requestBuilder.Request.Content.Headers.ContentType.ToString(),
"application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase);
if (string.IsNullOrEmpty(queryParams) && isFormData)
{
// Convert stream content to byte[]
var cntntByteData = Span.Empty;
if (requestBuilder.Request.Content is not null)
cntntByteData = requestBuilder.Request.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
// UTF conversion - String from bytes
queryParams = Encoding.UTF8.GetString(cntntByteData);
}
if (!string.IsNullOrEmpty(queryParams) &&
!isFormData &&
!string.Equals(requestBuilder.RequestUri.Query, "?location=", StringComparison.OrdinalIgnoreCase))
requestBuilder.RequestUri = new Uri(requestBuilder.RequestUri + "?" + queryParams);
_ = canonicalStringList.AddLast(requestBuilder.RequestUri.AbsolutePath);
_ = canonicalStringList.AddLast(queryParams);
// Headers to sign
foreach (var header in headersToSign.Keys)
_ = canonicalStringList.AddLast(header + ":" + S3utils.TrimAll(headersToSign[header]));
_ = canonicalStringList.AddLast(string.Empty);
_ = canonicalStringList.AddLast(string.Join(";", headersToSign.Keys));
if (headersToSign.TryGetValue("x-amz-content-sha256", out var value))
_ = canonicalStringList.AddLast(value);
else
_ = canonicalStringList.AddLast(sha256EmptyFileHash);
return string.Join("\n", canonicalStringList);
}
public static IDictionary ToDictionary(object obj)
{
var json = JsonSerializer.Serialize(obj);
var dictionary = JsonSerializer.Deserialize>(json);
return dictionary;
}
///
/// Get headers to be signed.
///
/// Instantiated requesst
/// Sorted dictionary of headers to be signed
private SortedDictionary GetHeadersToSign(HttpRequestMessageBuilder requestBuilder)
{
var headers = requestBuilder.HeaderParameters.ToList();
var sortedHeaders = new SortedDictionary(StringComparer.Ordinal);
foreach (var header in headers)
{
var headerName = header.Key.ToLowerInvariant();
var headerValue = header.Value;
if (!ignoredHeaders.Contains(headerName)) sortedHeaders.Add(headerName, headerValue);
}
return sortedHeaders;
}
///
/// Sets 'x-amz-date' http header.
///
/// Instantiated requestBuilder object
/// Date for signature to be signed
private void SetDateHeader(HttpRequestMessageBuilder requestBuilder, DateTime signingDate)
{
requestBuilder.AddOrUpdateHeaderParameter("x-amz-date",
signingDate.ToString("yyyyMMddTHHmmssZ", CultureInfo.InvariantCulture));
}
///
/// Set 'Host' http header.
///
/// Instantiated requestBuilder object
/// Host url
private void SetHostHeader(HttpRequestMessageBuilder requestBuilder, string hostUrl)
{
requestBuilder.AddOrUpdateHeaderParameter("Host", hostUrl);
}
///
/// Set 'X-Amz-Security-Token' http header.
///
/// Instantiated requestBuilder object
/// session token
private void SetSessionTokenHeader(HttpRequestMessageBuilder requestBuilder, string sessionToken)
{
if (!string.IsNullOrEmpty(sessionToken))
requestBuilder.AddOrUpdateHeaderParameter("X-Amz-Security-Token", sessionToken);
}
///
/// Set 'x-amz-content-sha256' http header.
///
/// Instantiated requestBuilder object
/// boolean; if true role credentials, otherwise IAM user
private void SetContentSha256(HttpRequestMessageBuilder requestBuilder, bool isSts = false)
{
if (IsAnonymous)
return;
// No need to compute SHA256 if the endpoint scheme is https
// or the command method is not a Post to delete multiple files
var isMultiDeleteRequest = false;
if (requestBuilder.Method == HttpMethod.Post)
isMultiDeleteRequest =
requestBuilder.QueryParameters.Any(p => p.Key.Equals("delete", StringComparison.OrdinalIgnoreCase));
if ((IsSecure && !isSts) || isMultiDeleteRequest)
{
requestBuilder.AddOrUpdateHeaderParameter("x-amz-content-sha256", "UNSIGNED-PAYLOAD");
return;
}
// For insecure, authenticated requests set sha256 header instead of MD5.
if (requestBuilder.Method.Equals(HttpMethod.Put) ||
requestBuilder.Method.Equals(HttpMethod.Post))
{
var body = requestBuilder.Content;
if (body.IsEmpty)
{
requestBuilder.AddOrUpdateHeaderParameter("x-amz-content-sha256", sha256EmptyFileHash);
return;
}
#if NETSTANDARD
using var sha = SHA256.Create();
var hash
= sha.ComputeHash(body.ToArray());
#else
var hash = SHA256.HashData(body.Span);
#endif
var hex = BitConverter.ToString(hash).Replace("-", string.Empty, StringComparison.OrdinalIgnoreCase)
.ToLowerInvariant();
requestBuilder.AddOrUpdateHeaderParameter("x-amz-content-sha256", hex);
}
else if (!IsSecure && !requestBuilder.Content.IsEmpty)
{
ReadOnlySpan bytes = Encoding.UTF8.GetBytes(requestBuilder.Content.ToString());
#if NETSTANDARD
#pragma warning disable CA5351 // Do Not Use Broken Cryptographic Algorithms
using var md5
= MD5.Create();
#pragma warning restore CA5351 // Do Not Use Broken Cryptographic Algorithms
var hash
= md5.ComputeHash(bytes.ToArray());
#else
ReadOnlySpan hash = MD5.HashData(bytes);
#endif
var base64 = Convert.ToBase64String(hash);
requestBuilder.AddHeaderParameter("Content-Md5", base64);
}
else
{
requestBuilder.AddOrUpdateHeaderParameter("x-amz-content-sha256", sha256EmptyFileHash);
}
}
}