Skip to content

Commit b448b72

Browse files
[Release/6.1] Fix fetching signature verification result from cache (#4356)
* [Release/6.1] Fix fetching signature verification result from cache * Minor fixes to compilation issues
1 parent fe4f248 commit b448b72

3 files changed

Lines changed: 175 additions & 84 deletions

File tree

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SignatureVerificationCache.cs

Lines changed: 116 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,29 @@
99

1010
namespace Microsoft.Data.SqlClient
1111
{
12+
/// <summary>
13+
/// Tri-state result returned by <see cref="ColumnMasterKeyMetadataSignatureVerificationCache.GetSignatureVerificationResult"/>.
14+
/// Distinguishes a cache miss from a cached negative result so callers cannot conflate the two.
15+
/// </summary>
16+
internal enum SignatureVerificationResult
17+
{
18+
/// <summary>
19+
/// No cached entry exists for the requested CMK metadata.
20+
/// The caller must verify the signature with the key store provider.
21+
/// </summary>
22+
NotFound,
23+
24+
/// <summary>
25+
/// A cached entry exists and indicates that signature verification previously failed.
26+
/// </summary>
27+
False,
28+
29+
/// <summary>
30+
/// A cached entry exists and indicates that signature verification previously succeeded.
31+
/// </summary>
32+
True,
33+
}
34+
1235
/// <summary>
1336
/// Cache for storing result of signature verification of CMK Metadata
1437
/// </summary>
@@ -17,140 +40,157 @@ internal class ColumnMasterKeyMetadataSignatureVerificationCache
1740
private const int _cacheSize = 2000; // Cache size in number of entries.
1841
private const int _cacheTrimThreshold = 300; // Threshold above the cache size when we start trimming.
1942

20-
private const string _className = "ColumnMasterKeyMetadataSignatureVerificationCache";
21-
private const string _getSignatureVerificationResultMethodName = "GetSignatureVerificationResult";
22-
private const string _addSignatureVerificationResultMethodName = "AddSignatureVerificationResult";
23-
private const string _masterkeypathArgumentName = "masterKeyPath";
24-
private const string _keyStoreNameArgumentName = "keyStoreName";
25-
private const string _signatureName = "signature";
2643
private const string _cacheLookupKeySeparator = ":";
44+
private static readonly TimeSpan s_verificationCacheTimeout = TimeSpan.FromDays(10);
2745

28-
private static readonly ColumnMasterKeyMetadataSignatureVerificationCache _signatureVerificationCache = new ColumnMasterKeyMetadataSignatureVerificationCache();
29-
30-
//singleton instance
31-
internal static ColumnMasterKeyMetadataSignatureVerificationCache Instance { get { return _signatureVerificationCache; } }
46+
/// <summary>
47+
/// Gets the process-wide singleton instance of the signature verification cache.
48+
/// </summary>
49+
internal static ColumnMasterKeyMetadataSignatureVerificationCache Instance { get; } = new();
3250

3351
private readonly MemoryCache _cache;
34-
private int _inTrim = 0;
52+
private int _inTrim;
3553

3654
private ColumnMasterKeyMetadataSignatureVerificationCache()
3755
{
3856
_cache = new MemoryCache(new MemoryCacheOptions());
39-
_inTrim = 0;
4057
}
4158

4259
/// <summary>
43-
/// Get signature verification result for given CMK metadata (KeystoreName, MasterKeyPath, allowEnclaveComputations) and a given signature
60+
/// Get signature verification result for given CMK metadata
61+
/// (KeystoreName, MasterKeyPath, allowEnclaveComputations) and a given signature
4462
/// </summary>
4563
/// <param name="keyStoreName">Key Store name for CMK</param>
4664
/// <param name="masterKeyPath">Key Path for CMK</param>
4765
/// <param name="allowEnclaveComputations">boolean indicating whether the key can be sent to enclave</param>
4866
/// <param name="signature">Signature for the CMK metadata</param>
49-
internal bool GetSignatureVerificationResult(string keyStoreName, string masterKeyPath, bool allowEnclaveComputations, byte[] signature)
67+
/// <returns>Tri-state result indicating whether signature verification succeeded, failed, or was not found in cache</returns>
68+
/// <exception cref="System.ArgumentNullException">
69+
/// Thrown when <paramref name="masterKeyPath"/>, <paramref name="keyStoreName"/>,
70+
/// or <paramref name="signature"/> is <see langword="null"/>.
71+
/// </exception>
72+
/// <exception cref="System.ArgumentException">
73+
/// Thrown when <paramref name="masterKeyPath"/> or <paramref name="keyStoreName"/>
74+
/// is empty or whitespace, or when <paramref name="signature"/> has length zero.
75+
/// </exception>
76+
internal SignatureVerificationResult GetSignatureVerificationResult(string keyStoreName, string masterKeyPath, bool allowEnclaveComputations, byte[] signature)
5077
{
51-
ValidateStringArgumentNotNullOrEmpty(masterKeyPath, _masterkeypathArgumentName, _getSignatureVerificationResultMethodName);
52-
ValidateStringArgumentNotNullOrEmpty(keyStoreName, _keyStoreNameArgumentName, _getSignatureVerificationResultMethodName);
53-
ValidateSignatureNotNullOrEmpty(signature, _getSignatureVerificationResultMethodName);
78+
ValidateStringArgumentNotNullOrEmpty(masterKeyPath, nameof(masterKeyPath), nameof(GetSignatureVerificationResult));
79+
ValidateStringArgumentNotNullOrEmpty(keyStoreName, nameof(keyStoreName), nameof(GetSignatureVerificationResult));
80+
ValidateSignatureNotNullOrEmpty(signature, nameof(GetSignatureVerificationResult));
5481

5582
string cacheLookupKey = GetCacheLookupKey(masterKeyPath, allowEnclaveComputations, signature, keyStoreName);
5683

57-
return _cache.TryGetValue<bool>(cacheLookupKey, out bool value);
84+
if (!_cache.TryGetValue(cacheLookupKey, out bool value))
85+
{
86+
return SignatureVerificationResult.NotFound;
87+
}
88+
89+
return value ? SignatureVerificationResult.True : SignatureVerificationResult.False;
5890
}
5991

6092
/// <summary>
61-
/// Add signature verification result for given CMK metadata (KeystoreName, MasterKeyPath, allowEnclaveComputations) and a given signature in the cache
93+
/// Add signature verification result for given CMK metadata (KeystoreName,
94+
/// MasterKeyPath, allowEnclaveComputations) and a given signature in the cache
6295
/// </summary>
6396
/// <param name="keyStoreName">Key Store name for CMK</param>
6497
/// <param name="masterKeyPath">Key Path for CMK</param>
6598
/// <param name="allowEnclaveComputations">boolean indicating whether the key can be sent to enclave</param>
6699
/// <param name="signature">Signature for the CMK metadata</param>
67100
/// <param name="result">result indicating signature verification success/failure</param>
101+
/// <exception cref="System.ArgumentNullException">
102+
/// Thrown when <paramref name="masterKeyPath"/>, <paramref name="keyStoreName"/>,
103+
/// or <paramref name="signature"/> is <see langword="null"/>.
104+
/// </exception>
105+
/// <exception cref="System.ArgumentException">
106+
/// Thrown when <paramref name="masterKeyPath"/> or <paramref name="keyStoreName"/> is empty or whitespace,
107+
/// or when <paramref name="signature"/> has length zero.
108+
/// </exception>
68109
internal void AddSignatureVerificationResult(string keyStoreName, string masterKeyPath, bool allowEnclaveComputations, byte[] signature, bool result)
69110
{
70-
ValidateStringArgumentNotNullOrEmpty(masterKeyPath, _masterkeypathArgumentName, _addSignatureVerificationResultMethodName);
71-
ValidateStringArgumentNotNullOrEmpty(keyStoreName, _keyStoreNameArgumentName, _addSignatureVerificationResultMethodName);
72-
ValidateSignatureNotNullOrEmpty(signature, _addSignatureVerificationResultMethodName);
111+
ValidateStringArgumentNotNullOrEmpty(masterKeyPath, nameof(masterKeyPath), nameof(AddSignatureVerificationResult));
112+
ValidateStringArgumentNotNullOrEmpty(keyStoreName, nameof(keyStoreName), nameof(AddSignatureVerificationResult));
113+
ValidateSignatureNotNullOrEmpty(signature, nameof(AddSignatureVerificationResult));
73114

74115
string cacheLookupKey = GetCacheLookupKey(masterKeyPath, allowEnclaveComputations, signature, keyStoreName);
75116

76117
TrimCacheIfNeeded();
77118

78119
// By default evict after 10 days.
79-
MemoryCacheEntryOptions options = new MemoryCacheEntryOptions
80-
{
81-
AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(10)
82-
};
83-
_cache.Set<bool>(cacheLookupKey, result, options);
120+
_cache.Set(cacheLookupKey, result, absoluteExpirationRelativeToNow: s_verificationCacheTimeout);
84121
}
85122

86-
private void ValidateSignatureNotNullOrEmpty(byte[] signature, string methodName)
123+
private static void ValidateSignatureNotNullOrEmpty(byte[] signature, string methodName)
87124
{
88-
if (signature == null || signature.Length == 0)
125+
if (signature is null)
89126
{
90-
if (signature == null)
91-
{
92-
throw SQL.NullArgumentInternal(_signatureName, _className, methodName);
93-
}
94-
else
95-
{
96-
throw SQL.EmptyArgumentInternal(_signatureName, _className, methodName);
97-
}
127+
throw SQL.NullArgumentInternal(nameof(signature), nameof(ColumnMasterKeyMetadataSignatureVerificationCache), methodName);
128+
}
129+
if (signature.Length == 0)
130+
{
131+
throw SQL.EmptyArgumentInternal(nameof(signature), nameof(ColumnMasterKeyMetadataSignatureVerificationCache), methodName);
98132
}
99133
}
100134

101-
private void ValidateStringArgumentNotNullOrEmpty(string stringArgValue, string stringArgName, string methodName)
135+
private static void ValidateStringArgumentNotNullOrEmpty(string value, string argumentName, string methodName)
102136
{
103-
if (string.IsNullOrWhiteSpace(stringArgValue))
137+
if (value is null)
138+
{
139+
throw SQL.NullArgumentInternal(argumentName, nameof(ColumnMasterKeyMetadataSignatureVerificationCache), methodName);
140+
}
141+
if (string.IsNullOrWhiteSpace(value))
104142
{
105-
if (stringArgValue == null)
106-
{
107-
throw SQL.NullArgumentInternal(stringArgName, _className, methodName);
108-
}
109-
else
110-
{
111-
throw SQL.EmptyArgumentInternal(stringArgName, _className, methodName);
112-
}
143+
throw SQL.EmptyArgumentInternal(argumentName, nameof(ColumnMasterKeyMetadataSignatureVerificationCache), methodName);
113144
}
114145
}
115146

116147
private void TrimCacheIfNeeded()
117148
{
118149
// If the size of the cache exceeds the threshold, set that we are in trimming and trim the cache accordingly.
119150
long currentCacheSize = _cache.Count;
120-
if ((currentCacheSize > _cacheSize + _cacheTrimThreshold) && (0 == Interlocked.CompareExchange(ref _inTrim, 1, 0)))
151+
if (currentCacheSize <= _cacheSize + _cacheTrimThreshold || Interlocked.CompareExchange(ref _inTrim, 1, 0) != 0)
121152
{
122-
try
123-
{
124-
// Example: 2301 - 2000 = 301; 301 / 2301 = 0.1308 * 100 = 13% compacting
125-
_cache.Compact((((double)(currentCacheSize - _cacheSize) / (double)currentCacheSize) * 100));
126-
}
127-
finally
128-
{
129-
// Reset _inTrim flag
130-
Interlocked.CompareExchange(ref _inTrim, 0, 1);
131-
}
153+
return;
154+
}
155+
156+
try
157+
{
158+
// Example: 2301 - 2000 = 301; 301 / 2301 = 0.1308 * 100 = 13% compacting
159+
_cache.Compact((double)(currentCacheSize - _cacheSize) / currentCacheSize * 100);
160+
}
161+
finally
162+
{
163+
Interlocked.Exchange(ref _inTrim, 0);
132164
}
133165
}
134166

135-
private string GetCacheLookupKey(string masterKeyPath, bool allowEnclaveComputations, byte[] signature, string keyStoreName)
167+
/// <summary>
168+
/// Generates a cache key for the given CMK metadata and signature. The key is a
169+
/// concatenation of the key store name, master key path, allowEnclaveComputations value, and signature, separated by a delimiter.
170+
/// </summary>
171+
/// <param name="masterKeyPath">The master key path.</param>
172+
/// <param name="allowEnclaveComputations">Whether enclave computations are allowed.</param>
173+
/// <param name="signature">The signature.</param>
174+
/// <param name="keyStoreName">The key store name.</param>
175+
/// <returns>A string that can be used as a cache key.</returns>
176+
private static string GetCacheLookupKey(string masterKeyPath, bool allowEnclaveComputations, byte[] signature, string keyStoreName)
136177
{
137-
StringBuilder cacheLookupKeyBuilder = new StringBuilder(keyStoreName,
138-
capacity:
139-
keyStoreName.Length +
140-
masterKeyPath.Length +
141-
SqlSecurityUtility.GetBase64LengthFromByteLength(signature.Length) +
142-
3 /*separators*/ +
143-
10 /*boolean value + somebuffer*/);
144-
145-
cacheLookupKeyBuilder.Append(_cacheLookupKeySeparator);
146-
cacheLookupKeyBuilder.Append(masterKeyPath);
147-
cacheLookupKeyBuilder.Append(_cacheLookupKeySeparator);
148-
cacheLookupKeyBuilder.Append(allowEnclaveComputations);
149-
cacheLookupKeyBuilder.Append(_cacheLookupKeySeparator);
150-
cacheLookupKeyBuilder.Append(Convert.ToBase64String(signature));
151-
cacheLookupKeyBuilder.Append(_cacheLookupKeySeparator);
152-
string cacheLookupKey = cacheLookupKeyBuilder.ToString();
153-
return cacheLookupKey;
178+
int cacheCapacity =
179+
keyStoreName.Length +
180+
masterKeyPath.Length +
181+
SqlSecurityUtility.GetBase64LengthFromByteLength(signature.Length) +
182+
4 * _cacheLookupKeySeparator.Length +
183+
10 /* boolean value + buffer */;
184+
185+
return new StringBuilder(keyStoreName, capacity: cacheCapacity)
186+
.Append(_cacheLookupKeySeparator)
187+
.Append(masterKeyPath)
188+
.Append(_cacheLookupKeySeparator)
189+
.Append(allowEnclaveComputations)
190+
.Append(_cacheLookupKeySeparator)
191+
.Append(Convert.ToBase64String(signature))
192+
.Append(_cacheLookupKeySeparator)
193+
.ToString();
154194
}
155195
}
156196
}

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -374,18 +374,20 @@ internal static void VerifyColumnMasterKeySignature(string keyStoreName, string
374374
}
375375
else
376376
{
377-
bool signatureVerificationResult = ColumnMasterKeyMetadataSignatureVerificationCache.GetSignatureVerificationResult(keyStoreName, keyPath, isEnclaveEnabled, CMKSignature);
378-
if (signatureVerificationResult == false)
379-
{
380-
// We will simply bubble up the exception from VerifyColumnMasterKeyMetadata function.
381-
isValidSignature = provider.VerifyColumnMasterKeyMetadata(keyPath, isEnclaveEnabled,
382-
CMKSignature);
377+
SignatureVerificationResult cachedResult = ColumnMasterKeyMetadataSignatureVerificationCache.Instance
378+
.GetSignatureVerificationResult(keyStoreName, keyPath, isEnclaveEnabled, CMKSignature);
383379

384-
ColumnMasterKeyMetadataSignatureVerificationCache.AddSignatureVerificationResult(keyStoreName, keyPath, isEnclaveEnabled, CMKSignature, isValidSignature);
380+
if (cachedResult == SignatureVerificationResult.NotFound)
381+
{
382+
// Cache miss: verify with the provider and cache the result.
383+
// Exceptions from VerifyColumnMasterKeyMetadata bubble up to the outer catch.
384+
isValidSignature = provider.VerifyColumnMasterKeyMetadata(keyPath, isEnclaveEnabled, CMKSignature);
385+
ColumnMasterKeyMetadataSignatureVerificationCache.Instance
386+
.AddSignatureVerificationResult(keyStoreName, keyPath, isEnclaveEnabled, CMKSignature, isValidSignature);
385387
}
386388
else
387389
{
388-
isValidSignature = signatureVerificationResult;
390+
isValidSignature = cachedResult == SignatureVerificationResult.True;
389391
}
390392
}
391393
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using Xunit;
7+
8+
namespace Microsoft.Data.SqlClient.UnitTests
9+
{
10+
public class SignatureVerificationCacheTests
11+
{
12+
[Fact]
13+
public void GetSignatureVerificationResult_ReturnsFalseForCachedFailure()
14+
{
15+
ColumnMasterKeyMetadataSignatureVerificationCache cache = ColumnMasterKeyMetadataSignatureVerificationCache.Instance;
16+
string keyStoreName = $"TEST_PROVIDER_{Guid.NewGuid():N}";
17+
string masterKeyPath = $"https://unit-test/{Guid.NewGuid():N}";
18+
byte[] signature = [1, 2, 3, 4];
19+
20+
cache.AddSignatureVerificationResult(keyStoreName, masterKeyPath, allowEnclaveComputations: true, signature, result: false);
21+
22+
Assert.Equal(SignatureVerificationResult.False, cache.GetSignatureVerificationResult(keyStoreName, masterKeyPath, allowEnclaveComputations: true, signature));
23+
}
24+
25+
[Fact]
26+
public void GetSignatureVerificationResult_ReturnsTrueForCachedSuccess()
27+
{
28+
ColumnMasterKeyMetadataSignatureVerificationCache cache = ColumnMasterKeyMetadataSignatureVerificationCache.Instance;
29+
string keyStoreName = $"TEST_PROVIDER_{Guid.NewGuid():N}";
30+
string masterKeyPath = $"https://unit-test/{Guid.NewGuid():N}";
31+
byte[] signature = [4, 3, 2, 1];
32+
33+
cache.AddSignatureVerificationResult(keyStoreName, masterKeyPath, allowEnclaveComputations: true, signature, result: true);
34+
35+
Assert.Equal(SignatureVerificationResult.True, cache.GetSignatureVerificationResult(keyStoreName, masterKeyPath, allowEnclaveComputations: true, signature));
36+
}
37+
38+
[Fact]
39+
public void GetSignatureVerificationResult_ReturnsNotFoundForCacheMiss()
40+
{
41+
ColumnMasterKeyMetadataSignatureVerificationCache cache = ColumnMasterKeyMetadataSignatureVerificationCache.Instance;
42+
string keyStoreName = $"TEST_PROVIDER_{Guid.NewGuid():N}";
43+
string masterKeyPath = $"https://unit-test/{Guid.NewGuid():N}";
44+
byte[] signature = [9, 9, 9, 9];
45+
46+
Assert.Equal(SignatureVerificationResult.NotFound, cache.GetSignatureVerificationResult(keyStoreName, masterKeyPath, allowEnclaveComputations: true, signature));
47+
}
48+
}
49+
}

0 commit comments

Comments
 (0)