internal UrlSigningState(RequestTemplate template, Options options, IBlobSigner blobSigner, IClock clock) { (_host, _resourcePath) = options.UrlStyle switch { UrlStyle.PathStyle => (StorageHost, $"/{template.Bucket}"), UrlStyle.VirtualHostedStyle => ($"{template.Bucket}.{StorageHost}", string.Empty), UrlStyle.BucketBoundHostname => (options.BucketBoundHostname, string.Empty), _ => throw new ArgumentOutOfRangeException(nameof(options.UrlStyle)) }; _scheme = options.Scheme; options = options.ToExpiration(clock); var now = clock.GetCurrentDateTimeUtc(); var timestamp = now.ToString("yyyyMMdd'T'HHmmss'Z'", CultureInfo.InvariantCulture); var datestamp = now.ToString("yyyyMMdd", CultureInfo.InvariantCulture); int expirySeconds = (int)(options.Expiration.Value - now).TotalSeconds; if (expirySeconds <= 0) { throw new ArgumentOutOfRangeException(nameof(options.Expiration), "Expiration must be at least 1 second"); } if (expirySeconds > MaxExpirySecondsInclusive) { throw new ArgumentOutOfRangeException(nameof(options.Expiration), "Expiration must not be greater than 7 days."); } string expiryText = expirySeconds.ToString(CultureInfo.InvariantCulture); string credentialScope = $"{datestamp}/{DefaultRegion}/{ScopeSuffix}"; var headers = new SortedDictionary <string, string>(StringComparer.Ordinal); headers.AddHeader("host", _host); headers.AddHeaders(template.RequestHeaders); headers.AddHeaders(template.ContentHeaders); var canonicalHeaders = string.Join("", headers.Select(pair => $"{pair.Key}:{pair.Value}\n")); var signedHeaders = string.Join(";", headers.Keys.Select(k => k.ToLowerInvariant())); var queryParameters = new SortedSet <string>(StringComparer.Ordinal); queryParameters.AddQueryParameter("X-Goog-Algorithm", Algorithm); queryParameters.AddQueryParameter("X-Goog-Credential", $"{blobSigner.Id}/{credentialScope}"); queryParameters.AddQueryParameter("X-Goog-Date", timestamp); queryParameters.AddQueryParameter("X-Goog-Expires", expirySeconds.ToString(CultureInfo.InvariantCulture)); queryParameters.AddQueryParameter("X-Goog-SignedHeaders", signedHeaders); var effectiveRequestMethod = template.HttpMethod; if (effectiveRequestMethod == ResumableHttpMethod) { effectiveRequestMethod = HttpMethod.Post; queryParameters.AddQueryParameter("X-Goog-Resumable", "Start"); } queryParameters.AddQueryParameters(template.QueryParameters); _canonicalQueryString = string.Join("&", queryParameters); if (!string.IsNullOrEmpty(template.ObjectName)) { // EscapeDataString escapes slashes, which we *don't* want to escape here. The simplest option is to // split the path into segments by slashes, escape each segment, then join the escaped segments together again. var segments = template.ObjectName.Split('/'); var escaped = string.Join("/", segments.Select(Uri.EscapeDataString)); _resourcePath = _resourcePath + "/" + escaped; } string payloadHash = "UNSIGNED-PAYLOAD"; var payloadHashHeader = headers.Where( header => header.Key.Equals("X-Goog-Content-SHA256", StringComparison.OrdinalIgnoreCase)).ToList(); if (payloadHashHeader.Count == 1) { payloadHash = payloadHashHeader[0].Value; } var canonicalRequest = $"{effectiveRequestMethod}\n{_resourcePath}\n{_canonicalQueryString}\n{canonicalHeaders}\n{signedHeaders}\n{payloadHash}"; string hashHex; using (var sha256 = SHA256.Create()) { hashHex = FormatHex(sha256.ComputeHash(Encoding.UTF8.GetBytes(canonicalRequest))); } _blobToSign = Encoding.UTF8.GetBytes($"{Algorithm}\n{timestamp}\n{credentialScope}\n{hashHex}"); }