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