-
Notifications
You must be signed in to change notification settings - Fork 311
Fix encryption key cache design for AKV provider #3464
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR refactors the Azure Key Vault (AKV) provider cache design to address cache TTL enforcement, remove sync-over-async calls, and prevent redundant cache fetches under concurrency.
- Modernized
SqlSymmetricKeyCache
to use target-typednew
, inlineTryGetValue
, and removed forced TTL reset. - Switched
SqlConnection
to C# 12 collection expressions and dropped global TTL zeroing. - Refactored
SqlColumnEncryptionAzureKeyVaultProvider
andAzureSqlKeyCryptographer
from async task-based caching to synchronous patterns with a semaphore guard.
Reviewed Changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
File | Description |
---|---|
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSymmetricKeyCache.cs | Modernized cache lookup and entry options; removed TTL override |
src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs | Replaced new List<string>(…) with [ .. keys] ; removed TTL zeroing |
src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs | Added SemaphoreSlim around local cache access |
src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/LocalCache.cs | Adjusted using order; removed unused Contains method |
src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs | Replaced async fetch/dictionaries with sync KeyVaultKey cache |
Comments suppressed due to low confidence (5)
src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs:31
- [nitpick] The
_keyFetchTaskDictionary
now storesKeyVaultKey
instances rather than tasks. Consider renaming it to better reflect its contents, e.g._keyCacheDictionary
or_fetchedKeys
.
private readonly ConcurrentDictionary<string, KeyVaultKey> _keyFetchTaskDictionary = new();
src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs:175
FetchKeyFromKeyVault
retrieveskeyClient
but never initializes it. You need to callCreateKeyClient(vaultUri)
before accessing_keyClientDictionary
or handle the case wherekeyClient
is null.
return keyClient?.GetKey(keyName, keyVersion);
src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs:81
- With the async/task-based guard removed, parallel calls for a missing key will all invoke
FetchKeyFromKeyVault
. Consider adding a lock or double-checked locking to prevent N+1 fetches under concurrency.
if (_keyFetchTaskDictionary.TryGetValue(keyIdentifierUri, out KeyVaultKey keyVaultKey))
src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs:279
- The method signature returns
List<string>
, but the collection expression[...]
produces an array by default. This will cause a type mismatch. Either construct aList<string>
explicitly or verify that C# 12 collection expressions target the declared return type.
return [.. s_systemColumnEncryptionKeyStoreProviders.Keys];
src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs:294
- Same as above: returning a collection expression for a method declared to return
List<string>
may not compile. Considernew List<string>(...)
or confirm the collection expression creates aList<string>
.
return [.. _customColumnEncryptionKeyStoreProviders.Keys];
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSymmetricKeyCache.cs
Outdated
Show resolved
Hide resolved
<value>Internal error. Empty '{0}' specified.</value> | ||
</data> | ||
<data name="GetKeyFailed" xml:space="preserve"> | ||
<value>Fetching the key failed: '{0}'.</value> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<value>Fetching the key failed: '{0}'.</value> | |
<value>Failed to fetch Key from Azure Key Vault. Key: {0}</value> |
try | ||
{ | ||
ParseAKVPath(keyIdentifierUri, out Uri vaultUri, out string keyName, out string keyVersion); | ||
CreateKeyClient(vaultUri); | ||
FetchKey(vaultUri, keyName, keyVersion, keyIdentifierUri); | ||
} | ||
// Allow only one thread to proceed to ensure thread safety | ||
// as we will need to fetch key information from Azure Key Vault if the key is not found in cache. | ||
_keyDictionarySemaphore.Wait(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move _keyDictionarySemaphore.Wait();
to before the try. We don't want to execute the finally if Wait() throws an exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if GetOrAdd() could be used here as well, and avoid the semaphore? Pass a lambda that fetches the key, and let ConcurrentDictionary block all other calls until the fetch + add is complete. Would that work?
try | ||
{ | ||
// Allow only one thread to access the cache at a time. | ||
_cacheSemaphore.Wait(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move _cacheSemaphore.Wait();
to before the try. We don't want to execute the finally if Wait() throws an exception.
try | ||
{ | ||
Debug.Assert(SqlConnection.ColumnEncryptionTrustedMasterKeyPaths is not null, @"SqlConnection.ColumnEncryptionTrustedMasterKeyPaths should not be null"); | ||
|
||
SqlSecurityUtility.ThrowIfKeyPathIsNotTrustedForServer(serverName, keyInfo.keyPath); | ||
// Acquire the lock to ensure thread safety when accessing the cache | ||
_cacheLock.Wait(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move _cacheLock.Wait();
to before the try. We don't want to execute the finally if Wait() throws an exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A variety of comments.
/// </summary> | ||
private readonly ConcurrentDictionary<string, KeyVaultKey> _keyDictionary = new(); | ||
private SemaphoreSlim _keyDictionarySemaphore = new(1, 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SemaphoreSlim is IDisposable. Do we need to dispose of it?
{ | ||
Task<Azure.Response<KeyVaultKey>> fetchKeyTask = FetchKeyFromKeyVault(vaultUri, keyName, keyVersion); | ||
_keyFetchTaskDictionary.AddOrUpdate(keyResourceUri, fetchKeyTask, (k, v) => fetchKeyTask); | ||
AKVEventSource.Log.TryTraceEvent("Fetching requested master key: {0}", keyName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Master key, or column encryption key?
} | ||
throw ADP.GetKeyFailed(keyName); | ||
} | ||
else |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary else block.
_keyClientDictionary.TryAdd(vaultUri, keyClient); | ||
} | ||
|
||
return keyClient; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If TryAdd() fails (because another thread just added a KeyClient for this vaultUri), then we're returning a different KeyClient instance than has been stored in the dictionary. Is this a problem?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you could replace this function body with:
return _keyClientDictionary.GetOrAdd(vaultUri, (_) => new KeyClient(vaultUri, TokenCredential));
try | ||
{ | ||
ParseAKVPath(keyIdentifierUri, out Uri vaultUri, out string keyName, out string keyVersion); | ||
CreateKeyClient(vaultUri); | ||
FetchKey(vaultUri, keyName, keyVersion, keyIdentifierUri); | ||
} | ||
// Allow only one thread to proceed to ensure thread safety | ||
// as we will need to fetch key information from Azure Key Vault if the key is not found in cache. | ||
_keyDictionarySemaphore.Wait(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if GetOrAdd() could be used here as well, and avoid the semaphore? Pass a lambda that fetches the key, and let ConcurrentDictionary block all other calls until the fetch + add is complete. Would that work?
} | ||
|
||
/// <summary> | ||
/// Validates that a key is of type RSA | ||
/// </summary> | ||
/// <param name="key"></param> | ||
/// <returns></returns> | ||
private KeyVaultKey ValidateRsaKey(KeyVaultKey key) | ||
private static KeyVaultKey ValidateRsaKey(KeyVaultKey key) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this function return KeyVaultKey? Not sure what the intention is here, since neither the return value nor exceptions are documented.
@@ -397,14 +400,7 @@ private byte[] CompileMasterKeyMetadata(string masterKeyPath, bool allowEnclaveC | |||
/// Produces a string of hexadecimal character pairs preceded with "0x", where each pair represents the corresponding element in value; for example, "0x7F2C4A00". |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
value -> source
Description
This PR fixes a couple of issues:
Issues
Fixes above issues, and additionally:
Testing
Repro can be found here: https://gist.github.com/cheenamalhotra/79f8ae709147e0b52239fd54c3049932
When registering AKV provider globally:
Guidelines
Please review the contribution guidelines before submitting a pull request:
P.S. We might want to consider it for 6.1.0 as it's critically needed for a customer.