From 65dfdb973a2e0558121c3a7cf515b5062337d9bc Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Wed, 10 Jun 2026 23:44:27 -0700 Subject: [PATCH 1/2] [Release/6.1] Fix fetching signature verification result from cache --- .../SqlClient/SignatureVerificationCache.cs | 196 +++++++++++------- .../Data/SqlClient/SqlSecurityUtility.cs | 18 +- .../SignatureVerificationCacheTests.cs | 49 +++++ 3 files changed, 176 insertions(+), 87 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SignatureVerificationCacheTests.cs diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs index 40d4ed9b79..15c951af24 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs @@ -9,6 +9,29 @@ namespace Microsoft.Data.SqlClient { + /// + /// Tri-state result returned by . + /// Distinguishes a cache miss from a cached negative result so callers cannot conflate the two. + /// + internal enum SignatureVerificationResult + { + /// + /// No cached entry exists for the requested CMK metadata. + /// The caller must verify the signature with the key store provider. + /// + NotFound, + + /// + /// A cached entry exists and indicates that signature verification previously failed. + /// + False, + + /// + /// A cached entry exists and indicates that signature verification previously succeeded. + /// + True, + } + /// /// Cache for storing result of signature verification of CMK Metadata /// @@ -17,99 +40,105 @@ internal class ColumnMasterKeyMetadataSignatureVerificationCache private const int _cacheSize = 2000; // Cache size in number of entries. private const int _cacheTrimThreshold = 300; // Threshold above the cache size when we start trimming. - private const string _className = "ColumnMasterKeyMetadataSignatureVerificationCache"; - private const string _getSignatureVerificationResultMethodName = "GetSignatureVerificationResult"; - private const string _addSignatureVerificationResultMethodName = "AddSignatureVerificationResult"; - private const string _masterkeypathArgumentName = "masterKeyPath"; - private const string _keyStoreNameArgumentName = "keyStoreName"; - private const string _signatureName = "signature"; private const string _cacheLookupKeySeparator = ":"; - - private static readonly ColumnMasterKeyMetadataSignatureVerificationCache _signatureVerificationCache = new ColumnMasterKeyMetadataSignatureVerificationCache(); - - //singleton instance - internal static ColumnMasterKeyMetadataSignatureVerificationCache Instance { get { return _signatureVerificationCache; } } + + /// + /// Gets the process-wide singleton instance of the signature verification cache. + /// + internal static ColumnMasterKeyMetadataSignatureVerificationCache Instance { get; } = new(); private readonly MemoryCache _cache; - private int _inTrim = 0; + private int _inTrim; private ColumnMasterKeyMetadataSignatureVerificationCache() { _cache = new MemoryCache(new MemoryCacheOptions()); - _inTrim = 0; } - - /// - /// Get signature verification result for given CMK metadata (KeystoreName, MasterKeyPath, allowEnclaveComputations) and a given signature +/// + /// Get signature verification result for given CMK metadata + /// (KeystoreName, MasterKeyPath, allowEnclaveComputations) and a given signature /// /// Key Store name for CMK /// Key Path for CMK /// boolean indicating whether the key can be sent to enclave /// Signature for the CMK metadata - internal bool GetSignatureVerificationResult(string keyStoreName, string masterKeyPath, bool allowEnclaveComputations, byte[] signature) + /// Tri-state result indicating whether signature verification succeeded, failed, or was not found in cache + /// + /// Thrown when , , + /// or is . + /// + /// + /// Thrown when or + /// is empty or whitespace, or when has length zero. + /// + internal SignatureVerificationResult GetSignatureVerificationResult(string keyStoreName, string masterKeyPath, bool allowEnclaveComputations, byte[] signature) { - ValidateStringArgumentNotNullOrEmpty(masterKeyPath, _masterkeypathArgumentName, _getSignatureVerificationResultMethodName); - ValidateStringArgumentNotNullOrEmpty(keyStoreName, _keyStoreNameArgumentName, _getSignatureVerificationResultMethodName); - ValidateSignatureNotNullOrEmpty(signature, _getSignatureVerificationResultMethodName); + ValidateStringArgumentNotNullOrEmpty(masterKeyPath, nameof(masterKeyPath), nameof(GetSignatureVerificationResult)); + ValidateStringArgumentNotNullOrEmpty(keyStoreName, nameof(keyStoreName), nameof(GetSignatureVerificationResult)); + ValidateSignatureNotNullOrEmpty(signature, nameof(GetSignatureVerificationResult)); string cacheLookupKey = GetCacheLookupKey(masterKeyPath, allowEnclaveComputations, signature, keyStoreName); - return _cache.TryGetValue(cacheLookupKey, out bool value); + if (!_cache.TryGetValue(cacheLookupKey, out bool value)) + { + return SignatureVerificationResult.NotFound; + } + + return value ? SignatureVerificationResult.True : SignatureVerificationResult.False; } /// - /// Add signature verification result for given CMK metadata (KeystoreName, MasterKeyPath, allowEnclaveComputations) and a given signature in the cache + /// Add signature verification result for given CMK metadata (KeystoreName, + /// MasterKeyPath, allowEnclaveComputations) and a given signature in the cache /// /// Key Store name for CMK /// Key Path for CMK /// boolean indicating whether the key can be sent to enclave /// Signature for the CMK metadata /// result indicating signature verification success/failure + /// + /// Thrown when , , + /// or is . + /// + /// + /// Thrown when or is empty or whitespace, + /// or when has length zero. + /// internal void AddSignatureVerificationResult(string keyStoreName, string masterKeyPath, bool allowEnclaveComputations, byte[] signature, bool result) { - ValidateStringArgumentNotNullOrEmpty(masterKeyPath, _masterkeypathArgumentName, _addSignatureVerificationResultMethodName); - ValidateStringArgumentNotNullOrEmpty(keyStoreName, _keyStoreNameArgumentName, _addSignatureVerificationResultMethodName); - ValidateSignatureNotNullOrEmpty(signature, _addSignatureVerificationResultMethodName); + ValidateStringArgumentNotNullOrEmpty(masterKeyPath, nameof(masterKeyPath), nameof(AddSignatureVerificationResult)); + ValidateStringArgumentNotNullOrEmpty(keyStoreName, nameof(keyStoreName), nameof(AddSignatureVerificationResult)); + ValidateSignatureNotNullOrEmpty(signature, nameof(AddSignatureVerificationResult)); string cacheLookupKey = GetCacheLookupKey(masterKeyPath, allowEnclaveComputations, signature, keyStoreName); TrimCacheIfNeeded(); // By default evict after 10 days. - MemoryCacheEntryOptions options = new MemoryCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(10) - }; - _cache.Set(cacheLookupKey, result, options); + _cache.Set(cacheLookupKey, result, absoluteExpirationRelativeToNow: s_verificationCacheTimeout); } - private void ValidateSignatureNotNullOrEmpty(byte[] signature, string methodName) + private static void ValidateSignatureNotNullOrEmpty(byte[] signature, string methodName) { - if (signature == null || signature.Length == 0) + if (signature is null) + { + throw SQL.NullArgumentInternal(nameof(signature), nameof(ColumnMasterKeyMetadataSignatureVerificationCache), methodName); + } + if (signature.Length == 0) { - if (signature == null) - { - throw SQL.NullArgumentInternal(_signatureName, _className, methodName); - } - else - { - throw SQL.EmptyArgumentInternal(_signatureName, _className, methodName); - } + throw SQL.EmptyArgumentInternal(nameof(signature), nameof(ColumnMasterKeyMetadataSignatureVerificationCache), methodName); } } - private void ValidateStringArgumentNotNullOrEmpty(string stringArgValue, string stringArgName, string methodName) + private static void ValidateStringArgumentNotNullOrEmpty(string value, string argumentName, string methodName) { - if (string.IsNullOrWhiteSpace(stringArgValue)) + if (value is null) { - if (stringArgValue == null) - { - throw SQL.NullArgumentInternal(stringArgName, _className, methodName); - } - else - { - throw SQL.EmptyArgumentInternal(stringArgName, _className, methodName); - } + throw SQL.NullArgumentInternal(argumentName, nameof(ColumnMasterKeyMetadataSignatureVerificationCache), methodName); + } + if (string.IsNullOrWhiteSpace(value)) + { + throw SQL.EmptyArgumentInternal(argumentName, nameof(ColumnMasterKeyMetadataSignatureVerificationCache), methodName); } } @@ -117,40 +146,49 @@ private void TrimCacheIfNeeded() { // If the size of the cache exceeds the threshold, set that we are in trimming and trim the cache accordingly. long currentCacheSize = _cache.Count; - if ((currentCacheSize > _cacheSize + _cacheTrimThreshold) && (0 == Interlocked.CompareExchange(ref _inTrim, 1, 0))) + if (currentCacheSize <= CacheSize + CacheTrimThreshold || Interlocked.CompareExchange(ref _inTrim, 1, 0) != 0) + { + return; + } + + try { - try - { - // Example: 2301 - 2000 = 301; 301 / 2301 = 0.1308 * 100 = 13% compacting - _cache.Compact((((double)(currentCacheSize - _cacheSize) / (double)currentCacheSize) * 100)); - } - finally - { - // Reset _inTrim flag - Interlocked.CompareExchange(ref _inTrim, 0, 1); - } + // Example: 2301 - 2000 = 301; 301 / 2301 = 0.1308 * 100 = 13% compacting + _cache.Compact((double)(currentCacheSize - CacheSize) / currentCacheSize * 100); + } + finally + { + Interlocked.Exchange(ref _inTrim, 0); } } - private string GetCacheLookupKey(string masterKeyPath, bool allowEnclaveComputations, byte[] signature, string keyStoreName) + /// + /// Generates a cache key for the given CMK metadata and signature. The key is a + /// concatenation of the key store name, master key path, allowEnclaveComputations value, and signature, separated by a delimiter. + /// + /// The master key path. + /// Whether enclave computations are allowed. + /// The signature. + /// The key store name. + /// A string that can be used as a cache key. + private static string GetCacheLookupKey(string masterKeyPath, bool allowEnclaveComputations, byte[] signature, string keyStoreName) { - StringBuilder cacheLookupKeyBuilder = new StringBuilder(keyStoreName, - capacity: - keyStoreName.Length + - masterKeyPath.Length + - SqlSecurityUtility.GetBase64LengthFromByteLength(signature.Length) + - 3 /*separators*/ + - 10 /*boolean value + somebuffer*/); - - cacheLookupKeyBuilder.Append(_cacheLookupKeySeparator); - cacheLookupKeyBuilder.Append(masterKeyPath); - cacheLookupKeyBuilder.Append(_cacheLookupKeySeparator); - cacheLookupKeyBuilder.Append(allowEnclaveComputations); - cacheLookupKeyBuilder.Append(_cacheLookupKeySeparator); - cacheLookupKeyBuilder.Append(Convert.ToBase64String(signature)); - cacheLookupKeyBuilder.Append(_cacheLookupKeySeparator); - string cacheLookupKey = cacheLookupKeyBuilder.ToString(); - return cacheLookupKey; + int cacheCapacity = + keyStoreName.Length + + masterKeyPath.Length + + SqlSecurityUtility.GetBase64LengthFromByteLength(signature.Length) + + 4 * _cacheLookupKeySeparator.Length + + 10 /* boolean value + buffer */; + + return new StringBuilder(keyStoreName, capacity: cacheCapacity) + .Append(_cacheLookupKeySeparator) + .Append(masterKeyPath) + .Append(_cacheLookupKeySeparator) + .Append(allowEnclaveComputations) + .Append(_cacheLookupKeySeparator) + .Append(Convert.ToBase64String(signature)) + .Append(_cacheLookupKeySeparator) + .ToString(); } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs index 90fb9a5d32..c41bc23d1f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs @@ -374,18 +374,20 @@ internal static void VerifyColumnMasterKeySignature(string keyStoreName, string } else { - bool signatureVerificationResult = ColumnMasterKeyMetadataSignatureVerificationCache.GetSignatureVerificationResult(keyStoreName, keyPath, isEnclaveEnabled, CMKSignature); - if (signatureVerificationResult == false) - { - // We will simply bubble up the exception from VerifyColumnMasterKeyMetadata function. - isValidSignature = provider.VerifyColumnMasterKeyMetadata(keyPath, isEnclaveEnabled, - CMKSignature); + SignatureVerificationResult cachedResult = ColumnMasterKeyMetadataSignatureVerificationCache.Instance + .GetSignatureVerificationResult(keyStoreName, keyPath, isEnclaveEnabled, CMKSignature); - ColumnMasterKeyMetadataSignatureVerificationCache.AddSignatureVerificationResult(keyStoreName, keyPath, isEnclaveEnabled, CMKSignature, isValidSignature); + if (cachedResult == SignatureVerificationResult.NotFound) + { + // Cache miss: verify with the provider and cache the result. + // Exceptions from VerifyColumnMasterKeyMetadata bubble up to the outer catch. + isValidSignature = provider.VerifyColumnMasterKeyMetadata(keyPath, isEnclaveEnabled, CMKSignature); + ColumnMasterKeyMetadataSignatureVerificationCache.Instance + .AddSignatureVerificationResult(keyStoreName, keyPath, isEnclaveEnabled, CMKSignature, isValidSignature); } else { - isValidSignature = signatureVerificationResult; + isValidSignature = cachedResult == SignatureVerificationResult.True; } } } diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SignatureVerificationCacheTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SignatureVerificationCacheTests.cs new file mode 100644 index 0000000000..a03dd8910e --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SignatureVerificationCacheTests.cs @@ -0,0 +1,49 @@ +// 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 Xunit; + +namespace Microsoft.Data.SqlClient.UnitTests +{ + public class SignatureVerificationCacheTests + { + [Fact] + public void GetSignatureVerificationResult_ReturnsFalseForCachedFailure() + { + ColumnMasterKeyMetadataSignatureVerificationCache cache = ColumnMasterKeyMetadataSignatureVerificationCache.Instance; + string keyStoreName = $"TEST_PROVIDER_{Guid.NewGuid():N}"; + string masterKeyPath = $"https://unit-test/{Guid.NewGuid():N}"; + byte[] signature = [1, 2, 3, 4]; + + cache.AddSignatureVerificationResult(keyStoreName, masterKeyPath, allowEnclaveComputations: true, signature, result: false); + + Assert.Equal(SignatureVerificationResult.False, cache.GetSignatureVerificationResult(keyStoreName, masterKeyPath, allowEnclaveComputations: true, signature)); + } + + [Fact] + public void GetSignatureVerificationResult_ReturnsTrueForCachedSuccess() + { + ColumnMasterKeyMetadataSignatureVerificationCache cache = ColumnMasterKeyMetadataSignatureVerificationCache.Instance; + string keyStoreName = $"TEST_PROVIDER_{Guid.NewGuid():N}"; + string masterKeyPath = $"https://unit-test/{Guid.NewGuid():N}"; + byte[] signature = [4, 3, 2, 1]; + + cache.AddSignatureVerificationResult(keyStoreName, masterKeyPath, allowEnclaveComputations: true, signature, result: true); + + Assert.Equal(SignatureVerificationResult.True, cache.GetSignatureVerificationResult(keyStoreName, masterKeyPath, allowEnclaveComputations: true, signature)); + } + + [Fact] + public void GetSignatureVerificationResult_ReturnsNotFoundForCacheMiss() + { + ColumnMasterKeyMetadataSignatureVerificationCache cache = ColumnMasterKeyMetadataSignatureVerificationCache.Instance; + string keyStoreName = $"TEST_PROVIDER_{Guid.NewGuid():N}"; + string masterKeyPath = $"https://unit-test/{Guid.NewGuid():N}"; + byte[] signature = [9, 9, 9, 9]; + + Assert.Equal(SignatureVerificationResult.NotFound, cache.GetSignatureVerificationResult(keyStoreName, masterKeyPath, allowEnclaveComputations: true, signature)); + } + } +} \ No newline at end of file From 83216bb8e810fb6f7cf6d956aace6c44a5afa3bb Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Wed, 10 Jun 2026 23:57:15 -0700 Subject: [PATCH 2/2] Minor fixes to compilation issues --- .../Data/SqlClient/SignatureVerificationCache.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs index 15c951af24..31bd004a3a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs @@ -41,7 +41,8 @@ internal class ColumnMasterKeyMetadataSignatureVerificationCache private const int _cacheTrimThreshold = 300; // Threshold above the cache size when we start trimming. private const string _cacheLookupKeySeparator = ":"; - + private static readonly TimeSpan s_verificationCacheTimeout = TimeSpan.FromDays(10); + /// /// Gets the process-wide singleton instance of the signature verification cache. /// @@ -54,7 +55,8 @@ private ColumnMasterKeyMetadataSignatureVerificationCache() { _cache = new MemoryCache(new MemoryCacheOptions()); } -/// + + /// /// Get signature verification result for given CMK metadata /// (KeystoreName, MasterKeyPath, allowEnclaveComputations) and a given signature /// @@ -146,7 +148,7 @@ private void TrimCacheIfNeeded() { // If the size of the cache exceeds the threshold, set that we are in trimming and trim the cache accordingly. long currentCacheSize = _cache.Count; - if (currentCacheSize <= CacheSize + CacheTrimThreshold || Interlocked.CompareExchange(ref _inTrim, 1, 0) != 0) + if (currentCacheSize <= _cacheSize + _cacheTrimThreshold || Interlocked.CompareExchange(ref _inTrim, 1, 0) != 0) { return; } @@ -154,7 +156,7 @@ private void TrimCacheIfNeeded() try { // Example: 2301 - 2000 = 301; 301 / 2301 = 0.1308 * 100 = 13% compacting - _cache.Compact((double)(currentCacheSize - CacheSize) / currentCacheSize * 100); + _cache.Compact((double)(currentCacheSize - _cacheSize) / currentCacheSize * 100); } finally {