Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature | AKV Provider Upgrade to use new Azure key Vault libraries #630

Merged
merged 25 commits into from
Feb 26, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
185fd6f
AKV Upgrade
cheenamalhotra Jun 29, 2020
5b637b7
Add net461 support, drops net46 support.
cheenamalhotra Jun 30, 2020
7c0ecde
Adjust tests for AKV Upgrade
cheenamalhotra Jul 6, 2020
6d7eb45
Apply feedback comments
cheenamalhotra Jul 8, 2020
8b6d6aa
Address review feedback + remove old constructors.
cheenamalhotra Jul 20, 2020
47f4a0f
Update design of AKV Provider with multi-user support example
cheenamalhotra Aug 7, 2020
8c9fefc
Minor edit
cheenamalhotra Aug 7, 2020
33ab71d
Merge branch 'master' into akv-upgrade
cheenamalhotra Aug 26, 2020
aea83c6
Merge branch 'master' into akv-upgrade
cheenamalhotra Aug 31, 2020
65d4696
fix key version bug
johnnypham Feb 2, 2021
0c6c2f5
add enclave example using new akv. update public api comments
johnnypham Feb 5, 2021
52dc29d
Merge pull request #3 from johnnypham/akv-upgrade
cheenamalhotra Feb 5, 2021
0049b64
update failing tests
johnnypham Feb 8, 2021
d8278f8
update tests
johnnypham Feb 8, 2021
e54c463
Merge pull request #4 from johnnypham/akv-upgrade
cheenamalhotra Feb 9, 2021
6d7caa3
Merge branch 'akv-upgrade' of https://github.com/cheenamalhotra/SqlCl…
johnnypham Feb 10, 2021
cf8e1f7
resolve merge conflicts
johnnypham Feb 11, 2021
471d9e8
resolve merge conflicts
johnnypham Feb 11, 2021
fc20e0c
Merge pull request #5 from johnnypham/akv-upgrade
cheenamalhotra Feb 11, 2021
56e2aa0
Merge branch 'master' of https://github.com/dotnet/SqlClient into akv…
cheenamalhotra Feb 23, 2021
47cbaf6
Touch-ups and cleanups
cheenamalhotra Feb 23, 2021
ffa98ad
Formatting fix
cheenamalhotra Feb 23, 2021
68853b8
Apply suggestions from code review
cheenamalhotra Feb 26, 2021
405e564
Apply Suggestions from Code Review
cheenamalhotra Feb 26, 2021
34af2f7
Merge branch 'akv-upgrade' of https://github.com/cheenamalhotra/SqlCl…
cheenamalhotra Feb 26, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions BUILDGUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ Manual Tests require the below setup to run:
|AADSecurePrincipalId | (Optional) The Application Id of a registered application which has been granted permission to the database defined in the AADPasswordConnectionString. | {Application ID} |
|AADSecurePrincipalSecret | (Optional) A Secret defined for a registered application which has been granted permission to the database defined in the AADPasswordConnectionString. | {Secret} |
|AzureKeyVaultURL | (Optional) Azure Key Vault Identifier URL | `https://{keyvaultname}.vault.azure.net/` |
|AzureKeyVaultTenantId | (Optional) The Azure Active Directory tenant (directory) Id of the service principal. | _{Tenant ID of Active Directory}_ |
|AzureKeyVaultClientId | (Optional) "Application (client) ID" of an Active Directory registered application, granted access to the Azure Key Vault specified in `AZURE_KEY_VAULT_URL`. Requires the key permissions Get, List, Import, Decrypt, Encrypt, Unwrap, Wrap, Verify, and Sign. | _{Client Application ID}_ |
|AzureKeyVaultClientSecret | (Optional) "Client Secret" of the Active Directory registered application, granted access to the Azure Key Vault specified in `AZURE_KEY_VAULT_URL` | _{Client Application Secret}_ |
|SupportsLocalDb | (Optional) Whether or not a LocalDb instance of SQL Server is installed on the machine running the tests. |`true` OR `false`|
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;

namespace Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider
{
internal class AzureKeyVaultProviderTokenCredential : TokenCredential
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
{
private const string Bearer = "Bearer ";

private AuthenticationCallback Callback { get; set; }

private string Authority { get; set; }

private string Resource { get; set; }

internal AzureKeyVaultProviderTokenCredential(AuthenticationCallback authenticationCallback, string masterKeyPath)
{
Callback = authenticationCallback;
using (HttpClient httpClient = new HttpClient())
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
{
HttpResponseMessage response = httpClient.GetAsync(masterKeyPath).GetAwaiter().GetResult();
string challenge = response?.Headers.WwwAuthenticate.FirstOrDefault()?.ToString();
string trimmedChallenge = ValidateChallenge(challenge);
string[] pairs = trimmedChallenge.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries);

if (pairs != null && pairs.Length > 0)
{
for (int i = 0; i < pairs.Length; i++)
{
string[] pair = pairs[i]?.Split('=');

if (pair.Length == 2)
{
string key = pair[0]?.Trim().Trim(new char[] { '\"' });
string value = pair[1]?.Trim().Trim(new char[] { '\"' });

if (!string.IsNullOrEmpty(key))
{
if (key.Equals("authorization", StringComparison.InvariantCultureIgnoreCase))
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
{
Authority = value;
}
else if (key.Equals("resource", StringComparison.InvariantCultureIgnoreCase))
{
Resource = value;
}
}
}
}
}
}
}

public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
string token = Callback.Invoke(Authority, Resource, string.Empty).GetAwaiter().GetResult();
return new AccessToken(token, DateTimeOffset.Now.AddHours(1));
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
}

public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
string token = Callback.Invoke(Authority, Resource, string.Empty).GetAwaiter().GetResult();
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
Task<AccessToken> task = Task.FromResult(new AccessToken(token, DateTimeOffset.Now.AddHours(1)));
return new ValueTask<AccessToken>(task);
}

private static string ValidateChallenge(string challenge)
{
if (string.IsNullOrEmpty(challenge))
throw new ArgumentNullException(nameof(challenge));

string trimmedChallenge = challenge.Trim();

if (!trimmedChallenge.StartsWith(Bearer))
throw new ArgumentException("Challenge is not Bearer", nameof(challenge));

return trimmedChallenge.Substring(Bearer.Length);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Azure.Core;
using Azure.Security.KeyVault.Keys;
using Azure.Security.KeyVault.Keys.Cryptography;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using static Azure.Security.KeyVault.Keys.Cryptography.SignatureAlgorithm;


namespace Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider
{
internal class AzureSqlKeyCryptographer
{
/// <summary>
/// TokenCredential to be used with the KeyClient
/// </summary>
private TokenCredential TokenCredential { get; set; }

/// <summary>
/// AuthenticationCallback to be used with the KeyClient for legacy support.
/// </summary>
private AuthenticationCallback AuthenticationCallback { get; set; }

/// <summary>
/// A flag to determine whether to use AuthenticationCallback with the KeyClient for legacy support.
/// </summary>
private readonly bool isUsingLegacyAuthentication = false;

/// <summary>
/// A mapping of the KeyClient objects to the corresponding Azure Key Vault URI
/// </summary>
private readonly ConcurrentDictionary<Uri, KeyClient> keyClientDictionary = new ConcurrentDictionary<Uri, KeyClient>();

/// <summary>
/// Holds references to the fetch key tasks and maps them to their corresponding Azure Key Vault Key Identifier (URI).
/// These tasks will be used for returning the key in the event that the fetch task has not finished depositing the
/// key into the key dictionary.
/// </summary>
private readonly ConcurrentDictionary<string, Task<Azure.Response<KeyVaultKey>>> _keyFetchTaskDictionary = new ConcurrentDictionary<string, Task<Azure.Response<KeyVaultKey>>>();

/// <summary>
/// Holds references to the Azure Key Vault keys and maps them to their corresponding Azure Key Vault Key Identifier (URI).
/// </summary>
private readonly ConcurrentDictionary<string, KeyVaultKey> _keyDictionary = new ConcurrentDictionary<string, KeyVaultKey>();

/// <summary>
/// Holds references to the Azure Key Vault CryptographyClient objects and maps them to their corresponding Azure Key Vault Key Identifier (URI).
/// </summary>
private readonly ConcurrentDictionary<string, CryptographyClient> cryptoClientDictionary = new ConcurrentDictionary<string, CryptographyClient>();

/// <summary>
/// Constructs a new KeyCryptographer
/// </summary>
/// <param name="tokenCredential"></param>
internal AzureSqlKeyCryptographer(TokenCredential tokenCredential)
{
TokenCredential = tokenCredential;
}

internal AzureSqlKeyCryptographer(AuthenticationCallback authenticationCallback)
{
AuthenticationCallback = authenticationCallback;
isUsingLegacyAuthentication = true;
}

/// <summary>
/// Adds the key, specified by the Key Identifier URI, to the cache.
/// </summary>
/// <param name="keyIdentifierUri"></param>
internal void AddKey(string keyIdentifierUri)
{
if (TheKeyHasNotBeenCached(keyIdentifierUri))
{
if (isUsingLegacyAuthentication)
{
TokenCredential = new AzureKeyVaultProviderTokenCredential(AuthenticationCallback, keyIdentifierUri);
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
}

ParseAKVPath(keyIdentifierUri, out Uri vaultUri, out string keyName);
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
CreateKeyClient(vaultUri);
FetchKey(vaultUri, keyName, keyIdentifierUri);
}

bool TheKeyHasNotBeenCached(string k) => !_keyDictionary.ContainsKey(k) && !_keyFetchTaskDictionary.ContainsKey(k);
}

/// <summary>
/// Returns the key specified by the Key Identifier URI
/// </summary>
/// <param name="keyIdentifierUri"></param>
/// <returns></returns>
internal KeyVaultKey GetKey(string keyIdentifierUri)
{
if (_keyDictionary.ContainsKey(keyIdentifierUri))
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
{
return _keyDictionary[keyIdentifierUri];
}

if (_keyFetchTaskDictionary.ContainsKey(keyIdentifierUri))
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
{
return Task.Run(() => _keyFetchTaskDictionary[keyIdentifierUri]).Result;
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
}

// Not a public exception - not likely to occur.
throw new KeyNotFoundException($"The key with identifier {keyIdentifierUri} was not found.");
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
/// Gets the public Key size in bytes.
/// </summary>
/// <param name="keyIdentifierUri">The key vault key identifier URI</param>
/// <returns></returns>
internal int GetKeySize(string keyIdentifierUri)
{
return GetKey(keyIdentifierUri).Key.N.Length;
}

/// <summary>
/// Generates signature based on RSA PKCS#v1.5 scheme using a specified Azure Key Vault Key URL.
/// </summary>
/// <param name="message">The data to sign</param>
/// <param name="keyIdentifierUri">The key vault key identifier URI</param>
/// <returns></returns>
internal byte[] SignData(byte[] message, string keyIdentifierUri)
{
CryptographyClient cryptographyClient = GetCryptographyClient(keyIdentifierUri);
return cryptographyClient.SignData(RS256, message).Signature;
}

internal bool VerifyData(byte[] message, byte[] signature, string keyIdentifierUri)
{
CryptographyClient cryptographyClient = GetCryptographyClient(keyIdentifierUri);
return cryptographyClient.VerifyData(RS256, message, signature).IsValid;
}

internal byte[] UnwrapKey(KeyWrapAlgorithm keyWrapAlgorithm, byte[] encryptedKey, string keyIdentifierUri)
{
CryptographyClient cryptographyClient = GetCryptographyClient(keyIdentifierUri);
return cryptographyClient.UnwrapKey(keyWrapAlgorithm, encryptedKey).Key;
}

internal byte[] WrapKey(KeyWrapAlgorithm keyWrapAlgorithm, byte[] key, string keyIdentifierUri)
{
CryptographyClient cryptographyClient = GetCryptographyClient(keyIdentifierUri);
return cryptographyClient.WrapKey(keyWrapAlgorithm, key).EncryptedKey;
}

private CryptographyClient GetCryptographyClient(string keyIdentifierUri)
{
if (cryptoClientDictionary.ContainsKey(keyIdentifierUri))
{
return cryptoClientDictionary[keyIdentifierUri];
}

CryptographyClient cryptographyClient = new CryptographyClient(GetKey(keyIdentifierUri).Id, TokenCredential);
cryptoClientDictionary[keyIdentifierUri] = cryptographyClient;
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved

return cryptographyClient;
}

/// <summary>
///
/// </summary>
/// <param name="vaultUri">The Azure Key Vault URI</param>
/// <param name="keyName">The name of the Azure Key Vault key</param>
/// <param name="keyResourceUri">The Azure Key Vault key identifier</param>
private void FetchKey(Uri vaultUri, string keyName, string keyResourceUri)
{
var fetchKeyTask = FetchKeyFromKeyVault(vaultUri, keyName);
_keyFetchTaskDictionary[keyResourceUri] = fetchKeyTask;

fetchKeyTask
.ContinueWith(k => ValidateRsaKey(k.Result))
.ContinueWith(k => _keyDictionary[keyResourceUri] = k.Result);

Task.Run(() => fetchKeyTask);
}

/// <summary>
/// Looks up the KeyClient object by it's URI and then fethces the key by name.
/// </summary>
/// <param name="vaultUri">The Azure Key Vault URI</param>
/// <param name="keyName">Then name of the key</param>
/// <returns></returns>
private Task<Azure.Response<KeyVaultKey>> FetchKeyFromKeyVault(Uri vaultUri, string keyName) => keyClientDictionary[vaultUri].GetKeyAsync(keyName);

/// <summary>
/// Validates that a key is of type RSA
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
private KeyVaultKey ValidateRsaKey(KeyVaultKey key)
{
if (key.KeyType != KeyType.Rsa && key.KeyType != KeyType.RsaHsm)
{
throw new FormatException(string.Format(CultureInfo.InvariantCulture, Strings.NonRsaKeyTemplate, key.KeyType));
}

return key;
}

/// <summary>
/// Instantiates and adds a KeyClient to the KeyClient dictionary
/// </summary>
/// <param name="vaultUri">The Azure Key Vault URI</param>
private void CreateKeyClient(Uri vaultUri)
{
if (!keyClientDictionary.ContainsKey(vaultUri))
{
keyClientDictionary[vaultUri] = new KeyClient(vaultUri, TokenCredential);
}
}

/// <summary>
/// Validates and parses the Azure Key Vault URI and key name.
/// </summary>
/// <param name="masterKeyPath">The Azure Key Vault key identifier</param>
/// <param name="vaultUri">The Azure Key Vault URI</param>
/// <param name="masterKeyName">The name of the key</param>
private void ParseAKVPath(string masterKeyPath, out Uri vaultUri, out string masterKeyName)
{
Uri masterKeyPathUri = new Uri(masterKeyPath);
vaultUri = new Uri(masterKeyPathUri.GetLeftPart(UriPartial.Authority));
masterKeyName = masterKeyPathUri.Segments[2];
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@
<PackageReference Condition="$(ReferenceType)=='Package'" Include="Microsoft.Data.SqlClient" Version="$(TestMicrosoftDataSqlClientVersion)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.KeyVault" Version="$(MicrosoftAzureKeyVaultVersion)" />
<PackageReference Include="Microsoft.Azure.KeyVault.WebKey" Version="$(MicrosoftAzureKeyVaultWebKeyVersion)" />
<PackageReference Include="Microsoft.Rest.ClientRuntime" Version="$(MicrosoftRestClientRuntimeVersion)" />
<PackageReference Include="Microsoft.Rest.ClientRuntime.Azure" Version="$(MicrosoftRestClientRuntimeAzureVersion)" />
<PackageReference Include="Azure.Core" Version="$(AzureCoreVersion)" />
<PackageReference Include="Azure.Security.KeyVault.Keys" Version="$(AzureSecurityKeyVaultKeysVersion)" />
</ItemGroup>
</Project>
Loading