From 66edd5801f0a94dc172788b294e02fd1223f2dce Mon Sep 17 00:00:00 2001 From: Karina Zhou Date: Thu, 13 Feb 2020 16:16:00 -0800 Subject: [PATCH 1/3] Internal | Add Enclave Simulator for Internal Testing (#427) - Added support for enclave simulator bypassing the actual attestation. - Added BuildSimulator property to enable the simulator support. How to use the enclave simulator: Given Attestation Protocol = SIM and Enclave Attestation Url= SomeDummyURL in the connection string and run the AE enclave-enabled tests. NOTE: This change is for internal testing only. The simulator support should be disabled in the official nuget package releases. --- .../SqlConnectionAttestationProtocol.xml | 4 + src/Directory.Build.props | 4 + .../Microsoft.Data.SqlClient.NetCoreApp.cs | 5 + .../src/Microsoft.Data.SqlClient.csproj | 3 + .../Data/Common/DbConnectionStringCommon.cs | 23 +++- .../SqlClient/EnclaveDelegate.NetCoreApp.cs | 13 ++ .../SimulatorEnclaveProvider.NetCoreApp.cs | 116 ++++++++++++++++++ .../src/Microsoft/Data/SqlClient/TdsEnums.cs | 9 +- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 23 +++- .../netfx/ref/Microsoft.Data.SqlClient.cs | 5 + .../netfx/src/Microsoft.Data.SqlClient.csproj | 3 + .../Data/Common/DbConnectionStringCommon.cs | 23 +++- .../Data/SqlClient/EnclaveDelegate.cs | 13 ++ .../SqlClient/SimulatorEnclaveProvider.cs | 116 ++++++++++++++++++ .../src/Microsoft/Data/SqlClient/TdsEnums.cs | 9 +- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 23 +++- 16 files changed, 386 insertions(+), 6 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SimulatorEnclaveProvider.NetCoreApp.cs create mode 100644 src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SimulatorEnclaveProvider.cs diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionAttestationProtocol.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionAttestationProtocol.xml index 3f64ef38b8..d5f32ef2d3 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionAttestationProtocol.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionAttestationProtocol.xml @@ -13,6 +13,10 @@ Attestation portocol for Azure Attestation Service 1 + + Attestation protocol for Simulator + 2 + Attestation protocol for Host Guardian Service 3 diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 99dc4bc27a..955ea8cc4a 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -39,6 +39,10 @@ $(DotNetRoot)dotnet $(DotNetCmd).exe true + false + + + $(DefineConstants);ENCLAVE_SIMULATOR diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.NetCoreApp.cs b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.NetCoreApp.cs index 965902f3b8..837533659c 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.NetCoreApp.cs +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.NetCoreApp.cs @@ -113,6 +113,11 @@ public enum SqlConnectionAttestationProtocol /// AAS = 1, +#if ENCLAVE_SIMULATOR + /// + SIM = 2, +#endif + /// HGS = 3 } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 7b20a62142..1b8b1bb014 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -53,6 +53,9 @@ + + + diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionStringCommon.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionStringCommon.cs index c45b12af7f..8cf67f1aca 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionStringCommon.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionStringCommon.cs @@ -221,6 +221,9 @@ internal static string ColumnEncryptionSettingToString(SqlConnectionColumnEncryp /// const string AttestationProtocolHGS = "HGS"; const string AttestationProtocolAAS = "AAS"; +#if ENCLAVE_SIMULATOR + const string AttestationProtocolSIM = "SIM"; +#endif /// /// Convert a string value to the corresponding SqlConnectionAttestationProtocol @@ -240,6 +243,13 @@ internal static bool TryConvertToAttestationProtocol(string value, out SqlConnec result = SqlConnectionAttestationProtocol.AAS; return true; } +#if ENCLAVE_SIMULATOR + else if (StringComparer.InvariantCultureIgnoreCase.Equals(value, AttestationProtocolSIM)) + { + result = SqlConnectionAttestationProtocol.SIM; + return true; + } +#endif else { result = DbConnectionStringDefaults.AttestationProtocol; @@ -249,11 +259,18 @@ internal static bool TryConvertToAttestationProtocol(string value, out SqlConnec internal static bool IsValidAttestationProtocol(SqlConnectionAttestationProtocol value) { +#if ENCLAVE_SIMULATOR + Debug.Assert(Enum.GetNames(typeof(SqlConnectionAttestationProtocol)).Length == 4, "SqlConnectionAttestationProtocol enum has changed, update needed"); + return value == SqlConnectionAttestationProtocol.NotSpecified + || value == SqlConnectionAttestationProtocol.HGS + || value == SqlConnectionAttestationProtocol.AAS + || value == SqlConnectionAttestationProtocol.SIM; +#else Debug.Assert(Enum.GetNames(typeof(SqlConnectionAttestationProtocol)).Length == 3, "SqlConnectionAttestationProtocol enum has changed, update needed"); return value == SqlConnectionAttestationProtocol.NotSpecified || value == SqlConnectionAttestationProtocol.HGS || value == SqlConnectionAttestationProtocol.AAS; - +#endif } internal static string AttestationProtocolToString(SqlConnectionAttestationProtocol value) @@ -266,6 +283,10 @@ internal static string AttestationProtocolToString(SqlConnectionAttestationProto return AttestationProtocolHGS; case SqlConnectionAttestationProtocol.AAS: return AttestationProtocolAAS; +#if ENCLAVE_SIMULATOR + case SqlConnectionAttestationProtocol.SIM: + return AttestationProtocolSIM; +#endif default: return null; } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/EnclaveDelegate.NetCoreApp.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/EnclaveDelegate.NetCoreApp.cs index 48473b0722..a9ff36f11d 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/EnclaveDelegate.NetCoreApp.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/EnclaveDelegate.NetCoreApp.cs @@ -166,6 +166,14 @@ private SqlColumnEncryptionEnclaveProvider GetEnclaveProvider(SqlConnectionAttes sqlColumnEncryptionEnclaveProvider = EnclaveProviders[attestationProtocol]; break; +#if ENCLAVE_SIMULATOR + case SqlConnectionAttestationProtocol.SIM: + SimulatorEnclaveProvider simulatorEnclaveProvider = new SimulatorEnclaveProvider(); + EnclaveProviders[attestationProtocol] = (SqlColumnEncryptionEnclaveProvider)simulatorEnclaveProvider; + sqlColumnEncryptionEnclaveProvider = EnclaveProviders[attestationProtocol]; + break; +#endif + default: break; } @@ -189,6 +197,11 @@ private string ConvertAttestationProtocolToString(SqlConnectionAttestationProtoc case SqlConnectionAttestationProtocol.HGS: return "HGS"; +#if ENCLAVE_SIMULATOR + case SqlConnectionAttestationProtocol.SIM: + return "SIM"; +#endif + default: return "NotSpecified"; } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SimulatorEnclaveProvider.NetCoreApp.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SimulatorEnclaveProvider.NetCoreApp.cs new file mode 100644 index 0000000000..542af35688 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SimulatorEnclaveProvider.NetCoreApp.cs @@ -0,0 +1,116 @@ +// 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.Data.SqlClient; +using System.Diagnostics; +using System.Linq; +using System.Runtime.Caching; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Concurrent; + +namespace Microsoft.Data.SqlClient +{ + internal class SimulatorEnclaveProvider : EnclaveProviderBase + { + private static readonly int EnclaveSessionHandleSize = 8; + + // When overridden in a derived class, looks up an existing enclave session information in the enclave session cache. + // If the enclave provider doesn't implement enclave session caching, this method is expected to return null in the sqlEnclaveSession parameter. + public override void GetEnclaveSession(string servername, string attestationUrl, bool generateCustomData, out SqlEnclaveSession sqlEnclaveSession, out long counter, out byte[] customData, out int customDataLength) + { + GetEnclaveSessionHelper(servername, attestationUrl, false, out sqlEnclaveSession, out counter, out customData, out customDataLength); + } + + // Gets the information that SqlClient subsequently uses to initiate the process of attesting the enclave and to establish a secure session with the enclave. + // The information SqlClient subsequently uses to initiate the process of attesting the enclave and to establish a secure session with the enclave. + public override SqlEnclaveAttestationParameters GetAttestationParameters(string attestationUrl, byte[] customData, int customDataLength) + { + ECDiffieHellmanCng clientDHKey = new ECDiffieHellmanCng(384); + clientDHKey.KeyDerivationFunction = ECDiffieHellmanKeyDerivationFunction.Hash; + clientDHKey.HashAlgorithm = CngAlgorithm.Sha256; + + return new SqlEnclaveAttestationParameters(2, new byte[] { }, clientDHKey); + } + + // When overridden in a derived class, performs enclave attestation, generates a symmetric key for the session, creates a an enclave session and stores the session information in the cache. + public override void CreateEnclaveSession(byte[] attestationInfo, ECDiffieHellmanCng clientDHKey, string attestationUrl, string servername, byte[] customData, int customDataLength, out SqlEnclaveSession sqlEnclaveSession, out long counter) + { + ////for simulator: enclave does not send public key, and sends an empty attestation info + //// The only non-trivial content it sends is the session setup info (DH pubkey of enclave) + + sqlEnclaveSession = null; + counter = 0; + try + { + ThreadRetryCache.Remove(Thread.CurrentThread.ManagedThreadId.ToString()); + sqlEnclaveSession = GetEnclaveSessionFromCache(servername, attestationUrl, out counter); + + if (sqlEnclaveSession == null) + { + if (!string.IsNullOrEmpty(attestationUrl)) + { + ////Read AttestationInfo + int attestationInfoOffset = 0; + uint sizeOfTrustedModuleAttestationInfoBuffer = BitConverter.ToUInt32(attestationInfo, attestationInfoOffset); + attestationInfoOffset += sizeof(UInt32); + int sizeOfTrustedModuleAttestationInfoBufferInt = checked((int)sizeOfTrustedModuleAttestationInfoBuffer); + Debug.Assert(sizeOfTrustedModuleAttestationInfoBuffer == 0); + + ////read secure session info + uint sizeOfSecureSessionInfoResponse = BitConverter.ToUInt32(attestationInfo, attestationInfoOffset); + attestationInfoOffset += sizeof(UInt32); + + byte[] enclaveSessionHandle = new byte[EnclaveSessionHandleSize]; + Buffer.BlockCopy(attestationInfo, attestationInfoOffset, enclaveSessionHandle, 0, EnclaveSessionHandleSize); + attestationInfoOffset += EnclaveSessionHandleSize; + + uint sizeOfTrustedModuleDHPublicKeyBuffer = BitConverter.ToUInt32(attestationInfo, attestationInfoOffset); + attestationInfoOffset += sizeof(UInt32); + uint sizeOfTrustedModuleDHPublicKeySignatureBuffer = BitConverter.ToUInt32(attestationInfo, attestationInfoOffset); + attestationInfoOffset += sizeof(UInt32); + int sizeOfTrustedModuleDHPublicKeyBufferInt = checked((int)sizeOfTrustedModuleDHPublicKeyBuffer); + + byte[] trustedModuleDHPublicKey = new byte[sizeOfTrustedModuleDHPublicKeyBuffer]; + Buffer.BlockCopy(attestationInfo, attestationInfoOffset, trustedModuleDHPublicKey, 0, + sizeOfTrustedModuleDHPublicKeyBufferInt); + attestationInfoOffset += sizeOfTrustedModuleDHPublicKeyBufferInt; + + byte[] trustedModuleDHPublicKeySignature = new byte[sizeOfTrustedModuleDHPublicKeySignatureBuffer]; + Buffer.BlockCopy(attestationInfo, attestationInfoOffset, trustedModuleDHPublicKeySignature, 0, + checked((int)sizeOfTrustedModuleDHPublicKeySignatureBuffer)); + + CngKey k = CngKey.Import(trustedModuleDHPublicKey, CngKeyBlobFormat.EccPublicBlob); + byte[] sharedSecret = clientDHKey.DeriveKeyMaterial(k); + long sessionId = BitConverter.ToInt64(enclaveSessionHandle, 0); + sqlEnclaveSession = AddEnclaveSessionToCache(attestationUrl, servername, sharedSecret, sessionId, out counter); + } + else + { + throw new AlwaysEncryptedAttestationException(SR.FailToCreateEnclaveSession); + } + } + } + finally + { + UpdateEnclaveSessionLockStatus(sqlEnclaveSession); + } + } + + /// + /// When overridden in a derived class, looks up and evicts an enclave session from the enclave session cache, if the provider implements session caching. + /// + /// The name of the SQL Server instance containing the enclave. + /// The endpoint of an attestation service, SqlClient contacts to attest the enclave. + /// The session to be invalidated. + public override void InvalidateEnclaveSession(string serverName, string enclaveAttestationUrl, SqlEnclaveSession enclaveSessionToInvalidate) + { + InvalidateEnclaveSessionHelper(serverName, enclaveAttestationUrl, enclaveSessionToInvalidate); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs index 48aadf85e5..8828409ddf 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -983,6 +983,9 @@ internal static string GetSniContextEnumName(SniContext sniContext) internal const int AEAD_AES_256_CBC_HMAC_SHA256 = 2; internal const string ENCLAVE_TYPE_VBS = "VBS"; internal const string ENCLAVE_TYPE_SGX = "SGX"; +#if ENCLAVE_SIMULATOR + internal const string ENCLAVE_TYPE_SIMULATOR = "SIMULATOR"; +#endif // TCE Param names for exec handling internal const string TCE_PARAM_CIPHERTEXT = "cipherText"; @@ -1056,6 +1059,11 @@ public enum SqlConnectionAttestationProtocol /// AAS = 1, +#if ENCLAVE_SIMULATOR + /// + SIM = 2, +#endif + /// HGS = 3 } @@ -1159,4 +1167,3 @@ internal enum DescribeParameterEncryptionResultSet3 AttestationInfo = 0, } } - diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index 68ce6b544b..3c827f323e 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -3031,10 +3031,13 @@ private bool TryProcessFeatureExtAck(TdsParserStateObject stateObj) private bool IsValidAttestationProtocol(SqlConnectionAttestationProtocol attestationProtocol, string enclaveType) { - switch (enclaveType) + switch (enclaveType.ToUpper()) { case TdsEnums.ENCLAVE_TYPE_VBS: if (attestationProtocol != SqlConnectionAttestationProtocol.AAS +#if ENCLAVE_SIMULATOR + && attestationProtocol != SqlConnectionAttestationProtocol.SIM +#endif && attestationProtocol != SqlConnectionAttestationProtocol.HGS) { return false; @@ -3042,12 +3045,25 @@ private bool IsValidAttestationProtocol(SqlConnectionAttestationProtocol attesta break; case TdsEnums.ENCLAVE_TYPE_SGX: +#if ENCLAVE_SIMULATOR + if (attestationProtocol != SqlConnectionAttestationProtocol.AAS + && attestationProtocol != SqlConnectionAttestationProtocol.SIM) +#else if (attestationProtocol != SqlConnectionAttestationProtocol.AAS) +#endif { return false; } break; +#if ENCLAVE_SIMULATOR + case TdsEnums.ENCLAVE_TYPE_SIMULATOR: + if (attestationProtocol != SqlConnectionAttestationProtocol.SIM) + { + return false; + } + break; +#endif default: // if we reach here, the enclave type is not supported throw SQL.EnclaveTypeNotSupported(enclaveType); @@ -3066,6 +3082,11 @@ private string ConvertAttestationProtocolToString(SqlConnectionAttestationProtoc case SqlConnectionAttestationProtocol.HGS: return "HGS"; +#if ENCLAVE_SIMULATOR + case SqlConnectionAttestationProtocol.SIM: + return "SIM"; +#endif + default: return "NotSpecified"; } diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs index 8a9fa8429d..6371d4fbca 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs @@ -801,6 +801,11 @@ public enum SqlConnectionAttestationProtocol /// AAS = 1, +#if ENCLAVE_SIMULATOR + /// + SIM = 2, +#endif + /// HGS = 3 } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index bdeb3cc6c0..ed6135157b 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -308,6 +308,9 @@ + + + diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Common/DbConnectionStringCommon.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Common/DbConnectionStringCommon.cs index 772f422b1a..08857968cb 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Common/DbConnectionStringCommon.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Common/DbConnectionStringCommon.cs @@ -822,6 +822,9 @@ internal static SqlConnectionColumnEncryptionSetting ConvertToColumnEncryptionSe /// const string AttestationProtocolHGS = "HGS"; const string AttestationProtocolAAS = "AAS"; +#if ENCLAVE_SIMULATOR + const string AttestationProtocolSIM = "SIM"; +#endif /// /// Convert a string value to the corresponding SqlConnectionAttestationProtocol @@ -841,6 +844,13 @@ internal static bool TryConvertToAttestationProtocol(string value, out SqlConnec result = SqlConnectionAttestationProtocol.AAS; return true; } +#if ENCLAVE_SIMULATOR + else if (StringComparer.InvariantCultureIgnoreCase.Equals(value, AttestationProtocolSIM)) + { + result = SqlConnectionAttestationProtocol.SIM; + return true; + } +#endif else { result = DbConnectionStringDefaults.AttestationProtocol; @@ -850,11 +860,18 @@ internal static bool TryConvertToAttestationProtocol(string value, out SqlConnec internal static bool IsValidAttestationProtocol(SqlConnectionAttestationProtocol value) { +#if ENCLAVE_SIMULATOR + Debug.Assert(Enum.GetNames(typeof(SqlConnectionAttestationProtocol)).Length == 4, "SqlConnectionAttestationProtocol enum has changed, update needed"); + return value == SqlConnectionAttestationProtocol.NotSpecified + || value == SqlConnectionAttestationProtocol.HGS + || value == SqlConnectionAttestationProtocol.AAS + || value == SqlConnectionAttestationProtocol.SIM; +#else Debug.Assert(Enum.GetNames(typeof(SqlConnectionAttestationProtocol)).Length == 3, "SqlConnectionAttestationProtocol enum has changed, update needed"); return value == SqlConnectionAttestationProtocol.NotSpecified || value == SqlConnectionAttestationProtocol.HGS || value == SqlConnectionAttestationProtocol.AAS; - +#endif } internal static string AttestationProtocolToString(SqlConnectionAttestationProtocol value) @@ -867,6 +884,10 @@ internal static string AttestationProtocolToString(SqlConnectionAttestationProto return AttestationProtocolHGS; case SqlConnectionAttestationProtocol.AAS: return AttestationProtocolAAS; +#if ENCLAVE_SIMULATOR + case SqlConnectionAttestationProtocol.SIM: + return AttestationProtocolSIM; +#endif default: return null; } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/EnclaveDelegate.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/EnclaveDelegate.cs index 3ab2a78a38..d57bc47067 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/EnclaveDelegate.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/EnclaveDelegate.cs @@ -212,6 +212,14 @@ private SqlColumnEncryptionEnclaveProvider GetEnclaveProvider(SqlConnectionAttes sqlColumnEncryptionEnclaveProvider = EnclaveProviders[attestationProtocol]; break; +#if ENCLAVE_SIMULATOR + case SqlConnectionAttestationProtocol.SIM: + SimulatorEnclaveProvider simulatorEnclaveProvider = new SimulatorEnclaveProvider(); + EnclaveProviders[attestationProtocol] = (SqlColumnEncryptionEnclaveProvider)simulatorEnclaveProvider; + sqlColumnEncryptionEnclaveProvider = EnclaveProviders[attestationProtocol]; + break; +#endif + default: break; } @@ -235,6 +243,11 @@ private string ConvertAttestationProtocolToString(SqlConnectionAttestationProtoc case SqlConnectionAttestationProtocol.HGS: return "HGS"; +#if ENCLAVE_SIMULATOR + case SqlConnectionAttestationProtocol.SIM: + return "SIM"; +#endif + default: return "NotSpecified"; } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SimulatorEnclaveProvider.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SimulatorEnclaveProvider.cs new file mode 100644 index 0000000000..811816f1e6 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SimulatorEnclaveProvider.cs @@ -0,0 +1,116 @@ +// 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.Data.SqlClient; +using System.Diagnostics; +using System.Linq; +using System.Runtime.Caching; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Concurrent; + +namespace Microsoft.Data.SqlClient +{ + internal class SimulatorEnclaveProvider : EnclaveProviderBase + { + private static readonly int EnclaveSessionHandleSize = 8; + + // When overridden in a derived class, looks up an existing enclave session information in the enclave session cache. + // If the enclave provider doesn't implement enclave session caching, this method is expected to return null in the sqlEnclaveSession parameter. + public override void GetEnclaveSession(string servername, string attestationUrl, bool generateCustomData, out SqlEnclaveSession sqlEnclaveSession, out long counter, out byte[] customData, out int customDataLength) + { + GetEnclaveSessionHelper(servername, attestationUrl, false, out sqlEnclaveSession, out counter, out customData, out customDataLength); + } + + // Gets the information that SqlClient subsequently uses to initiate the process of attesting the enclave and to establish a secure session with the enclave. + // The information SqlClient subsequently uses to initiate the process of attesting the enclave and to establish a secure session with the enclave. + public override SqlEnclaveAttestationParameters GetAttestationParameters(string attestationUrl, byte[] customData, int customDataLength) + { + ECDiffieHellmanCng clientDHKey = new ECDiffieHellmanCng(384); + clientDHKey.KeyDerivationFunction = ECDiffieHellmanKeyDerivationFunction.Hash; + clientDHKey.HashAlgorithm = CngAlgorithm.Sha256; + + return new SqlEnclaveAttestationParameters(2, new byte[] { }, clientDHKey); + } + + // When overridden in a derived class, performs enclave attestation, generates a symmetric key for the session, creates a an enclave session and stores the session information in the cache. + public override void CreateEnclaveSession(byte[] attestationInfo, ECDiffieHellmanCng clientDHKey, string attestationUrl, string servername, byte[] customData, int customDataLength, out SqlEnclaveSession sqlEnclaveSession, out long counter) + { + ////for simulator: enclave does not send public key, and sends an empty attestation info + //// The only non-trivial content it sends is the session setup info (DH pubkey of enclave) + + sqlEnclaveSession = null; + counter = 0; + try + { + ThreadRetryCache.Remove(Thread.CurrentThread.ManagedThreadId.ToString()); + sqlEnclaveSession = GetEnclaveSessionFromCache(servername, attestationUrl, out counter); + + if (sqlEnclaveSession == null) + { + if (!string.IsNullOrEmpty(attestationUrl)) + { + ////Read AttestationInfo + int attestationInfoOffset = 0; + uint sizeOfTrustedModuleAttestationInfoBuffer = BitConverter.ToUInt32(attestationInfo, attestationInfoOffset); + attestationInfoOffset += sizeof(UInt32); + int sizeOfTrustedModuleAttestationInfoBufferInt = checked((int)sizeOfTrustedModuleAttestationInfoBuffer); + Debug.Assert(sizeOfTrustedModuleAttestationInfoBuffer == 0); + + ////read secure session info + uint sizeOfSecureSessionInfoResponse = BitConverter.ToUInt32(attestationInfo, attestationInfoOffset); + attestationInfoOffset += sizeof(UInt32); + + byte[] enclaveSessionHandle = new byte[EnclaveSessionHandleSize]; + Buffer.BlockCopy(attestationInfo, attestationInfoOffset, enclaveSessionHandle, 0, EnclaveSessionHandleSize); + attestationInfoOffset += EnclaveSessionHandleSize; + + uint sizeOfTrustedModuleDHPublicKeyBuffer = BitConverter.ToUInt32(attestationInfo, attestationInfoOffset); + attestationInfoOffset += sizeof(UInt32); + uint sizeOfTrustedModuleDHPublicKeySignatureBuffer = BitConverter.ToUInt32(attestationInfo, attestationInfoOffset); + attestationInfoOffset += sizeof(UInt32); + int sizeOfTrustedModuleDHPublicKeyBufferInt = checked((int)sizeOfTrustedModuleDHPublicKeyBuffer); + + byte[] trustedModuleDHPublicKey = new byte[sizeOfTrustedModuleDHPublicKeyBuffer]; + Buffer.BlockCopy(attestationInfo, attestationInfoOffset, trustedModuleDHPublicKey, 0, + sizeOfTrustedModuleDHPublicKeyBufferInt); + attestationInfoOffset += sizeOfTrustedModuleDHPublicKeyBufferInt; + + byte[] trustedModuleDHPublicKeySignature = new byte[sizeOfTrustedModuleDHPublicKeySignatureBuffer]; + Buffer.BlockCopy(attestationInfo, attestationInfoOffset, trustedModuleDHPublicKeySignature, 0, + checked((int)sizeOfTrustedModuleDHPublicKeySignatureBuffer)); + + CngKey k = CngKey.Import(trustedModuleDHPublicKey, CngKeyBlobFormat.EccPublicBlob); + byte[] sharedSecret = clientDHKey.DeriveKeyMaterial(k); + long sessionId = BitConverter.ToInt64(enclaveSessionHandle, 0); + sqlEnclaveSession = AddEnclaveSessionToCache(attestationUrl, servername, sharedSecret, sessionId, out counter); + } + else + { + throw new AlwaysEncryptedAttestationException(Strings.FailToCreateEnclaveSession); + } + } + } + finally + { + UpdateEnclaveSessionLockStatus(sqlEnclaveSession); + } + } + + /// + /// When overridden in a derived class, looks up and evicts an enclave session from the enclave session cache, if the provider implements session caching. + /// + /// The name of the SQL Server instance containing the enclave. + /// The endpoint of an attestation service, SqlClient contacts to attest the enclave. + /// The session to be invalidated. + public override void InvalidateEnclaveSession(string serverName, string enclaveAttestationUrl, SqlEnclaveSession enclaveSessionToInvalidate) + { + InvalidateEnclaveSessionHelper(serverName, enclaveAttestationUrl, enclaveSessionToInvalidate); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs index 8661bc70e7..f09836b163 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -947,7 +947,9 @@ internal enum FedAuthInfoId : byte internal const int AEAD_AES_256_CBC_HMAC_SHA256 = 2; internal const string ENCLAVE_TYPE_VBS = "VBS"; internal const string ENCLAVE_TYPE_SGX = "SGX"; - +#if ENCLAVE_SIMULATOR + internal const string ENCLAVE_TYPE_SIMULATOR = "SIMULATOR"; +#endif // TCE Param names for exec handling internal const string TCE_PARAM_CIPHERTEXT = "cipherText"; internal const string TCE_PARAM_CIPHER_ALGORITHM_ID = "cipherAlgorithmId"; @@ -1046,6 +1048,11 @@ public enum SqlConnectionAttestationProtocol /// AAS = 1, +#if ENCLAVE_SIMULATOR + /// + SIM = 2, +#endif + /// HGS = 3 } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index 5f5ddc62e0..cb4fc848db 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -3509,10 +3509,13 @@ private bool TryProcessFeatureExtAck(TdsParserStateObject stateObj) private bool IsValidAttestationProtocol(SqlConnectionAttestationProtocol attestationProtocol, string enclaveType) { - switch (enclaveType) + switch (enclaveType.ToUpper()) { case TdsEnums.ENCLAVE_TYPE_VBS: if (attestationProtocol != SqlConnectionAttestationProtocol.AAS +#if ENCLAVE_SIMULATOR + && attestationProtocol != SqlConnectionAttestationProtocol.SIM +#endif && attestationProtocol != SqlConnectionAttestationProtocol.HGS) { return false; @@ -3520,12 +3523,25 @@ private bool IsValidAttestationProtocol(SqlConnectionAttestationProtocol attesta break; case TdsEnums.ENCLAVE_TYPE_SGX: +#if ENCLAVE_SIMULATOR + if (attestationProtocol != SqlConnectionAttestationProtocol.AAS + && attestationProtocol != SqlConnectionAttestationProtocol.SIM) +#else if (attestationProtocol != SqlConnectionAttestationProtocol.AAS) +#endif { return false; } break; +#if ENCLAVE_SIMULATOR + case TdsEnums.ENCLAVE_TYPE_SIMULATOR: + if (attestationProtocol != SqlConnectionAttestationProtocol.SIM) + { + return false; + } + break; +#endif default: // if we reach here, the enclave type is not supported throw SQL.EnclaveTypeNotSupported(enclaveType); @@ -3544,6 +3560,11 @@ private string ConvertAttestationProtocolToString(SqlConnectionAttestationProtoc case SqlConnectionAttestationProtocol.HGS: return "HGS"; +#if ENCLAVE_SIMULATOR + case SqlConnectionAttestationProtocol.SIM: + return "SIM"; +#endif + default: return "NotSpecified"; } From 0aeaa2e0586217e11eeee280a251b046e01454cd Mon Sep 17 00:00:00 2001 From: David Engel Date: Tue, 24 Mar 2020 17:34:54 -0700 Subject: [PATCH 2/3] Address MARS TDS header contained errors --- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 8 +- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 2 +- ....Data.SqlClient.ManualTesting.Tests.csproj | 1 + .../AsyncCancelledConnectionsTest.cs | 258 ++++++++++++++++++ 4 files changed, 264 insertions(+), 5 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index 19f3398aac..ada0f9c514 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -2938,12 +2938,12 @@ private bool TryProcessDone(SqlCommand cmd, SqlDataReader reader, ref RunBehavio // _pendingData set by e.g. 'TdsExecuteSQLBatch' // _hasOpenResult always set to true by 'WriteMarsHeader' // - if (!stateObj.HasPendingData && stateObj.HasOpenResult) + if (!stateObj._attentionSent && !stateObj.HasPendingData && stateObj.HasOpenResult) { /* Debug.Assert(!((sqlTransaction != null && _distributedTransaction != null) || - (_userStartedLocalTransaction != null && _distributedTransaction != null)) - , "ProcessDone - have both distributed and local transactions not null!"); + (_userStartedLocalTransaction != null && _distributedTransaction != null)) + , "ProcessDone - have both distributed and local transactions not null!"); */ // WebData 112722 @@ -8706,7 +8706,7 @@ internal Task TdsExecuteSQLBatch(string text, int timeout, SqlNotificationReques { Debug.Assert(!sync, "Should not have gotten a Task when writing in sync mode"); - // Need to wait for flush - continuation will unlock the connection + // Need to wait for flush - continuation will unlock the connection bool taskReleaseConnectionLock = releaseConnectionLock; releaseConnectionLock = false; return executeTask.ContinueWith( diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index eed44ac752..b03e1ee479 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -3329,7 +3329,7 @@ private bool TryProcessDone(SqlCommand cmd, SqlDataReader reader, ref RunBehavio // _pendingData set by e.g. 'TdsExecuteSQLBatch' // _hasOpenResult always set to true by 'WriteMarsHeader' // - if (!stateObj._pendingData && stateObj._hasOpenResult) + if (!stateObj._attentionSent && !stateObj._pendingData && stateObj._hasOpenResult) { /* Debug.Assert(!((sqlTransaction != null && _distributedTransaction != null) || diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index 3a596dd237..e6c028a870 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -99,6 +99,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs new file mode 100644 index 0000000000..c3cc2d6b07 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests +{ + public class AsyncCancelledConnectionsTest + { + private readonly ITestOutputHelper _output; + private const int NumberOfTasks = 100; // How many attempts to poison the connection pool we will try + private const int NumberOfNonPoisoned = 10; // Number of normal requests for each attempt + + public AsyncCancelledConnectionsTest(ITestOutputHelper output) + { + this._output = output; + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public void CancelAsyncConnections() + { + string connectionString = DataTestUtility.TCPConnectionString; + _watch = Stopwatch.StartNew(); + _random = new Random(4); // chosen via fair dice role. + ParallelLoopResult results = new ParallelLoopResult(); + try + { + // Setup a timer so that we can see what is going on while our tasks run + using (new Timer(TimerCallback, state: null, dueTime: TimeSpan.FromSeconds(5), period: TimeSpan.FromSeconds(5))) + { + results = Parallel.For( + fromInclusive: 0, + toExclusive: NumberOfTasks, + (int i) => DoManyAsync(connectionString).GetAwaiter().GetResult()); + } + } + catch (Exception ex) + { + _output.WriteLine(ex.ToString()); + } + while (!results.IsCompleted) + { + Thread.Sleep(50); + } + DisplaySummary(); + foreach (var detail in _exceptionDetails) + { + _output.WriteLine(detail); + } + Assert.Empty(_exceptionDetails); + } + + // Display one row every 5'ish seconds + private void TimerCallback(object state) + { + lock (_lockObject) + { + DisplaySummary(); + } + } + + private void DisplaySummary() + { + int count; + lock (_exceptionDetails) + { + count = _exceptionDetails.Count; + } + + _output.WriteLine($"{_watch.Elapsed} {_continue} Started:{_start} Done:{_done} InFlight:{_inFlight} RowsRead:{_rowsRead} ResultRead:{_resultRead} PoisonedEnded:{_poisonedEnded} nonPoisonedExceptions:{_nonPoisonedExceptions} PoisonedCleanupExceptions:{_poisonCleanUpExceptions} Count:{count} Found:{_found}"); + } + + // This is the the main body that our Tasks run + private async Task DoManyAsync(string connectionString) + { + Interlocked.Increment(ref _start); + Interlocked.Increment(ref _inFlight); + + // First poison + await DoOneAsync(connectionString, poison: true); + + for (int i = 0; i < NumberOfNonPoisoned && _continue; i++) + { + // now run some without poisoning + await DoOneAsync(connectionString); + } + + Interlocked.Decrement(ref _inFlight); + Interlocked.Increment(ref _done); + } + + // This will do our work, open a connection, and run a query (that returns 4 results sets) + // if we are poisoning we will + // 1 - Interject some sleeps in the sql statement so that it will run long enough that we can cancel it + // 2 - Setup a time bomb task that will cancel the command a random amount of time later + private async Task DoOneAsync(string connectionString, bool poison = false) + { + try + { + using (var connection = new SqlConnection(connectionString)) + { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < 4; i++) + { + builder.AppendLine("SELECT name FROM sys.tables"); + if (poison && i < 3) + { + builder.AppendLine("WAITFOR DELAY '00:00:01'"); + } + } + + int rowsRead = 0; + int resultRead = 0; + + try + { + await connection.OpenAsync(); + using (var command = connection.CreateCommand()) + { + Task timeBombTask = default; + try + { + // Setup our time bomb + if (poison) + { + timeBombTask = TimeBombAsync(command); + } + + command.CommandText = builder.ToString(); + + // Attempt to read all of the data + using (var reader = await command.ExecuteReaderAsync()) + { + try + { + do + { + resultRead++; + while (await reader.ReadAsync() && _continue) + { + rowsRead++; + } + } + while (await reader.NextResultAsync() && _continue); + } + catch when (poison) + { + // This looks a little strange, we failed to read above so this should fail too + // But consider the case where this code is elsewhere (in the Dispose method of a class holding this logic) + try + { + while (await reader.NextResultAsync()) + { + } + } + catch + { + Interlocked.Increment(ref _poisonCleanUpExceptions); + } + + throw; + } + } + } + finally + { + // Make sure to clean up our time bomb + // It is unlikely, but the timebomb may get delayed in the Task Queue + // And we don't want it running after we dispose the command + if (timeBombTask != default) + { + await timeBombTask; + } + } + } + } + finally + { + Interlocked.Add(ref _rowsRead, rowsRead); + Interlocked.Add(ref _resultRead, resultRead); + if (poison) + { + Interlocked.Increment(ref _poisonedEnded); + } + } + } + } + catch (Exception ex) + { + if (!poison) + { + Interlocked.Increment(ref _nonPoisonedExceptions); + + string details = ex.ToString(); + details = details.Substring(0, Math.Min(200, details.Length)); + lock (_exceptionDetails) + { + _exceptionDetails.Add(details); + } + } + + if (ex.Message.Contains("The MARS TDS header contained errors.")) + { + _continue = false; + if (_found == 0) // This check is not really safe we may list more than one. + { + lock (_lockObject) + { + // You will notice that poison will be likely be false here, it is the normal commands that suffer + // Once we have successfully poisoned the connection pool, we may start to see some other request to poison fail just like the normal requests + _output.WriteLine($"{poison} {DateTime.UtcNow.ToString("O")}"); + _output.WriteLine(ex.ToString()); + } + } + Interlocked.Increment(ref _found); + } + } + } + + private async Task TimeBombAsync(SqlCommand command) + { + await SleepAsync(100, 3000); + command.Cancel(); + } + + private async Task SleepAsync(int minMs, int maxMs) + { + int delayMs; + lock (_random) + { + delayMs = _random.Next(minMs, maxMs); + } + await Task.Delay(delayMs); + } + + private Stopwatch _watch; + + private int _inFlight; + private int _start; + private int _done; + private int _rowsRead; + private int _resultRead; + private int _nonPoisonedExceptions; + private int _poisonedEnded; + private int _poisonCleanUpExceptions; + private bool _continue = true; + private int _found; + private Random _random; + private object _lockObject = new object(); + + private HashSet _exceptionDetails = new HashSet(); + } +} From 161b8834e83f19f9f24310a962c6d2e89c76451f Mon Sep 17 00:00:00 2001 From: David Engel Date: Wed, 25 Mar 2020 15:55:12 -0700 Subject: [PATCH 3/3] Adding comment and disabling new test for pipelines. --- .../netcore/src/Microsoft/Data/SqlClient/TdsParser.cs | 1 + .../netfx/src/Microsoft/Data/SqlClient/TdsParser.cs | 1 + .../ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs | 1 + 3 files changed, 3 insertions(+) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index ada0f9c514..4c0a87c882 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -2935,6 +2935,7 @@ private bool TryProcessDone(SqlCommand cmd, SqlDataReader reader, ref RunBehavio } } + // _attentionSent set by 'SendAttention' // _pendingData set by e.g. 'TdsExecuteSQLBatch' // _hasOpenResult always set to true by 'WriteMarsHeader' // diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index b03e1ee479..4ec7e734f6 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -3326,6 +3326,7 @@ private bool TryProcessDone(SqlCommand cmd, SqlDataReader reader, ref RunBehavio } } + // _attentionSent set by 'SendAttention' // _pendingData set by e.g. 'TdsExecuteSQLBatch' // _hasOpenResult always set to true by 'WriteMarsHeader' // diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs index c3cc2d6b07..5578e968ce 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AsyncTest/AsyncCancelledConnectionsTest.cs @@ -21,6 +21,7 @@ public AsyncCancelledConnectionsTest(ITestOutputHelper output) this._output = output; } + [ActiveIssue(490)] // This test seems to fail regularly in pipelines due to deadlocks. But it's still useful for local testing. [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] public void CancelAsyncConnections() {