From dfad68e47ed7a72c9ef97b5676c59adec2f87aa2 Mon Sep 17 00:00:00 2001 From: Amanda Tarafa Mas Date: Fri, 6 Jan 2023 12:34:22 +0000 Subject: [PATCH] feat: Support HMAC URL signing. Closes #6026 --- .../Program.cs | 39 +++++++- .../UrlSignerSnippets.cs | 63 ++++++++++--- .../UrlSigner.HmacBlobSigner.cs | 89 +++++++++++++++++++ apis/Google.Cloud.Storage.V1/docs/index.md | 13 ++- 4 files changed, 190 insertions(+), 14 deletions(-) create mode 100644 apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1/UrlSigner.HmacBlobSigner.cs diff --git a/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1.CleanTestData/Program.cs b/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1.CleanTestData/Program.cs index 722b2d28bdeb..3e7a47db3d0b 100644 --- a/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1.CleanTestData/Program.cs +++ b/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1.CleanTestData/Program.cs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Google.Apis.Storage.v1.Data; using Google.Cloud.Storage.V1; using System; using System.Linq; @@ -34,6 +33,43 @@ private static int Main(string[] args) } string projectId = args[0]; var client = StorageClient.Create(); + + DeleteBuckets(client, projectId); + DeleteHmacKeys(client, projectId); + + return 0; + } + + private static void DeleteHmacKeys(StorageClient client, string projectId) + { + var keys = client.ListHmacKeys(projectId); + foreach (var key in keys) + { + var toDelete = key; + try + { + // We need to deactivate keys before we can delete them. + if (toDelete.State == HmacKeyStates.Active) + { + toDelete.State = HmacKeyStates.Inactive; + toDelete = client.UpdateHmacKey(toDelete); + } + + // If it's already deleted skip it + if (toDelete.State == HmacKeyStates.Inactive) + { + client.DeleteHmacKey(projectId, toDelete.AccessId); + } + } + catch (GoogleApiException e) + { + Console.WriteLine($"Failed to delete key {key.Id}: {e.Message}"); + } + } + } + + private static void DeleteBuckets(StorageClient client, string projectId) + { var buckets = client.ListBuckets(projectId).Select(b => b.Name).ToList(); foreach (var bucket in buckets.Where(IsTestBucket)) { @@ -49,7 +85,6 @@ private static int Main(string[] args) RemoveHolds(client, bucket); } } - return 0; } private static void DeleteBucket(StorageClient client, string bucket) diff --git a/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1.Snippets/UrlSignerSnippets.cs b/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1.Snippets/UrlSignerSnippets.cs index 9cb19e6275c9..eeb1db166859 100644 --- a/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1.Snippets/UrlSignerSnippets.cs +++ b/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1.Snippets/UrlSignerSnippets.cs @@ -47,8 +47,8 @@ public async Task SignedURLGet() { var bucketName = _fixture.BucketName; var objectName = _fixture.HelloStorageObjectName; - var credential = (await GoogleCredential.GetApplicationDefaultAsync()).UnderlyingCredential as ServiceAccountCredential; - var httpClient = new HttpClient(); + var credential = (ServiceAccountCredential) (await GoogleCredential.GetApplicationDefaultAsync()).UnderlyingCredential; + using var httpClient = new HttpClient(); // Sample: SignedURLGet // Additional: Sign(string,string,TimeSpan,*,*) @@ -64,6 +64,47 @@ public async Task SignedURLGet() Assert.Equal(_fixture.HelloWorldContent, content); } + [Fact] + public async Task HmacSignedURLGet() + { + var bucketName = _fixture.BucketName; + var objectName = _fixture.HelloStorageObjectName; + var credential = (ServiceAccountCredential) (await GoogleCredential.GetApplicationDefaultAsync()).UnderlyingCredential; + using var httpClient = new HttpClient(); + + using var storageClient = await StorageClient.CreateAsync(); + var hmacKey = await storageClient.CreateHmacKeyAsync(_fixture.ProjectId, credential.Id); + var hmacKeyId = hmacKey.Metadata.AccessId; + var hmacKeySecret = hmacKey.Secret; + + try + { + + // Sample: HmacSignedURLGet + // Additional: Sign(string,string,TimeSpan,*,*) + // Create an HmacBlobSigner from an HMAC Key ID and Secret. + UrlSigner.IBlobSigner blobSigner = UrlSigner.HmacBlobSigner.Create(hmacKeyId, hmacKeySecret); + // Create a URL signer from the HmacBlobSigner. + UrlSigner urlSigner = UrlSigner.FromBlobSigner(blobSigner); + // Create an HMAC signed URL which can be used to get a specific object for one hour. + string url = urlSigner.Sign(bucketName, objectName, TimeSpan.FromHours(1)); + + // Get the content at the created URL. + HttpResponseMessage response = await httpClient.GetAsync(url); + string content = await response.Content.ReadAsStringAsync(); + // End sample + + Assert.Equal(_fixture.HelloWorldContent, content); + } + finally + { + // We need to deactivate key before we can delete it. + hmacKey.Metadata.State = HmacKeyStates.Inactive; + await storageClient.UpdateHmacKeyAsync(hmacKey.Metadata); + await storageClient.DeleteHmacKeyAsync(_fixture.ProjectId, hmacKey.Metadata.AccessId); + } + } + // See-also: Sign(string,string,TimeSpan,*,*) // Member: Sign(UrlSigner.RequestTemplate, UrlSigner.Options) // See [Sign](ref) for an example using an alternative overload. @@ -74,8 +115,8 @@ public async Task WithSigningVersion() { var bucketName = _fixture.BucketName; var objectName = _fixture.HelloStorageObjectName; - var credential = (await GoogleCredential.GetApplicationDefaultAsync()).UnderlyingCredential as ServiceAccountCredential; - var httpClient = new HttpClient(); + var credential = (ServiceAccountCredential) (await GoogleCredential.GetApplicationDefaultAsync()).UnderlyingCredential; + using var httpClient = new HttpClient(); // Sample: WithSigningVersion // Create a signed URL which can be used to get a specific object for one hour, @@ -96,8 +137,8 @@ public async Task WithSigningVersion() public async Task SignedURLPut() { var bucketName = _fixture.BucketName; - var credential = (await GoogleCredential.GetApplicationDefaultAsync()).UnderlyingCredential as ServiceAccountCredential; - var httpClient = new HttpClient(); + var credential = (ServiceAccountCredential) (await GoogleCredential.GetApplicationDefaultAsync()).UnderlyingCredential; + using var httpClient = new HttpClient(); // Sample: SignedURLPut // Create a request template that will be used to create the signed URL. @@ -191,7 +232,7 @@ public async Task SignedUrlWithIamServiceBlobSigner() var bucketName = _fixture.BucketName; var objectName = _fixture.HelloStorageObjectName; - var httpClient = new HttpClient(); + using var httpClient = new HttpClient(); // Sample: IamServiceBlobSignerUsage // First obtain the email address of the default service account for this instance from the metadata server. @@ -243,7 +284,7 @@ public async Task PostPolicySimple() { var bucketName = _fixture.BucketName; var objectName = "places/world.txt"; - var credential = (await GoogleCredential.GetApplicationDefaultAsync()).UnderlyingCredential as ServiceAccountCredential; + var credential = (ServiceAccountCredential) (await GoogleCredential.GetApplicationDefaultAsync()).UnderlyingCredential; // Sample: PostPolicySimple // [START storage_generate_signed_post_policy_v4] @@ -287,7 +328,7 @@ public async Task PostPolicyCacheControl() { var bucketName = _fixture.BucketName; var objectName = "places/world.txt"; - var credential = (await GoogleCredential.GetApplicationDefaultAsync()).UnderlyingCredential as ServiceAccountCredential; + var credential = (ServiceAccountCredential) (await GoogleCredential.GetApplicationDefaultAsync()).UnderlyingCredential; // Sample: PostPolicyCacheControl // Create a signed post policy which can be used to upload a specific object with a @@ -330,7 +371,7 @@ public async Task PostPolicyAcl() { var bucketName = _fixture.BucketName; var objectName = "places/world.txt"; - var credential = (await GoogleCredential.GetApplicationDefaultAsync()).UnderlyingCredential as ServiceAccountCredential; + var credential = (ServiceAccountCredential) (await GoogleCredential.GetApplicationDefaultAsync()).UnderlyingCredential; // Sample: PostPolicyAcl // Create a signed post policy which can be used to upload a specific object and @@ -376,7 +417,7 @@ public async Task PostPolicySuccessStatus() { var bucketName = _fixture.BucketName; var objectName = "places/world.txt"; - var credential = (await GoogleCredential.GetApplicationDefaultAsync()).UnderlyingCredential as ServiceAccountCredential; + var credential = (ServiceAccountCredential) (await GoogleCredential.GetApplicationDefaultAsync()).UnderlyingCredential; // Sample: PostPolicySuccessStatus // Create a signed post policy which can be used to upload a specific object and diff --git a/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1/UrlSigner.HmacBlobSigner.cs b/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1/UrlSigner.HmacBlobSigner.cs new file mode 100644 index 000000000000..ed20601c1f33 --- /dev/null +++ b/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1/UrlSigner.HmacBlobSigner.cs @@ -0,0 +1,89 @@ +// Copyright 2023 Google LLC +// +// 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 +// +// https://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 Google.Api.Gax; +using System; +using System.Globalization; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Google.Cloud.Storage.V1; + +public sealed partial class UrlSigner +{ + /// + /// Blob signer to implement signing using an HMAC secret. + /// + public sealed class HmacBlobSigner : IBlobSigner + { + private const string DefaultPrefix = "GOOG4"; + + private readonly string _secret; + private readonly string _prefix; + + /// + public string Id { get; } + + /// + public string Algorithm => "GOOG4-HMAC-SHA256"; + + /// + /// Creates a new HMAC Blob Signer from an HMAC Key ID and Secret. + /// + /// The HMAC key ID. Must not be null or empty. + /// The HMAC key secret. Must not be null or empty. + private HmacBlobSigner(string keyId, string secret) => + (Id, _secret, _prefix) = (GaxPreconditions.CheckNotNullOrEmpty(keyId, nameof(keyId)), GaxPreconditions.CheckNotNullOrEmpty(secret, nameof(secret)), DefaultPrefix); + + /// + /// Creates a new HMAC Blob Signer from an HMAC Key ID and Secret. + /// + /// The HMAC key ID. Must not be null or empty. + /// The HMAC key secret. Must not be null or empty. + public static HmacBlobSigner Create(string keyId, string secret) => new HmacBlobSigner(keyId, secret); + + /// + public string CreateSignature(byte[] data, BlobSignerParameters parameters) + { + GaxPreconditions.CheckNotNull(data, nameof(data)); + GaxPreconditions.CheckNotNull(parameters, nameof(parameters)); + + string date = parameters.SignatureTimestamp.ToString("yyyyMMdd", CultureInfo.InvariantCulture); + + // Implements the steps in https://cloud.google.com/storage/docs/authentication/signatures#derive-key + var utf8 = Encoding.UTF8; + byte[] initialKey = utf8.GetBytes(_prefix + _secret); + byte[] keyDate = Hash(initialKey, utf8.GetBytes(date)); + byte[] keyRegion = Hash(keyDate, utf8.GetBytes(parameters.Region)); + byte[] keyService = Hash(keyRegion, utf8.GetBytes(parameters.Service)); + byte[] signingKey = Hash(keyService, utf8.GetBytes(parameters.RequestType)); + byte[] signature = Hash(signingKey, data); + return Convert.ToBase64String(signature); + + static byte[] Hash(byte[] key, byte[] data) + { + using (HMACSHA256 hmac = new HMACSHA256(key)) + { + return hmac.ComputeHash(data); + } + } + } + + /// + public Task CreateSignatureAsync(byte[] data, BlobSignerParameters parameters, CancellationToken cancellationToken) => + Task.FromResult(CreateSignature(data, parameters)); + } +} diff --git a/apis/Google.Cloud.Storage.V1/docs/index.md b/apis/Google.Cloud.Storage.V1/docs/index.md index 613a9c8d3a30..3110e7cdb141 100644 --- a/apis/Google.Cloud.Storage.V1/docs/index.md +++ b/apis/Google.Cloud.Storage.V1/docs/index.md @@ -57,7 +57,18 @@ Or write-only access to put specific object content into a bucket: {{sample:UrlSigner.SignedURLPut}} -### Signing URLs without a service account credential file +### HMAC Signed URLs + +If you have access to an HMAC key, you can also sign URLs, even if you +don't have access to a service account private key. See the +[HMAC Keys documentation](https://cloud.google.com/storage/docs/authentication/hmackeys) +and the [Signing documentation](https://cloud.google.com/storage/docs/authentication/signatures#overview) +for more details. Below you can find an example on how to create +HMAC signed URLs using this library: + +{{sample:UrlSigner.HmacSignedURLGet}} + +### Signing URLs without a service account credential file or HMAC key If you need to sign URLs but don't have a full service account credential file (with private keys) available, you can create a