src/AlibabaCloud.OSS.V2/Signer/SignerV4.cs (300 lines of code) (raw):

using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Security.Cryptography; using System.Text; using AlibabaCloud.OSS.V2.Extensions; namespace AlibabaCloud.OSS.V2.Signer { public class SignerV4 : ISigner { private const string UnsignedPayload = "UNSIGNED-PAYLOAD"; private const string DateTimeFormat = "yyyyMMdd'T'HHmmss'Z'"; private const string DateFormat = "yyyyMMdd"; private const string Rfc822DateFormat = @"ddd, dd MMM yyyy HH:mm:ss \G\M\T"; public void Sign(SigningContext signingContext) { if (signingContext.Request == null) throw new ArgumentException("signingContext.Request is null"); if (signingContext.Credentials == null) throw new ArgumentException("signingContext.Credentials is null"); if (signingContext.Region == null) throw new ArgumentException("signingContext.Region is null"); if (signingContext.Product == null) throw new ArgumentException("signingContext.Product is null"); if (signingContext.AuthMethodQuery) AuthQuery(signingContext); else AuthHeader(signingContext); } private static void AuthQuery(SigningContext signingContext) { var request = signingContext.Request; var credentials = signingContext.Credentials; var region = signingContext.Region ?? ""; var product = signingContext.Product ?? ""; // Date var signTime = signingContext.SignTime ?? DateTime.UtcNow; var datetime = FormatDateTime(signTime); var date = FormatDate(signTime); // Expiration var expiration = signingContext.Expiration ?? DateTime.UtcNow.AddMinutes(15); var expires = ((long)expiration.Subtract(signTime).TotalSeconds).ToString(CultureInfo.InvariantCulture); // Scope var scope = $"{date}/{region}/{product}/aliyun_v4_request"; // Headers var headers = request!.Headers; var additionalHeaders = GetAdditionalHeaders(headers, signingContext.AdditionalHeaders); additionalHeaders.Sort(); // Credentials information var parameters = new Dictionary<string, string>(); if (credentials!.SecurityToken.IsNotEmpty()) parameters.Add("x-oss-security-token", credentials.SecurityToken); parameters.Add("x-oss-signature-version", "OSS4-HMAC-SHA256"); parameters.Add("x-oss-date", datetime); parameters.Add("x-oss-expires", expires); parameters.Add("x-oss-credential", $"{credentials.AccessKeyId}/{scope}"); if (additionalHeaders.Count > 0) parameters.Add("x-oss-additional-headers", additionalHeaders.JoinToString(";")); // update query var queryStr = parameters .Select( x => x.Value.IsEmpty() ? x.Key.UrlEncode() : x.Key.UrlEncode() + "=" + x.Value.UrlEncode() ) .JoinToString("&"); request.RequestUri = request.RequestUri.AppendToQuery(queryStr); // CanonicalRequest var canonicalRequest = CanonicalizeRequest( request, ResourcePath(signingContext.Bucket, signingContext.Key), headers, additionalHeaders ); // StringToSign var stringToSign = CalcStringToSign(datetime, scope, canonicalRequest); // Signature var signature = CalcSignature(credentials.AccessKeySecret, date, region, product, stringToSign); // Credential request.RequestUri = request.RequestUri.AppendToQuery($"x-oss-signature={signature.UrlEncode()}"); //Console.WriteLine("canonicalRequest:{0}\n", canonicalRequest); //Console.WriteLine("stringToSign:{0}\n", stringToSign); //Console.WriteLine("signature:{0}\n", signature); //update signingContext.Request = request; signingContext.Expiration = expiration; signingContext.StringToSign = stringToSign; } private static void AuthHeader(SigningContext signingContext) { var request = signingContext.Request; var credentials = signingContext.Credentials; var region = signingContext.Region ?? ""; var product = signingContext.Product ?? ""; // Date var signTime = signingContext.SignTime ?? DateTime.UtcNow; var datetime = FormatDateTime(signTime); var date = FormatDate(signTime); var datetimeGmt = FormatRfc822Date(signTime); // Scope var scope = $"{date}/{region}/{product}/aliyun_v4_request"; // Credentials information if (credentials!.SecurityToken.IsNotEmpty()) request!.Headers["x-oss-security-token"] = credentials.SecurityToken; // Other Headers request!.Headers["x-oss-content-sha256"] = UnsignedPayload; request.Headers["x-oss-date"] = datetime; request.Headers["Date"] = datetimeGmt; // lower key & Sorted Headers // the headers is OrdinalIgnoreCase var headers = request.Headers; var additionalHeaders = GetAdditionalHeaders(headers, signingContext.AdditionalHeaders); additionalHeaders.Sort(); // CanonicalRequest var canonicalRequest = CanonicalizeRequest( request, ResourcePath(signingContext.Bucket, signingContext.Key), headers, additionalHeaders ); // StringToSign var stringToSign = CalcStringToSign(datetime, scope, canonicalRequest); // Signature var signature = CalcSignature(credentials.AccessKeySecret, date, region, product, stringToSign); // Credential var sb = new StringBuilder(); sb.AppendFormat("OSS4-HMAC-SHA256 Credential={0}/{1}", credentials.AccessKeyId, scope); if (additionalHeaders.Count > 0) sb.AppendFormat(",AdditionalHeaders={0}", additionalHeaders.JoinToString(";")); sb.AppendFormat(",Signature={0}", signature); request.Headers["Authorization"] = sb.ToString(); //Console.WriteLine("canonicalRequest:{0}\n", canonicalRequest); //Console.WriteLine("stringToSign:{0}\n", stringToSign); //Console.WriteLine("signature:{0}\n", signature); //update signingContext.StringToSign = stringToSign; } private static string FormatDateTime(DateTime time) { return time.ToUniversalTime().ToString(DateTimeFormat, CultureInfo.InvariantCulture); } private static string FormatDate(DateTime time) { return time.ToUniversalTime().ToString(DateFormat, CultureInfo.InvariantCulture); } public static string FormatRfc822Date(DateTime time) { return time.ToUniversalTime().ToString(Rfc822DateFormat, CultureInfo.InvariantCulture); } private static string ResourcePath(string? bucket, string? key) { var resourcePath = "/" + (bucket ?? string.Empty) + (key != null ? "/" + key : ""); if (bucket != null && key == null) resourcePath = resourcePath + "/"; return resourcePath; } private static string CanonicalizeRequest( RequestMessage request, string resourcePath, IDictionary<string, string> headers, List<string> additionalHeaders ) { /* Canonical Request HTTP Verb + "\n" + Canonical URI + "\n" + Canonical Query String + "\n" + Canonical Headers + "\n" + Additional Headers + "\n" + Hashed PayLoad */ var httpMethod = request.Method.ToUpperInvariant(); // Canonical Uri var canonicalUri = resourcePath.UrlEncodePath(); // Canonical Query var sortedParameters = new SortedDictionary<string, string>(StringComparer.Ordinal); if (request.RequestUri.Query.IsNotEmpty()) { var query = request.RequestUri.Query; if (query.StartsWith("?")) query = query.Substring(1); foreach (var param in query.Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries)) { var parts = param.Split(new char[] { '=' }, 2); sortedParameters.Add(parts[0], parts.Length == 1 ? "" : parts[1]); } } var sb = new StringBuilder(); foreach (var p in sortedParameters) { if (sb.Length > 0) sb.Append("&"); sb.AppendFormat("{0}", p.Key); if (p.Value.Length > 0) sb.AppendFormat("={0}", p.Value); } var canonicalQueryString = sb.ToString(); var canonicalHeaderString = CanonicalizeHeaders(headers, additionalHeaders); // Additional Headers var additionalHeadersString = additionalHeaders.JoinToString(";"); var hashBody = CanonicalizeBodyHash(headers); var canonicalRequest = new StringBuilder(); canonicalRequest.AppendFormat("{0}\n", httpMethod); canonicalRequest.AppendFormat("{0}\n", canonicalUri); canonicalRequest.AppendFormat("{0}\n", canonicalQueryString); canonicalRequest.AppendFormat("{0}\n", canonicalHeaderString); canonicalRequest.AppendFormat("{0}\n", additionalHeadersString); canonicalRequest.AppendFormat("{0}", hashBody); return canonicalRequest.ToString(); } private static string CanonicalizeHeaders(IDictionary<string, string> headers, List<string> additionalHeaders) { if (headers.Count == 0) return string.Empty; var addHeadersMap = new Dictionary<string, string>(); foreach (var header in additionalHeaders) addHeadersMap[header.ToLowerInvariant()] = string.Empty; var sortedHeaderMap = new SortedDictionary<string, string>(StringComparer.Ordinal); foreach (var header in headers) { if (header.Value == null) continue; var lowerKey = header.Key.ToLowerInvariant(); if (IsDefaultSignedHeader(lowerKey) || addHeadersMap.ContainsKey(lowerKey)) sortedHeaderMap[lowerKey] = header.Value.Trim(); } var sb = new StringBuilder(); foreach (var header in sortedHeaderMap) sb.AppendFormat("{0}:{1}\n", header.Key, header.Value.Trim()); return sb.ToString(); } private static string CanonicalizeBodyHash(IDictionary<string, string> headers) { return headers.TryGetValue("x-oss-content-sha256", out var value) ? value : UnsignedPayload; } private static bool IsDefaultSignedHeader(string lowerKey) { return lowerKey == "content-type" || lowerKey == "content-md5" || lowerKey.StartsWith("x-oss-"); } private static List<string> GetAdditionalHeaders( IDictionary<string, string> headers, List<string>? additionalHeaders ) { var keys = new List<string>(); if (additionalHeaders == null || additionalHeaders.Count == 0 || headers.Count == 0) return keys; foreach (var k in additionalHeaders) { var lowK = k.ToLowerInvariant(); if (IsDefaultSignedHeader(lowK)) continue; else if (headers.ContainsKey(lowK)) keys.Add(lowK); } return keys; } private static string CalcStringToSign(string datetime, string scope, string canonicalRequest) { /* StringToSign "OSS4-HMAC-SHA256" + "\n" + TimeStamp + "\n" + Scope + "\n" + Hex(SHA256Hash(Canonical Request)) */ using var hash = SHA256.Create(); var hashBytes = hash.ComputeHash(Encoding.UTF8.GetBytes(canonicalRequest)); return "OSS4-HMAC-SHA256" + "\n" + datetime + "\n" + scope + "\n" + ToHexString(hashBytes, true); } private static string CalcSignature( string accessKeySecret, string date, string region, string product, string stringToSign ) { using var kha = new HMACSHA256(); var ksecret = Encoding.UTF8.GetBytes("aliyun_v4" + accessKeySecret); kha.Key = ksecret; var hashDate = kha.ComputeHash(Encoding.UTF8.GetBytes(date)); kha.Key = hashDate; var hashRegion = kha.ComputeHash(Encoding.UTF8.GetBytes(region)); kha.Key = hashRegion; var hashProduct = kha.ComputeHash(Encoding.UTF8.GetBytes(product)); kha.Key = hashProduct; var signingKey = kha.ComputeHash(Encoding.UTF8.GetBytes("aliyun_v4_request")); kha.Key = signingKey; var signature = kha.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)); //Console.WriteLine("ksecret:{0}\n", OssUtils.ToHexString(ksecret, true)); //Console.WriteLine("hashDate:{0}\n", OssUtils.ToHexString(hashDate, true)); //Console.WriteLine("hashRegion:{0}\n", OssUtils.ToHexString(hashRegion, true)); //Console.WriteLine("hashProduct:{0}\n", OssUtils.ToHexString(hashProduct, true)); //Console.WriteLine("signature:{0}\n", OssUtils.ToHexString(signature, true)); return ToHexString(signature, true); } internal static string ToHexString(byte[] data, bool lowercase) { var sb = new StringBuilder(); for (var i = 0; i < data.Length; i++) sb.Append(data[i].ToString(lowercase ? "x2" : "X2")); return sb.ToString(); } } }