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 8d1ce98df1..dc086d4610 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 /// @@ -16,138 +39,159 @@ 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(); private static readonly TimeSpan s_verificationCacheTimeout = TimeSpan.FromDays(10); - //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. - _cache.Set(cacheLookupKey, result, absoluteExpirationRelativeToNow: s_verificationCacheTimeout); + _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) + { + throw SQL.NullArgumentInternal(argumentName, nameof(ColumnMasterKeyMetadataSignatureVerificationCache), methodName); + } + if (string.IsNullOrWhiteSpace(value)) { - if (stringArgValue == null) - { - throw SQL.NullArgumentInternal(stringArgName, _className, methodName); - } - else - { - throw SQL.EmptyArgumentInternal(stringArgName, _className, methodName); - } + throw SQL.EmptyArgumentInternal(argumentName, nameof(ColumnMasterKeyMetadataSignatureVerificationCache), methodName); } } + 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) { - 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); - } + return; + } + + try + { + // 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 253ab92db8..79ebc60d27 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs @@ -332,18 +332,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