TypeLoadException when using SqlColumnEncryptionAzureKeyVaultProvider

Problem

An exception is thrown when trying to use the SqlColumnEncryptionAzureKeyVaultProvider for working with Always Encrypted columns in SQL Azure.

An unhandled exception of type 'System.TypeLoadException' occurred in Hyak.Common.dllInheritance security rules violated by type: 'System.Net.Http.WebRequestHandler'. Derived types must either match the security accessibility of the base type or be less accessible.

Solution

There’s an issue with the 4.1 version of System.Net.Http assembly. Downgrade to version 4.0.

Background

I’m working on a data migration project that involves moving encrypted data over to Always Encrypted columns. The first part of getting that up and running is to get the Always Encrypted SQL database driver up and running. I’ve worked a fair bit with the Azure Key Vault so was surprised to see the exception detailed above when first running my application. The stack trace wasn’t overly helpful:

   at Microsoft.Azure.Common.Platform.HttpTransportHandlerProvider.CreateHttpTransportHandler()
   at Hyak.Common.ServiceClient`1..ctor()
   at Microsoft.Azure.KeyVault.Internal.KeyVaultInternalClient..ctor()
   at Microsoft.Azure.KeyVault.Internal.KeyVaultInternalClient..ctor(KeyVaultCredential credentials)
   at Microsoft.Azure.KeyVault.KeyVaultClient..ctor(AuthenticationCallback authenticationCallback)
   at Microsoft.SqlServer.Management.AlwaysEncrypted.AzureKeyVaultProvider.SqlColumnEncryptionAzureKeyVaultProvider..ctor(AuthenticationCallback authenticationCallback) in d:\DsMain\DS_Main\Sql\mpu\shared\Security\AzureKeyVaultProvider\SqlColumnEncryptionAzureKeyVaultProvider.cs:line 56
   at AlwaysEncryptedKeyVaultIssue.Program.InitializeAzureKeyVaultProvider() in C:\Dev\AlwaysEncryptedKeyVaultIssue\AlwaysEncryptedKeyVaultIssue\Program.cs:line 21
   at AlwaysEncryptedKeyVaultIssue.Program.Main(String[] args) in C:\Dev\AlwaysEncryptedKeyVaultIssue\AlwaysEncryptedKeyVaultIssue\Program.cs:line 44
   at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
   at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
   at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStart()

After a fair bit of Googling, I eventually came across up a fairly fresh Microsoft Connect ticket detailing the issue.

The issue really is not obvious and hats off to ‘lucavgobbi’ for diagnosing the root cause! Luckily it’s quite simple to remedy, although there are a few caveats.

For the purpose of this post, I created a simple test project that demonstrates the issue. It’s very easy to create using the following steps.

  1. Create a new console application (targeting 4.6.1)
  2. Install the following 2 Nuget packages:
    • Install-Package Microsoft.SqlServer.Management.AlwaysEncrypted.AzureKeyVaultProvider
    • Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory
  3. Copy this example Program.cs from the demo source.
  4. Run the app and watch it explode!

On running the application, you’ll be presented with the lovely exception from above. So what do?

As described in the Microsoft Connect issue, we need to switch the version of System.Net.Http from v4.1 to v4.0. It’s important to note that this dependency is added via Nuget as a dependency of which is the first gotcha. Run the following command to downgrade the package:

Update-Package System.Net.Http -Version 4.0.0

Run the application again and BOOM, it still fails… Ok…. The problem here lies in your app / web confg (App.config or web.config). An assembly binding redirect is added when the package is installed, redirecting to version 4.1.0, totally subverting our package downgrade. This is again simple to fix, change the dependency from this:

<dependentAssembly>
    <assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
    <bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.1.0.0" />
</dependentAssembly>

To look like this:

<dependentAssembly>
    <assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
    <bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.0.0.0" />
</dependentAssembly>

Now you’re good to go!

Browse the demo source code on GitHub.

Source Code for the application’s Program.cs:

namespace AlwaysEncryptedKeyVaultIssue
{
    using Microsoft.IdentityModel.Clients.ActiveDirectory;
    using Microsoft.SqlServer.Management.AlwaysEncrypted.AzureKeyVaultProvider;
    using System;
    using System.Collections.Generic;
    using System.Data.SqlClient;
    using System.Threading.Tasks;

    public class Program
    {
        private static ClientCredential clientCredential;

        static void Main(string[] args)
        {
            InitializeAzureKeyVaultProvider();
        }

        public static void InitializeAzureKeyVaultProvider()
        {
            var clientId = Guid.NewGuid().ToString();
            var clientSecret = "ITS_A_SECRET";

            clientCredential = new ClientCredential(clientId, clientSecret);

            var azureKeyVaultProvider = new SqlColumnEncryptionAzureKeyVaultProvider(GetToken);

            var providers = new Dictionary()
            {
                { SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, azureKeyVaultProvider }
            };

            SqlConnection.RegisterColumnEncryptionKeyStoreProviders(providers);
        }

        public async static Task GetToken(string authority, string resource, string scope)
        {
            var authContext = new AuthenticationContext(authority);
            AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCredential);

            if (result == null)
                throw new InvalidOperationException("Failed to obtain the access token");

            return result.AccessToken;
        }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *