Skip to content

Commit

Permalink
feat: Support HMAC URL signing.
Browse files Browse the repository at this point in the history
Closes #6026
  • Loading branch information
amanda-tarafa committed Jan 10, 2023
1 parent f77799a commit dfad68e
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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))
{
Expand All @@ -49,7 +85,6 @@ private static int Main(string[] args)
RemoveHolds(client, bucket);
}
}
return 0;
}

private static void DeleteBucket(StorageClient client, string bucket)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,*,*)
Expand All @@ -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.
Expand All @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Blob signer to implement signing using an HMAC secret.
/// </summary>
public sealed class HmacBlobSigner : IBlobSigner
{
private const string DefaultPrefix = "GOOG4";

private readonly string _secret;
private readonly string _prefix;

/// <inheritdoc/>
public string Id { get; }

/// <inheritdoc/>
public string Algorithm => "GOOG4-HMAC-SHA256";

/// <summary>
/// Creates a new HMAC Blob Signer from an HMAC Key ID and Secret.
/// </summary>
/// <param name="keyId">The HMAC key ID. Must not be null or empty.</param>
/// <param name="secret">The HMAC key secret. Must not be null or empty.</param>
private HmacBlobSigner(string keyId, string secret) =>
(Id, _secret, _prefix) = (GaxPreconditions.CheckNotNullOrEmpty(keyId, nameof(keyId)), GaxPreconditions.CheckNotNullOrEmpty(secret, nameof(secret)), DefaultPrefix);

/// <summary>
/// Creates a new HMAC Blob Signer from an HMAC Key ID and Secret.
/// </summary>
/// <param name="keyId">The HMAC key ID. Must not be null or empty.</param>
/// <param name="secret">The HMAC key secret. Must not be null or empty.</param>
public static HmacBlobSigner Create(string keyId, string secret) => new HmacBlobSigner(keyId, secret);

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

/// <inheritdoc/>
public Task<string> CreateSignatureAsync(byte[] data, BlobSignerParameters parameters, CancellationToken cancellationToken) =>
Task.FromResult(CreateSignature(data, parameters));
}
}
13 changes: 12 additions & 1 deletion apis/Google.Cloud.Storage.V1/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit dfad68e

Please sign in to comment.