Skip to content

Commit 3488b36

Browse files
Fix fetching signature verification result from cache (dotnet#4339)
* Fix fetching signature verification result from cache * Introduce Tri-State * Remove stale content, clean ups
1 parent 4afd45e commit 3488b36

3 files changed

Lines changed: 175 additions & 80 deletions

File tree

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

Lines changed: 116 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -9,145 +9,189 @@
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>
1538
internal class ColumnMasterKeyMetadataSignatureVerificationCache
1639
{
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.
19-
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";
2642
private const string _cacheLookupKeySeparator = ":";
2743

28-
private static readonly ColumnMasterKeyMetadataSignatureVerificationCache _signatureVerificationCache = new ColumnMasterKeyMetadataSignatureVerificationCache();
2944
private static readonly TimeSpan s_verificationCacheTimeout = TimeSpan.FromDays(10);
3045

31-
//singleton instance
32-
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();
3350

3451
private readonly MemoryCache _cache;
35-
private int _inTrim = 0;
52+
private int _inTrim;
3653

3754
private ColumnMasterKeyMetadataSignatureVerificationCache()
3855
{
3956
_cache = new MemoryCache(new MemoryCacheOptions());
40-
_inTrim = 0;
4157
}
4258

4359
/// <summary>
44-
/// 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
4562
/// </summary>
4663
/// <param name="keyStoreName">Key Store name for CMK</param>
4764
/// <param name="masterKeyPath">Key Path for CMK</param>
4865
/// <param name="allowEnclaveComputations">boolean indicating whether the key can be sent to enclave</param>
4966
/// <param name="signature">Signature for the CMK metadata</param>
50-
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)
5177
{
52-
ValidateStringArgumentNotNullOrEmpty(masterKeyPath, _masterkeypathArgumentName, _getSignatureVerificationResultMethodName);
53-
ValidateStringArgumentNotNullOrEmpty(keyStoreName, _keyStoreNameArgumentName, _getSignatureVerificationResultMethodName);
54-
ValidateSignatureNotNullOrEmpty(signature, _getSignatureVerificationResultMethodName);
78+
ValidateStringArgumentNotNullOrEmpty(masterKeyPath, nameof(masterKeyPath), nameof(GetSignatureVerificationResult));
79+
ValidateStringArgumentNotNullOrEmpty(keyStoreName, nameof(keyStoreName), nameof(GetSignatureVerificationResult));
80+
ValidateSignatureNotNullOrEmpty(signature, nameof(GetSignatureVerificationResult));
5581

5682
string cacheLookupKey = GetCacheLookupKey(masterKeyPath, allowEnclaveComputations, signature, keyStoreName);
5783

58-
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;
5990
}
6091

6192
/// <summary>
62-
/// 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
6395
/// </summary>
6496
/// <param name="keyStoreName">Key Store name for CMK</param>
6597
/// <param name="masterKeyPath">Key Path for CMK</param>
6698
/// <param name="allowEnclaveComputations">boolean indicating whether the key can be sent to enclave</param>
6799
/// <param name="signature">Signature for the CMK metadata</param>
68100
/// <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>
69109
internal void AddSignatureVerificationResult(string keyStoreName, string masterKeyPath, bool allowEnclaveComputations, byte[] signature, bool result)
70110
{
71-
ValidateStringArgumentNotNullOrEmpty(masterKeyPath, _masterkeypathArgumentName, _addSignatureVerificationResultMethodName);
72-
ValidateStringArgumentNotNullOrEmpty(keyStoreName, _keyStoreNameArgumentName, _addSignatureVerificationResultMethodName);
73-
ValidateSignatureNotNullOrEmpty(signature, _addSignatureVerificationResultMethodName);
111+
ValidateStringArgumentNotNullOrEmpty(masterKeyPath, nameof(masterKeyPath), nameof(AddSignatureVerificationResult));
112+
ValidateStringArgumentNotNullOrEmpty(keyStoreName, nameof(keyStoreName), nameof(AddSignatureVerificationResult));
113+
ValidateSignatureNotNullOrEmpty(signature, nameof(AddSignatureVerificationResult));
74114

75115
string cacheLookupKey = GetCacheLookupKey(masterKeyPath, allowEnclaveComputations, signature, keyStoreName);
76116

77117
TrimCacheIfNeeded();
78118

79119
// By default evict after 10 days.
80-
_cache.Set<bool>(cacheLookupKey, result, absoluteExpirationRelativeToNow: s_verificationCacheTimeout);
120+
_cache.Set(cacheLookupKey, result, absoluteExpirationRelativeToNow: s_verificationCacheTimeout);
81121
}
82122

83-
private void ValidateSignatureNotNullOrEmpty(byte[] signature, string methodName)
123+
private static void ValidateSignatureNotNullOrEmpty(byte[] signature, string methodName)
84124
{
85-
if (signature == null || signature.Length == 0)
125+
if (signature is null)
126+
{
127+
throw SQL.NullArgumentInternal(nameof(signature), nameof(ColumnMasterKeyMetadataSignatureVerificationCache), methodName);
128+
}
129+
if (signature.Length == 0)
86130
{
87-
if (signature == null)
88-
{
89-
throw SQL.NullArgumentInternal(_signatureName, _className, methodName);
90-
}
91-
else
92-
{
93-
throw SQL.EmptyArgumentInternal(_signatureName, _className, methodName);
94-
}
131+
throw SQL.EmptyArgumentInternal(nameof(signature), nameof(ColumnMasterKeyMetadataSignatureVerificationCache), methodName);
95132
}
96133
}
97134

98-
private void ValidateStringArgumentNotNullOrEmpty(string stringArgValue, string stringArgName, string methodName)
135+
private static void ValidateStringArgumentNotNullOrEmpty(string value, string argumentName, string methodName)
99136
{
100-
if (string.IsNullOrWhiteSpace(stringArgValue))
137+
if (value is null)
138+
{
139+
throw SQL.NullArgumentInternal(argumentName, nameof(ColumnMasterKeyMetadataSignatureVerificationCache), methodName);
140+
}
141+
if (string.IsNullOrWhiteSpace(value))
101142
{
102-
if (stringArgValue == null)
103-
{
104-
throw SQL.NullArgumentInternal(stringArgName, _className, methodName);
105-
}
106-
else
107-
{
108-
throw SQL.EmptyArgumentInternal(stringArgName, _className, methodName);
109-
}
143+
throw SQL.EmptyArgumentInternal(argumentName, nameof(ColumnMasterKeyMetadataSignatureVerificationCache), methodName);
110144
}
111145
}
112146

147+
113148
private void TrimCacheIfNeeded()
114149
{
115150
// If the size of the cache exceeds the threshold, set that we are in trimming and trim the cache accordingly.
116151
long currentCacheSize = _cache.Count;
117-
if ((currentCacheSize > CacheSize + CacheTrimThreshold) && (0 == Interlocked.CompareExchange(ref _inTrim, 1, 0)))
152+
if (currentCacheSize <= CacheSize + CacheTrimThreshold || Interlocked.CompareExchange(ref _inTrim, 1, 0) != 0)
118153
{
119-
try
120-
{
121-
// Example: 2301 - 2000 = 301; 301 / 2301 = 0.1308 * 100 = 13% compacting
122-
_cache.Compact((((double)(currentCacheSize - CacheSize) / (double)currentCacheSize) * 100));
123-
}
124-
finally
125-
{
126-
// Reset _inTrim flag
127-
Interlocked.CompareExchange(ref _inTrim, 0, 1);
128-
}
154+
return;
155+
}
156+
157+
try
158+
{
159+
// Example: 2301 - 2000 = 301; 301 / 2301 = 0.1308 * 100 = 13% compacting
160+
_cache.Compact((double)(currentCacheSize - CacheSize) / currentCacheSize * 100);
161+
}
162+
finally
163+
{
164+
Interlocked.Exchange(ref _inTrim, 0);
129165
}
130166
}
131167

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

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -332,18 +332,20 @@ internal static void VerifyColumnMasterKeySignature(string keyStoreName, string
332332
}
333333
else
334334
{
335-
bool signatureVerificationResult = ColumnMasterKeyMetadataSignatureVerificationCache.GetSignatureVerificationResult(keyStoreName, keyPath, isEnclaveEnabled, CMKSignature);
336-
if (signatureVerificationResult == false)
337-
{
338-
// We will simply bubble up the exception from VerifyColumnMasterKeyMetadata function.
339-
isValidSignature = provider.VerifyColumnMasterKeyMetadata(keyPath, isEnclaveEnabled,
340-
CMKSignature);
335+
SignatureVerificationResult cachedResult = ColumnMasterKeyMetadataSignatureVerificationCache.Instance
336+
.GetSignatureVerificationResult(keyStoreName, keyPath, isEnclaveEnabled, CMKSignature);
341337

342-
ColumnMasterKeyMetadataSignatureVerificationCache.AddSignatureVerificationResult(keyStoreName, keyPath, isEnclaveEnabled, CMKSignature, isValidSignature);
338+
if (cachedResult == SignatureVerificationResult.NotFound)
339+
{
340+
// Cache miss: verify with the provider and cache the result.
341+
// Exceptions from VerifyColumnMasterKeyMetadata bubble up to the outer catch.
342+
isValidSignature = provider.VerifyColumnMasterKeyMetadata(keyPath, isEnclaveEnabled, CMKSignature);
343+
ColumnMasterKeyMetadataSignatureVerificationCache.Instance
344+
.AddSignatureVerificationResult(keyStoreName, keyPath, isEnclaveEnabled, CMKSignature, isValidSignature);
343345
}
344346
else
345347
{
346-
isValidSignature = signatureVerificationResult;
348+
isValidSignature = cachedResult == SignatureVerificationResult.True;
347349
}
348350
}
349351
}
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)