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

Add multi-user key store provider registration support #1056

Merged
merged 49 commits into from
May 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
aa57be4
netcore implementation
johnnypham Apr 5, 2021
3343f11
netfx implementation
johnnypham Apr 6, 2021
3817594
functional+manual tests
johnnypham Apr 7, 2021
561b703
public api docs+refs
johnnypham Apr 7, 2021
1fe38ef
Merge remote-tracking branch 'upstream/main' into multitenant-provide…
johnnypham Apr 22, 2021
89f0467
Update ExceptionRegisterKeyStoreProvider.cs
johnnypham Apr 22, 2021
d230367
Merge remote-tracking branch 'upstream/main' into multitenant-provide…
johnnypham Apr 28, 2021
814f2d5
add local cache to akv provider
johnnypham Apr 28, 2021
cd44659
logic to use akv local cek cache
johnnypham Apr 28, 2021
ecb7171
tests for akv local cek cache
johnnypham Apr 28, 2021
2ad5fc0
refactor key store provider tests
johnnypham Apr 28, 2021
13e9344
Update AKVTests.cs
johnnypham Apr 28, 2021
c45dc23
Update AKVUnitTests.cs
johnnypham Apr 28, 2021
f2be4b2
Update AKVTests.cs
johnnypham Apr 29, 2021
4319ab7
Update AKVTests.cs
johnnypham Apr 29, 2021
36f6c0a
update failing test and remove unused constant in akv provider
johnnypham Apr 29, 2021
c79d95c
address feedback
johnnypham May 5, 2021
cc762ed
Update src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlS…
May 10, 2021
2d7ca05
Update src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlS…
May 10, 2021
dbdba21
Update src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlS…
May 10, 2021
1fa3c47
Update src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlS…
May 10, 2021
68f24f7
Update src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlS…
May 10, 2021
ba28eba
address feedback
May 10, 2021
2842c71
revert changes to .sln
May 10, 2021
bef6ec7
address feedback
May 11, 2021
c7f07c9
Update SqlSecurityUtility.cs
May 11, 2021
6fdc8bd
disable provider-level caching when akv provider is registered globally
May 11, 2021
46eab82
address feedback
May 13, 2021
b964ce6
Update src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClien…
May 13, 2021
8d486ee
Update Versions.props
May 13, 2021
b488f79
Merge branch 'multitenant-provider-command' of https://github.com/joh…
May 13, 2021
2f1afc4
update cache tests
May 13, 2021
5d93ef4
add signature cache test for enclave=false
May 13, 2021
1c55226
address feedback
May 14, 2021
74b06b6
remove unnecessary insert in test
May 14, 2021
ac4ff87
add test to check if cek caching is disabled when akv provider is reg…
May 14, 2021
d73a9d2
add tests
May 14, 2021
6ad4eb9
Update src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted…
May 14, 2021
c5dd485
Update src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted…
May 14, 2021
9dea525
Update src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted…
May 14, 2021
4707ff2
Update src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted…
May 14, 2021
6eccf60
Update doc/snippets/Microsoft.Data.SqlClient/SqlColumnEncryptionKeySt…
May 14, 2021
38923d1
Update src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted…
May 14, 2021
5b111d9
add threading namespace
May 14, 2021
4494979
refactor tests
May 14, 2021
82e8c7b
only run test when akv provider is registered globally
May 14, 2021
1a254d7
Update Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider…
May 17, 2021
1ebe277
Merge remote-tracking branch 'upstream/main' into multitenant-provide…
May 17, 2021
324e2e1
code style improvements
May 17, 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
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
<docs>
<members name="SqlColumnEncryptionKeyStoreProvider">
<SqlColumnEncryptionKeyStoreProvider>
<summary>Base class for all key store providers. A custom provider must derive from this class and override its member functions and then register it using SqlConnection.RegisterColumnEncryptionKeyStoreProviders(). For details see, <see href="https://docs.microsoft.com/sql/relational-databases/security/encryption/always-encrypted-database-engine"> Always Encrypted</see>.
<summary>Base class for all key store providers. A custom provider must derive from this class and override its member functions and then register it using
<see cref="M:Microsoft.Data.SqlClient.SqlConnection.RegisterColumnEncryptionKeyStoreProviders()" />,
<see cref="M:Microsoft.Data.SqlClient.SqlConnection.RegisterColumnEncryptionKeyStoreProvidersOnConnection()" /> or
<see cref="M:Microsoft.Data.SqlClient.SqlCommand.RegisterColumnEncryptionKeyStoreProvidersOnCommand()" />.
For details see, <see href="https://docs.microsoft.com/sql/relational-databases/security/encryption/always-encrypted-database-engine"> Always Encrypted</see>.
</summary>
<remarks>To be added.</remarks>
</SqlColumnEncryptionKeyStoreProvider>
<ctor>
<summary>Initializes a new instance of the SqlColumnEncryptionKeyStoreProviderClass.</summary>
Expand Down Expand Up @@ -55,5 +58,10 @@ The <xref:Microsoft.Data.SqlClient.SqlColumnEncryptionKeyStoreProvider.SignColum
<returns>When implemented in a derived class, the method is expected to return true if the specified signature is valid, or false if the specified signature is not valid. The default implementation throws NotImplementedException.</returns>
<remarks>To be added.</remarks>
</VerifyColumnMasterKeyMetadata>
<ColumnEncryptionKeyCacheTtl>
<summary>Gets or sets the lifespan of the decrypted column encryption key in the cache. Once the timespan has elapsed, the decrypted column encryption key is discarded and must be revalidated.</summary>
<remarks>Internally, there is a cache of column encryption keys (once they are decrypted). This is useful for rapidly decrypting multiple data values. The default value is 2 hours. Setting this value to zero disables caching.
</remarks>
</ColumnEncryptionKeyCacheTtl>
</members>
</docs>
19 changes: 19 additions & 0 deletions doc/snippets/Microsoft.Data.SqlClient/SqlCommand.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2769,6 +2769,25 @@ You must set the value for this property before the command is executed for it t
]]></format>
</remarks>
</Notification>
<RegisterColumnEncryptionKeyStoreProvidersOnCommand>
<param name="customProviders">Dictionary of custom column encryption key providers</param>
<summary>Registers the encryption key store providers on the <see cref="T:Microsoft.Data.SqlClient.SqlCommand" /> instance. If this function has been called, any providers registered using the <see cref="M:Microsoft.Data.SqlClient.SqlConnection.RegisterColumnEncryptionKeyStoreProviders()" /> or
<see cref="M:Microsoft.Data.SqlClient.SqlConnection.RegisterColumnEncryptionKeyStoreProvidersOnConnection()" /> methods will be ignored. This function can be called more than once. This does shallow copying of the dictionary so that the app cannot alter the custom provider list once it has been set.</summary>
<exception cref="T:System.ArgumentNullException">
A null dictionary was provided.

-or-

A string key in the dictionary was null or empty.

-or-

An EncryptionKeyStoreProvider value in the dictionary was null.
</exception>
<exception cref="T:System.ArgumentException">
A string key in the dictionary started with "MSSQL_". This prefix is reserved for system providers.
</exception>
</RegisterColumnEncryptionKeyStoreProvidersOnCommand>
<RetryLogicProvider>
<summary>
Gets or sets a value that specifies the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,6 @@ namespace Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider
{
internal static class Constants
{
/// <summary>
/// Hashing algorithm used for signing
/// </summary>
internal const string HashingAlgorithm = @"RS256";

/// <summary>
/// Azure Key Vault Domain Name
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// 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 Microsoft.Extensions.Caching.Memory;
using System;
using static System.Math;

namespace Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider
{
/// <summary>
/// LocalCache is to reuse heavy objects.
/// When performing a heavy creation operation, we will save the result in our cache container.
/// The next time that we need that result, we will pull it from the cache container, instead of performing the heavy operation again.
/// It is used for decrypting CEKs and verifying CMK metadata. Encrypted CEKs and signatures are different every time, even
/// when done with the same key, and should not be cached.
/// </summary>
internal class LocalCache<TKey, TValue>
{
/// <summary>
/// A simple thread-safe implementation of an in-memory Cache.
/// When the process dies, the cache dies with it.
/// </summary>
private readonly MemoryCache _cache;

private readonly int _maxSize;

/// <summary>
/// Sets an absolute expiration time, relative to now.
/// </summary>
internal TimeSpan? TimeToLive { get; set; }

/// <summary>
/// Gets the count of the current entries for diagnostic purposes.
/// </summary>
internal int Count => _cache.Count;

/// <summary>
/// Constructs a new <see cref="LocalCache{TKey, TValue}">LocalCache</see> object.
/// </summary>
internal LocalCache(int maxSizeLimit = int.MaxValue)
JRahnama marked this conversation as resolved.
Show resolved Hide resolved
{
if(maxSizeLimit <= 0)
{
throw new ArgumentOutOfRangeException(nameof(maxSizeLimit));
}

_maxSize = maxSizeLimit;
_cache = new MemoryCache(new MemoryCacheOptions());
}

/// <summary>
/// Looks for the cache entry that maps to the <paramref name="key"/> value. If it exists (cache hit) it will simply be
/// returned. Otherwise, the <paramref name="createItem"/> delegate function will be invoked to create the value.
/// It will then get stored it in the cache and set the time-to-live before getting returned.
/// </summary>
/// <param name="key">The key for the cache entry.</param>
/// <param name="createItem">The delegate function that will create the cache entry if it does not exist.</param>
/// <returns>The cache entry.</returns>
internal TValue GetOrCreate(TKey key, Func<TValue> createItem)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: this naming convention seems a bit insufficient. Is it possible to change it to GetOrCreateLocalCache or something more explanatory?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not getting/creating a local cache. It's getting/creating the given key. When used, it makes sense to me, e.g. columnEncryptionKeyCache.GetOrCreate(encryptedColumnEncryptionKey, createItem);

{
if (TimeToLive <= TimeSpan.Zero)
{
return createItem();
}

if (!_cache.TryGetValue(key, out TValue cacheEntry))
{
if (_cache.Count == _maxSize)
{
_cache.Compact(Max(0.10, 1.0 / _maxSize));
}

cacheEntry = createItem();
var cacheEntryOptions = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeToLive
};

_cache.Set(key, cacheEntry, cacheEntryOptions);
}

return cacheEntry;
}

/// <summary>
/// Determines whether the <see cref="LocalCache{TKey, TValue}">LocalCache</see> contains the specified key.
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
internal bool Contains(TKey key)
{
return _cache.TryGetValue(key, out _);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="$(MicrosoftSourceLinkGitHubVersion)" PrivateAssets="All" />
<PackageReference Include="Azure.Core" Version="$(AzureCoreVersion)" />
<PackageReference Include="Azure.Security.KeyVault.Keys" Version="$(AzureSecurityKeyVaultKeysVersion)" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="$(MicrosoftExtensionsCachingMemoryVersion)" />
johnnypham marked this conversation as resolved.
Show resolved Hide resolved
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Security.KeyVault.Keys.Cryptography;
using static Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.Validator;
Expand Down Expand Up @@ -63,6 +62,33 @@ public class SqlColumnEncryptionAzureKeyVaultProvider : SqlColumnEncryptionKeySt
/// </summary>
public readonly string[] TrustedEndPoints;

/// <summary>
johnnypham marked this conversation as resolved.
Show resolved Hide resolved
/// A cache of column encryption keys (once they are decrypted). This is useful for rapidly decrypting multiple data values.
/// </summary>
private readonly LocalCache<string, byte[]> _columnEncryptionKeyCache = new() { TimeToLive = TimeSpan.FromHours(2) };

/// <summary>
/// A cache for storing the results of signature verification of column master key metadata.
/// </summary>
private readonly LocalCache<Tuple<string, bool, string>, bool> _columnMasterKeyMetadataSignatureVerificationCache =
new(maxSizeLimit: 2000) { TimeToLive = TimeSpan.FromDays(10) };

/// <summary>
/// Gets or sets the lifespan of the decrypted column encryption key in the cache.
/// Once the timespan has elapsed, the decrypted column encryption key is discarded
/// and must be revalidated.
/// </summary>
/// <remarks>
/// Internally, there is a cache of column encryption keys (once they are decrypted).
/// This is useful for rapidly decrypting multiple data values. The default value is 2 hours.
/// Setting the <see cref="ColumnEncryptionKeyCacheTtl"/> to zero disables caching.
/// </remarks>
public override TimeSpan? ColumnEncryptionKeyCacheTtl
{
get => _columnEncryptionKeyCache.TimeToLive;
set => _columnEncryptionKeyCache.TimeToLive = value;
}

#endregion

#region Constructors
Expand Down Expand Up @@ -130,10 +156,16 @@ public override bool VerifyColumnMasterKeyMetadata(string masterKeyPath, bool al
{
ValidateNonEmptyAKVPath(masterKeyPath, isSystemOp: true);

// Also validates key is of RSA type.
KeyCryptographer.AddKey(masterKeyPath);
byte[] message = CompileMasterKeyMetadata(masterKeyPath, allowEnclaveComputations);
return KeyCryptographer.VerifyData(message, signature, masterKeyPath);
var key = Tuple.Create(masterKeyPath, allowEnclaveComputations, ToHexString(signature));
return GetOrCreateSignatureVerificationResult(key, VerifyColumnMasterKeyMetadata);

bool VerifyColumnMasterKeyMetadata()
JRahnama marked this conversation as resolved.
Show resolved Hide resolved
{
// Also validates key is of RSA type.
KeyCryptographer.AddKey(masterKeyPath);
byte[] message = CompileMasterKeyMetadata(masterKeyPath, allowEnclaveComputations);
return KeyCryptographer.VerifyData(message, signature, masterKeyPath);
}
}

/// <summary>
Expand All @@ -153,58 +185,62 @@ public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string e
ValidateNotEmpty(encryptedColumnEncryptionKey, nameof(encryptedColumnEncryptionKey));
ValidateVersionByte(encryptedColumnEncryptionKey[0], s_firstVersion[0]);

// Also validates whether the key is RSA one or not and then get the key size
KeyCryptographer.AddKey(masterKeyPath);
return GetOrCreateColumnEncryptionKey(ToHexString(encryptedColumnEncryptionKey), DecryptEncryptionKey);

int keySizeInBytes = KeyCryptographer.GetKeySize(masterKeyPath);
byte[] DecryptEncryptionKey()
{
// Also validates whether the key is RSA one or not and then get the key size
KeyCryptographer.AddKey(masterKeyPath);

// Get key path length
int currentIndex = s_firstVersion.Length;
ushort keyPathLength = BitConverter.ToUInt16(encryptedColumnEncryptionKey, currentIndex);
currentIndex += sizeof(ushort);
int keySizeInBytes = KeyCryptographer.GetKeySize(masterKeyPath);

// Get ciphertext length
ushort cipherTextLength = BitConverter.ToUInt16(encryptedColumnEncryptionKey, currentIndex);
currentIndex += sizeof(ushort);
// Get key path length
int currentIndex = s_firstVersion.Length;
ushort keyPathLength = BitConverter.ToUInt16(encryptedColumnEncryptionKey, currentIndex);
currentIndex += sizeof(ushort);

// Skip KeyPath
// KeyPath exists only for troubleshooting purposes and doesnt need validation.
currentIndex += keyPathLength;
// Get ciphertext length
ushort cipherTextLength = BitConverter.ToUInt16(encryptedColumnEncryptionKey, currentIndex);
currentIndex += sizeof(ushort);

// validate the ciphertext length
if (cipherTextLength != keySizeInBytes)
{
throw ADP.InvalidCipherTextLength(cipherTextLength, keySizeInBytes, masterKeyPath);
}
// Skip KeyPath
// KeyPath exists only for troubleshooting purposes and doesnt need validation.
currentIndex += keyPathLength;

// Validate the signature length
int signatureLength = encryptedColumnEncryptionKey.Length - currentIndex - cipherTextLength;
if (signatureLength != keySizeInBytes)
{
throw ADP.InvalidSignatureLengthTemplate(signatureLength, keySizeInBytes, masterKeyPath);
}
// validate the ciphertext length
if (cipherTextLength != keySizeInBytes)
{
throw ADP.InvalidCipherTextLength(cipherTextLength, keySizeInBytes, masterKeyPath);
}

// Validate the signature length
int signatureLength = encryptedColumnEncryptionKey.Length - currentIndex - cipherTextLength;
if (signatureLength != keySizeInBytes)
{
throw ADP.InvalidSignatureLengthTemplate(signatureLength, keySizeInBytes, masterKeyPath);
}

// Get ciphertext
byte[] cipherText = encryptedColumnEncryptionKey.Skip(currentIndex).Take(cipherTextLength).ToArray();
currentIndex += cipherTextLength;
// Get ciphertext
byte[] cipherText = encryptedColumnEncryptionKey.Skip(currentIndex).Take(cipherTextLength).ToArray();
currentIndex += cipherTextLength;

// Get signature
byte[] signature = encryptedColumnEncryptionKey.Skip(currentIndex).Take(signatureLength).ToArray();
// Get signature
byte[] signature = encryptedColumnEncryptionKey.Skip(currentIndex).Take(signatureLength).ToArray();

// Compute the message to validate the signature
byte[] message = encryptedColumnEncryptionKey.Take(encryptedColumnEncryptionKey.Length - signatureLength).ToArray();
// Compute the message to validate the signature
byte[] message = encryptedColumnEncryptionKey.Take(encryptedColumnEncryptionKey.Length - signatureLength).ToArray();

if (null == message)
{
throw ADP.NullHashFound();
}
if (null == message)
{
throw ADP.NullHashFound();
}

if (!KeyCryptographer.VerifyData(message, signature, masterKeyPath))
{
throw ADP.InvalidSignatureTemplate(masterKeyPath);
if (!KeyCryptographer.VerifyData(message, signature, masterKeyPath))
{
throw ADP.InvalidSignatureTemplate(masterKeyPath);
}
return KeyCryptographer.UnwrapKey(s_keyWrapAlgorithm, cipherText, masterKeyPath);
}

return KeyCryptographer.UnwrapKey(s_keyWrapAlgorithm, cipherText, masterKeyPath);
}

/// <summary>
Expand Down Expand Up @@ -310,6 +346,49 @@ private byte[] CompileMasterKeyMetadata(string masterKeyPath, bool allowEnclaveC
return Encoding.Unicode.GetBytes(masterkeyMetadata.ToLowerInvariant());
}

/// <summary>
/// Converts the numeric value of each element of a specified array of bytes to its equivalent hexadecimal string representation.
/// </summary>
/// <param name="source">An array of bytes to convert.</param>
/// <returns>A string of hexadecimal characters</returns>
/// <remarks>
/// Produces a string of hexadecimal character pairs preceded with "0x", where each pair represents the corresponding element in value; for example, "0x7F2C4A00".
/// </remarks>
private string ToHexString(byte[] source)
{
if (source is null)
{
return null;
}

return "0x" + BitConverter.ToString(source).Replace("-", "");
}

/// <summary>
/// Returns the cached decrypted column encryption key, or unwraps the encrypted column encryption key if not present.
/// </summary>
/// <param name="encryptedColumnEncryptionKey">Encrypted Column Encryption Key</param>
/// <param name="createItem">The delegate function that will decrypt the encrypted column encryption key.</param>
/// <returns>The decrypted column encryption key.</returns>
/// <remarks>
///
/// </remarks>
private byte[] GetOrCreateColumnEncryptionKey(string encryptedColumnEncryptionKey, Func<byte[]> createItem)
{
return _columnEncryptionKeyCache.GetOrCreate(encryptedColumnEncryptionKey, createItem);
}

/// <summary>
/// Returns the cached signature verification result, or proceeds to verify if not present.
/// </summary>
/// <param name="keyInformation">The encryptionKeyId, allowEnclaveComputations and hexadecimal signature.</param>
/// <param name="createItem">The delegate function that will perform the verification.</param>
/// <returns></returns>
private bool GetOrCreateSignatureVerificationResult(Tuple<string, bool, string> keyInformation, Func<bool> createItem)
{
return _columnMasterKeyMetadataSignatureVerificationCache.GetOrCreate(keyInformation, createItem);
}

#endregion
}
}
Loading