Aspnetcore: Authorization header error using DataProtection PersistKeysToAzureBlobStorage

Created on 9 Jan 2018  路  12Comments  路  Source: dotnet/aspnetcore

I've been trying to set up ASP.NET Core 2 DataProtection to persist shared keys to Azure Blob Storage. I'm pretty sure my setup is correct. At the moment this is running as localhost, the web apps aren't actually deployed to Azure yet. I know CORS is correct, I have other code in the same web app that uploads files to other containers in the same storage account using SAS generated on the fly. The containers are set to Private access.

My config is simple (X509 is a custom utility class that returns a certificate instance).

services.AddDataProtection()
    .SetApplicationName("xxxxxxxx.com")
    .ProtectKeysWithCertificate(X509.GetCertificate(xxxxxxx))
    .PersistKeysToAzureBlobStorage(new Uri(xxxxxxx));

services.AddMvc( ssl, https, etc );

Not shown is how I obtain and use the storage connection string, but since the resulting URI looks good (and the same connection string works for my upload process), I'm sure that part is ok. In fact, the only major difference between my working uploader and my DataProtection effort is that the uploader requests limited permissions on the fly, whereas the DataProtection URI is created referencing a policy (with all permissions and no start or expiration). The resulting URI looks normal to me. And since the error relates to authorization, I doubt it gets as far as the URI structure anyway, but here it is with sensitive details removed:

https://xxxxxxx.blob.core.windows.net/container_name/keys.xml?sv=2017-04-17&sr=b&si=aspnet-dpapi&sig=xxxxxxxxxxx

Locally I'm using Kestrel, if that matters. The beginning of the stack trace is as follows:

Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingProvider[48]
      An error occurred while reading the key ring.
Microsoft.WindowsAzure.Storage.StorageException: Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.

Kind of stumped about how to troubleshoot this, there aren't any other auth-relevant options on the DataProtection service, as far as I can tell. Shouldn't the use of a SAS token remove the requirement for an auth header?

Author Feedback area-dataprotection

Most helpful comment

Ok. If you do find there is something wrong the the Azure client, let us know and we can investigate more.

Re: ProtectKeysWithCertificate. Fortunately, this will be fixed in 2.1.0. I added support for this a few days ago. See https://github.com/aspnet/Home/issues/2814#issuecomment-367145437 and https://github.com/aspnet/DataProtection/pull/299

All 12 comments

For the record, using the container / filename version seems to work -- no exceptions are thrown, and a key file was written to storage. Is this secured only by CORS? (I understand CORS for localhost is dangerous but this is a temporary test configuration.)

.PersistKeysToAzureBlobStorage(container, "keys.xml");

@natemcmaster Could you try to reproduce this please?

@MV10 I haven't forgotten this one, however, I'm headed out on vacation for a few days. I'll try to reproduce later next week.

I couldn't reproduce this. It looks like you're doing the right thing. Here is my exact repro code
```c#
using System;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;

class Program
{
static void Main(string[] args)
{
var services = new ServiceCollection()
.AddLogging(b => b.AddConsole().SetMinimumLevel(LogLevel.Debug))
.AddDataProtection()
.SetApplicationName("TestApp")
.PersistKeysToAzureBlobStorage(new Uri("https://xxxxxxxx.blob.core.windows.net/test-issue-2759/keys.xml?sv=2017-04-17&si=test-issue-2759-testsas&sr=c&sig=xxxxxxxx"))
.Services
.BuildServiceProvider();

    var dp = services.GetDataProtectionProvider();
    Console.WriteLine(string.Format("{0:X2}", dp.CreateProtector("Test").Protect("Hello world")));
}

}

```xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.0.0"/>
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.0.0"/>
    <PackageReference Include="Microsoft.AspNetCore.DataProtection.AzureStorage" Version="2.0.0"/>
  </ItemGroup>

</Project>

Log output:

dbug: Microsoft.AspNetCore.DataProtection.KeyManagement.DefaultKeyResolver[53]
      Repository contains no viable default key. Caller should generate a key with immediate activation.
dbug: Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingProvider[57]
      Policy resolution states that a new key should be added to the key ring.
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[58]
      Creating key {c9ee8a8d-266a-4dba-a7c6-cc819fa9874a} with creation date 2018-01-29 22:17:44Z, activation date 2018-01-29 22:17:44Z, and expiration date 2018-04-29 22:17:44Z.
dbug: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[32]
      Descriptor deserializer type for key {c9ee8a8d-266a-4dba-a7c6-cc819fa9874a} is 'Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60'.
dbug: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[34]
      No key escrow sink found. Not writing key {c9ee8a8d-266a-4dba-a7c6-cc819fa9874a} to escrow.
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
      No XML encryptor configured. Key {c9ee8a8d-266a-4dba-a7c6-cc819fa9874a} may be persisted to storage in unencrypted form.
dbug: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[23]
      Key cache expiration token triggered by 'CreateNewKey' operation.
dbug: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[18]
      Found key {c9ee8a8d-266a-4dba-a7c6-cc819fa9874a}.
dbug: Microsoft.AspNetCore.DataProtection.KeyManagement.DefaultKeyResolver[13]
      Considering key {c9ee8a8d-266a-4dba-a7c6-cc819fa9874a} with expiration date 2018-04-29 22:17:44Z as default key.
dbug: Microsoft.AspNetCore.DataProtection.TypeForwardingActivator[0]
      Forwarded activator type request from Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60 to Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Culture=neutral, PublicKeyToken=adb9793829ddae60
dbug: Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.CngCbcAuthenticatedEncryptorFactory[4]
      Opening CNG algorithm 'AES' from provider '(null)' with chaining mode CBC.
dbug: Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.CngCbcAuthenticatedEncryptorFactory[3]
      Opening CNG algorithm 'SHA256' from provider '(null)' with HMAC.
dbug: Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingProvider[2]
      Using key {c9ee8a8d-266a-4dba-a7c6-cc819fa9874a} as the default key.
CfDJ8I2K7slqJrpNp8bMgZ-ph0re-aOec3tLwLyBrs_DVAh644iofGsfjF5gZxtonqOJ-E8tNI6kqAXRNVjlTED_61dKs0UBYzaJKNkHQmQCcuWQB0ZV5PKX_GzsyYsnvykpTw

I'm adding the calls in Startup.cs. Should it be done earlier at the console Program.cs level, or is that just something you did for testing convenience?

I'll try to reproduce the problem stand-alone later today.

Should it be done earlier at the console Program.cs level, or is that just something you did for testing convenience?

Test convenience. I was just trying to create a repro that will help isolate your issue.

@MV10 were you able to solve your issue?

No, sorry, as described earlier, I went with another route and haven't had time to circle back.

I do wonder if the error is misleading. It turns out that ProtectKeysWithCertificate is only valid for encryption. Decryption requires the certificate to be available from the certificate store. (This should probably be documented...) It isn't difficult to use a cert instance for decryption. I'm using a variation on this code. I've seen MS responses to the decryption failure reference "underlying framework limitations" but as that repo illustrates, it's a simple problem to fix.

Either way, I'll close this since it isn't blocking me and I doubt I'll have time to attempt to reproduce the problem any time soon. Thanks for your attention to it.

Ok. If you do find there is something wrong the the Azure client, let us know and we can investigate more.

Re: ProtectKeysWithCertificate. Fortunately, this will be fixed in 2.1.0. I added support for this a few days ago. See https://github.com/aspnet/Home/issues/2814#issuecomment-367145437 and https://github.com/aspnet/DataProtection/pull/299

Great news about the cert fix!

@natemcmaster @blowdart
I have similar issue. I initialize AddDataProtection method inside Startup.cs

  var keyVaultClient =
               new KeyVaultClient(
                   new KeyVaultClient.AuthenticationCallback(new AzureServiceTokenProvider().KeyVaultTokenCallback));

            services.AddDataProtection()
                .SetApplicationName("API")
                .PersistKeysToAzureBlobStorage(new Uri(ConfigurationSettings.GetBlobSasUri(Configuration["StorageAccounts:ConnectionString"], Configuration["DataProtectionBlobStorage:BlobContainer"], Configuration["DataProtectionBlobStorage:BlobName"])))
                .ProtectKeysWithAzureKeyVault(keyVaultClient,$"{Configuration["KeyVault:Url"]}keys/datakeysprotection");

Then generate BlobSasUri with 15 minutes expiration duration.

public static string GetBlobSasUri(string connectionString, string containerName, string blobName)
        {
            CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connectionString);

            CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
            CloudBlobContainer container = blobClient.GetContainerReference(containerName);

            container.CreateIfNotExistsAsync().ContinueWith(res => {
                if (res.Result)
                {
                    container.SetPermissionsAsync(new BlobContainerPermissions
                    {
                        PublicAccess = BlobContainerPublicAccessType.Off
                    });
                }
            });

            CloudBlockBlob blob = container.GetBlockBlobReference(blobName);

            SharedAccessBlobPolicy sasConstraints =
               new SharedAccessBlobPolicy
               {
                   SharedAccessExpiryTime = DateTime.UtcNow.AddMinutes(15), // 15 minutes expired
                   Permissions = SharedAccessBlobPermissions.Read | SharedAccessBlobPermissions.Write  //Read & Write
               };

            //Generate the shared access signature on the blob, setting the constraints directly on the signature.
            string sasBlobToken = blob.GetSharedAccessSignature(sasConstraints);

            return blob.Uri + sasBlobToken;
        }

Then when I log in, into my application I get error with Authorization header.

image

When I try to log in second time the error doesn't appear anymore. What can cause that? Do I do something wrong with configuration? It's strange that server application remembers publishing date (+ 15 minutes) and keeps it while generating BlobSas token. But only once. I don't get it.

are you able to resolve this issue ?

Was this page helpful?
0 / 5 - 0 ratings