|
9 | 9 |
|
10 | 10 | namespace Microsoft.Data.SqlClient |
11 | 11 | { |
| 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 | + |
12 | 35 | /// <summary> |
13 | 36 | /// Cache for storing result of signature verification of CMK Metadata |
14 | 37 | /// </summary> |
15 | 38 | internal class ColumnMasterKeyMetadataSignatureVerificationCache |
16 | 39 | { |
17 | 40 | private const int CacheSize = 2000; // Cache size in number of entries. |
18 | 41 | 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"; |
26 | 42 | private const string _cacheLookupKeySeparator = ":"; |
27 | 43 |
|
28 | | - private static readonly ColumnMasterKeyMetadataSignatureVerificationCache _signatureVerificationCache = new ColumnMasterKeyMetadataSignatureVerificationCache(); |
29 | 44 | private static readonly TimeSpan s_verificationCacheTimeout = TimeSpan.FromDays(10); |
30 | 45 |
|
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(); |
33 | 50 |
|
34 | 51 | private readonly MemoryCache _cache; |
35 | | - private int _inTrim = 0; |
| 52 | + private int _inTrim; |
36 | 53 |
|
37 | 54 | private ColumnMasterKeyMetadataSignatureVerificationCache() |
38 | 55 | { |
39 | 56 | _cache = new MemoryCache(new MemoryCacheOptions()); |
40 | | - _inTrim = 0; |
41 | 57 | } |
42 | 58 |
|
43 | 59 | /// <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 |
45 | 62 | /// </summary> |
46 | 63 | /// <param name="keyStoreName">Key Store name for CMK</param> |
47 | 64 | /// <param name="masterKeyPath">Key Path for CMK</param> |
48 | 65 | /// <param name="allowEnclaveComputations">boolean indicating whether the key can be sent to enclave</param> |
49 | 66 | /// <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) |
51 | 77 | { |
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)); |
55 | 81 |
|
56 | 82 | string cacheLookupKey = GetCacheLookupKey(masterKeyPath, allowEnclaveComputations, signature, keyStoreName); |
57 | 83 |
|
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; |
59 | 90 | } |
60 | 91 |
|
61 | 92 | /// <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 |
63 | 95 | /// </summary> |
64 | 96 | /// <param name="keyStoreName">Key Store name for CMK</param> |
65 | 97 | /// <param name="masterKeyPath">Key Path for CMK</param> |
66 | 98 | /// <param name="allowEnclaveComputations">boolean indicating whether the key can be sent to enclave</param> |
67 | 99 | /// <param name="signature">Signature for the CMK metadata</param> |
68 | 100 | /// <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> |
69 | 109 | internal void AddSignatureVerificationResult(string keyStoreName, string masterKeyPath, bool allowEnclaveComputations, byte[] signature, bool result) |
70 | 110 | { |
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)); |
74 | 114 |
|
75 | 115 | string cacheLookupKey = GetCacheLookupKey(masterKeyPath, allowEnclaveComputations, signature, keyStoreName); |
76 | 116 |
|
77 | 117 | TrimCacheIfNeeded(); |
78 | 118 |
|
79 | 119 | // By default evict after 10 days. |
80 | | - _cache.Set<bool>(cacheLookupKey, result, absoluteExpirationRelativeToNow: s_verificationCacheTimeout); |
| 120 | + _cache.Set(cacheLookupKey, result, absoluteExpirationRelativeToNow: s_verificationCacheTimeout); |
81 | 121 | } |
82 | 122 |
|
83 | | - private void ValidateSignatureNotNullOrEmpty(byte[] signature, string methodName) |
| 123 | + private static void ValidateSignatureNotNullOrEmpty(byte[] signature, string methodName) |
84 | 124 | { |
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) |
86 | 130 | { |
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); |
95 | 132 | } |
96 | 133 | } |
97 | 134 |
|
98 | | - private void ValidateStringArgumentNotNullOrEmpty(string stringArgValue, string stringArgName, string methodName) |
| 135 | + private static void ValidateStringArgumentNotNullOrEmpty(string value, string argumentName, string methodName) |
99 | 136 | { |
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)) |
101 | 142 | { |
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); |
110 | 144 | } |
111 | 145 | } |
112 | 146 |
|
| 147 | + |
113 | 148 | private void TrimCacheIfNeeded() |
114 | 149 | { |
115 | 150 | // If the size of the cache exceeds the threshold, set that we are in trimming and trim the cache accordingly. |
116 | 151 | 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) |
118 | 153 | { |
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); |
129 | 165 | } |
130 | 166 | } |
131 | 167 |
|
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) |
133 | 178 | { |
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(); |
151 | 195 | } |
152 | 196 | } |
153 | 197 | } |
0 commit comments