-
Notifications
You must be signed in to change notification settings - Fork 329
Expand file tree
/
Copy pathAKVTests.cs
More file actions
208 lines (178 loc) · 12.1 KB
/
AKVTests.cs
File metadata and controls
208 lines (178 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
// 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 System.Collections.Generic;
using System.Linq;
using Azure.Identity;
using Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider;
using Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted.Setup;
using Xunit;
namespace Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted
{
[Trait("Set", "AE")]
public class AKVTest : IClassFixture<SQLSetupStrategyAzureKeyVault>
{
private readonly SQLSetupStrategyAzureKeyVault _fixture;
private readonly string _akvTableName;
public AKVTest(SQLSetupStrategyAzureKeyVault fixture)
{
_fixture = fixture;
_akvTableName = fixture.AKVTestTable.Name;
// Disable the cache to avoid false failures.
SqlConnection.ColumnEncryptionQueryMetadataCacheEnabled = false;
}
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringSetupForAE), nameof(DataTestUtility.IsAKVSetupAvailable))]
public void TestEncryptDecryptWithAKV()
{
SqlConnectionStringBuilder builder = new(DataTestUtility.TCPConnectionStringHGSVBS)
{
ColumnEncryptionSetting = SqlConnectionColumnEncryptionSetting.Enabled,
AttestationProtocol = SqlConnectionAttestationProtocol.NotSpecified,
EnclaveAttestationUrl = ""
};
using SqlConnection sqlConnection = new(builder.ConnectionString);
sqlConnection.Open();
Customer customer = new(45, "Microsoft", "Corporation");
// Start a transaction and either commit or rollback based on the test variation.
using (SqlTransaction sqlTransaction = sqlConnection.BeginTransaction())
{
DatabaseHelper.InsertCustomerData(sqlConnection, sqlTransaction, _akvTableName, customer);
sqlTransaction.Commit();
}
// Test INPUT parameter on an encrypted parameter
using SqlCommand sqlCommand = new($"SELECT CustomerId, FirstName, LastName FROM [{_akvTableName}] WHERE FirstName = @firstName",
sqlConnection);
SqlParameter customerFirstParam = sqlCommand.Parameters.AddWithValue(@"firstName", @"Microsoft");
customerFirstParam.Direction = System.Data.ParameterDirection.Input;
customerFirstParam.ForceColumnEncryption = true;
using SqlDataReader sqlDataReader = sqlCommand.ExecuteReader();
DatabaseHelper.ValidateResultSet(sqlDataReader);
}
/*
This unit test is going to assess an issue where a failed decryption leaves a connection in a bad state
when it is returned to the connection pool. If a subsequent connection is retried it will result in an "Internal connection fatal error",
which causes that connection to be doomed, preventing it from being returned to the pool.
Consequently, retrying a third connection will encounter the same decryption error, leading to a repetitive failure cycle.
The purpose of this unit test is to simulate a decryption error and verify that the connection remains usable when returned to the pool.
It aims to confirm that three consecutive connections will consistently fail with the "Failed to decrypt column" error.
*/
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringSetupForAE), nameof(DataTestUtility.IsAKVSetupAvailable))]
public void ForcedColumnDecryptErrorTestShouldFail()
{
SqlConnectionStringBuilder builder = new(DataTestUtility.TCPConnectionStringHGSVBS)
{
ColumnEncryptionSetting = SqlConnectionColumnEncryptionSetting.Enabled,
AttestationProtocol = SqlConnectionAttestationProtocol.NotSpecified,
EnclaveAttestationUrl = ""
};
// Setup record to query
using (SqlConnection sqlConnection = new(builder.ConnectionString))
{
sqlConnection.Open();
Customer customer = new(88, "Microsoft2", "Corporation2");
using (SqlTransaction sqlTransaction = sqlConnection.BeginTransaction())
{
DatabaseHelper.InsertCustomerData(sqlConnection, sqlTransaction, _akvTableName, customer);
sqlTransaction.Commit();
}
}
// Setup Empty key store provider
Dictionary<String, SqlColumnEncryptionKeyStoreProvider> emptyKeyStoreProviders = new()
{
{ "AZURE_KEY_VAULT", new EmptyKeyStoreProvider() }
};
// Three consecutive connections should fail with "Failed to decrypt column" error. This proves that an error in decryption
// does not leave the connection in a bad state.
// In each try, when a "Failed to decrypt error" is thrown, the connection's TDS Parser state object buffer is drained of any
// pending data so it does not interfere with future operations. In addition, the TDS parser state object's reader.DataReady flag
// is set to false so that the calling function that catches the exception will not continue to use the reader. Otherwise, it will
// timeout waiting to read data that doesn't exist. Also, the TDS Parser state object HasPendingData flag is also set to false
// to indicate that the buffer has been cleared and to avoid it getting cleared again in SqlDataReader.TryCloseInternal function.
// Finally, after successfully handling the decryption error, the connection is then returned back to the connection pool without
// an error. A proof that the connection's state object is clean is in the second connection being able to throw the same error.
// The third connection is for making sure we test 3 times as the minimum number of connections to reproduce the issue previously.
for (int i = 0; i < 3; i++)
{
using (SqlConnection sqlConnection = new SqlConnection(builder.ConnectionString))
{
sqlConnection.Open();
// Setup connection using the empty key store provider thereby forcing a decryption error.
sqlConnection.RegisterColumnEncryptionKeyStoreProvidersOnConnection(emptyKeyStoreProviders);
using SqlCommand sqlCommand = new($"SELECT FirstName FROM [{_akvTableName}] WHERE FirstName = @firstName", sqlConnection);
SqlParameter customerFirstParam = sqlCommand.Parameters.AddWithValue(@"firstName", @"Microsoft2");
customerFirstParam.Direction = System.Data.ParameterDirection.Input;
customerFirstParam.ForceColumnEncryption = true;
using SqlDataReader sqlDataReader = sqlCommand.ExecuteReader();
while (sqlDataReader.Read())
{
var error = Assert.Throws<SqlException>(() => DatabaseHelper.CompareResults(sqlDataReader, new string[] { @"string" }, 1));
Assert.Contains("Failed to decrypt column", error.Message);
}
}
}
}
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAKVSetupAvailable))]
public void TestRoundTripWithAKVAndCertStoreProvider()
{
SqlColumnEncryptionCertificateStoreProvider certStoreProvider = new SqlColumnEncryptionCertificateStoreProvider();
byte[] plainTextColumnEncryptionKey = ColumnEncryptionKey.GenerateRandomBytes(ColumnEncryptionKey.KeySizeInBytes);
byte[] encryptedColumnEncryptionKeyUsingAKV = _fixture.AkvStoreProvider.EncryptColumnEncryptionKey(_fixture.AkvKeyUrl, @"RSA_OAEP", plainTextColumnEncryptionKey);
byte[] columnEncryptionKeyReturnedAKV2Cert = certStoreProvider.DecryptColumnEncryptionKey(_fixture.ColumnMasterKeyPath, @"RSA_OAEP", encryptedColumnEncryptionKeyUsingAKV);
Assert.True(plainTextColumnEncryptionKey.SequenceEqual(columnEncryptionKeyReturnedAKV2Cert), @"Roundtrip failed");
// Try the opposite.
byte[] encryptedColumnEncryptionKeyUsingCert = certStoreProvider.EncryptColumnEncryptionKey(_fixture.ColumnMasterKeyPath, @"RSA_OAEP", plainTextColumnEncryptionKey);
byte[] columnEncryptionKeyReturnedCert2AKV = _fixture.AkvStoreProvider.DecryptColumnEncryptionKey(_fixture.AkvKeyUrl, @"RSA_OAEP", encryptedColumnEncryptionKeyUsingCert);
Assert.True(plainTextColumnEncryptionKey.SequenceEqual(columnEncryptionKeyReturnedCert2AKV), @"Roundtrip failed");
}
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringSetupForAE), nameof(DataTestUtility.IsAKVSetupAvailable))]
public void TestLocalCekCacheIsScopedToProvider()
{
SqlConnectionStringBuilder builder = new(DataTestUtility.TCPConnectionStringHGSVBS)
{
ColumnEncryptionSetting = SqlConnectionColumnEncryptionSetting.Enabled,
AttestationProtocol = SqlConnectionAttestationProtocol.NotSpecified,
EnclaveAttestationUrl = ""
};
using SqlConnection sqlConnection = new(builder.ConnectionString);
sqlConnection.Open();
// Test INPUT parameter on an encrypted parameter
using SqlCommand sqlCommand = new($"SELECT CustomerId, FirstName, LastName FROM [{_akvTableName}] WHERE FirstName = @firstName",
sqlConnection);
SqlParameter customerFirstParam = sqlCommand.Parameters.AddWithValue(@"firstName", @"Microsoft");
customerFirstParam.Direction = System.Data.ParameterDirection.Input;
customerFirstParam.ForceColumnEncryption = true;
SqlDataReader sqlDataReader = sqlCommand.ExecuteReader();
sqlDataReader.Close();
SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider =
new(new SqlClientCustomTokenCredential());
Dictionary<string, SqlColumnEncryptionKeyStoreProvider> customProvider = new()
{
{ SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, sqlColumnEncryptionAzureKeyVaultProvider }
};
// execute a query using provider from command-level cache. this will cache the cek in the local cek cache
sqlCommand.RegisterColumnEncryptionKeyStoreProvidersOnCommand(customProvider);
SqlDataReader sqlDataReader2 = sqlCommand.ExecuteReader();
sqlDataReader2.Close();
// global cek cache and local cek cache are populated above
// when using a new per-command provider, it will only use its local cek cache
// the following query should fail due to an empty cek cache and invalid credentials
customProvider[SqlColumnEncryptionAzureKeyVaultProvider.ProviderName] =
new SqlColumnEncryptionAzureKeyVaultProvider(new ClientSecretCredential("tenant", "client", "secret"));
sqlCommand.RegisterColumnEncryptionKeyStoreProvidersOnCommand(customProvider);
Exception ex = Assert.Throws<SqlException>(() => sqlCommand.ExecuteReader());
Assert.StartsWith("The current credential is not configured to acquire tokens for tenant", ex.InnerException.Message);
}
private class EmptyKeyStoreProvider : SqlColumnEncryptionKeyStoreProvider
{
public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string encryptionAlgorithm, byte[] encryptedColumnEncryptionKey)
{
return new byte[32];
}
public override byte[] EncryptColumnEncryptionKey(string masterKeyPath, string encryptionAlgorithm, byte[] columnEncryptionKey)
{
return new byte[32];
}
}
}
}