From e9992fa3d15eea189cd353bd27e262ad8cb09e96 Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Wed, 17 Jan 2024 15:27:44 -0800 Subject: [PATCH] Abstract the SSPI context generation This change introduces SSPIContextProvider that can generate payloads for SSPI. Specifically, this change plumbs the current SSPI context generation into this object, while later changes will continue to update the shape to be a more general purpose, public API. --- .../src/Microsoft.Data.SqlClient.csproj | 17 +- .../Data/SqlClient/TdsParser.Windows.cs | 34 +- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 258 +-------------- .../SqlClient/TdsParserStateObject.netcore.cs | 7 +- .../SqlClient/TdsParserStateObjectManaged.cs | 39 +-- .../SqlClient/TdsParserStateObjectNative.cs | 9 +- .../netfx/src/Microsoft.Data.SqlClient.csproj | 15 + .../src/Microsoft/Data/SqlClient/TdsParser.cs | 312 +----------------- .../SqlClient/TdsParserStateObject.netfx.cs | 4 +- .../SSPI/ManagedSSPIContextProvider.cs | 23 ++ .../SSPI/NativeSSPIContextProvider.cs | 68 ++++ .../SSPI/NegotiateSSPIContextProvider.cs | 28 ++ .../SqlClient/SSPI/SSPIContextProvider.cs | 56 ++++ .../src/Microsoft/Data/SqlClient/TdsParser.cs | 265 +++++++++++++++ 14 files changed, 518 insertions(+), 617 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/ManagedSSPIContextProvider.cs create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NativeSSPIContextProvider.cs create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NegotiateSSPIContextProvider.cs create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SSPIContextProvider.cs create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs 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 371c7124c5..f50d7697ae 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -516,6 +516,18 @@ Common\System\Diagnostics\CodeAnalysis.cs + + Microsoft\Data\SqlClient\SSPI\ManagedSSPIContextProvider.cs + + + Microsoft\Data\SqlClient\SSPI\NegotiateSSPIContextProvider.cs + + + Microsoft\Data\SqlClient\SSPI\SSPIContextProvider.cs + + + Microsoft\Data\SqlClient\TdsParser.cs + @@ -580,7 +592,7 @@ - + Resources\StringsHelper.cs @@ -767,7 +779,8 @@ Common\Interop\Windows\kernel32\Interop.LoadLibraryEx.cs - + + diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs index f7a0363f27..7f15666951 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs @@ -10,8 +10,6 @@ namespace Microsoft.Data.SqlClient { internal sealed partial class TdsParser { - private static volatile bool s_fSSPILoaded = false; // bool to indicate whether library has been loaded - internal void PostReadAsyncForMars() { if (TdsParserStateObjectFactory.UseManagedSNI) @@ -43,37 +41,7 @@ internal void PostReadAsyncForMars() _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); ThrowExceptionAndWarning(_physicalStateObj); } - } - - private void LoadSSPILibrary() - { - if (TdsParserStateObjectFactory.UseManagedSNI) - return; - // Outer check so we don't acquire lock once it's loaded. - if (!s_fSSPILoaded) - { - lock (s_tdsParserLock) - { - // re-check inside lock - if (!s_fSSPILoaded) - { - // use local for ref param to defer setting s_maxSSPILength until we know the call succeeded. - uint maxLength = 0; - - if (0 != SNINativeMethodWrapper.SNISecInitPackage(ref maxLength)) - SSPIError(SQLMessage.SSPIInitializeError(), TdsEnums.INIT_SSPI_PACKAGE); - - s_maxSSPILength = maxLength; - s_fSSPILoaded = true; - } - } - } - - if (s_maxSSPILength > int.MaxValue) - { - throw SQL.InvalidSSPIPacketSize(); // SqlBu 332503 - } - } + } private void WaitForSSLHandShakeToComplete(ref uint error, ref int protocolVersion) { 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 7e04f6b9c5..4d731102fe 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 @@ -55,6 +55,8 @@ internal static void Assert(string message) private static int _objectTypeCount; // EventSource counter private readonly SqlClientLogger _logger = new SqlClientLogger(); + private SSPIContextProvider _authenticationProvider; + internal readonly int _objectID = Interlocked.Increment(ref _objectTypeCount); internal int ObjectID => _objectID; @@ -146,16 +148,9 @@ internal static void Assert(string message) // NIC address caching private static byte[] s_nicAddress; // cache the NIC address from the registry - // SSPI variables - - private volatile static uint s_maxSSPILength = 0; // variable to hold max SSPI data size, keep for token from server - // textptr sequence private static readonly byte[] s_longDataHeader = { 0x10, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; - private static object s_tdsParserLock = new object(); - - // XML metadata substitute sequence private static readonly byte[] s_xmlMetadataSubstituteSequence = { 0xe7, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00 }; @@ -422,11 +417,12 @@ internal void Connect( } _sniSpnBuffer = null; + _authenticationProvider = null; // AD Integrated behaves like Windows integrated when connecting to a non-fedAuth server if (integratedSecurity || authType == SqlAuthenticationMethod.ActiveDirectoryIntegrated) { - LoadSSPILibrary(); + _authenticationProvider = _physicalStateObj.CreateSSPIContextProvider(); SqlClientEventSource.Log.TryTraceEvent("TdsParser.Connect | SEC | SSPI or Active Directory Authentication Library loaded for SQL Server based integrated authentication"); } @@ -472,6 +468,8 @@ internal void Connect( hostNameInCertificate, serverCertificateFilename); + _authenticationProvider?.Initialize(serverInfo, _physicalStateObj, this); + if (TdsEnums.SNI_SUCCESS != _physicalStateObj.Status) { _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); @@ -569,6 +567,8 @@ internal void Connect( hostNameInCertificate, serverCertificateFilename); + _authenticationProvider?.Initialize(serverInfo, _physicalStateObj, this); + if (TdsEnums.SNI_SUCCESS != _physicalStateObj.Status) { _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); @@ -1286,7 +1286,7 @@ private void FireInfoMessageEvent(SqlConnection connection, SqlCommand command, sqlErs.Add(error); - SqlException exc = SqlException.CreateException(sqlErs, serverVersion, _connHandler, innerException:null, batchCommand: command?.GetCurrentBatchCommand()); + SqlException exc = SqlException.CreateException(sqlErs, serverVersion, _connHandler, innerException: null, batchCommand: command?.GetCurrentBatchCommand()); bool notified; connection.OnInfoMessage(new SqlInfoMessageEventArgs(exc), out notified); @@ -4001,7 +4001,7 @@ internal bool TryProcessError(byte token, TdsParserStateObject stateObj, SqlComm { batchIndex = command.GetCurrentBatchIndex(); } - error = new SqlError(number, state, errorClass, _server, message, procedure, line,exception: null, batchIndex: batchIndex); + error = new SqlError(number, state, errorClass, _server, message, procedure, line, exception: null, batchIndex: batchIndex); return true; } @@ -5744,7 +5744,7 @@ private bool TryReadSqlStringValue(SqlBuffer value, byte type, int length, Encod s = ""; } } - + if (buffIsRented) { // do not use clearArray:true on the rented array because it can be massively larger @@ -8108,165 +8108,6 @@ internal int WriteFedAuthFeatureRequest(FederatedAuthenticationFeatureExtensionD return len; } - internal void TdsLogin(SqlLogin rec, TdsEnums.FeatureExtension requestedFeatures, SessionData recoverySessionData, FederatedAuthenticationFeatureExtensionData fedAuthFeatureExtensionData, SqlConnectionEncryptOption encrypt) - { - _physicalStateObj.SetTimeoutSeconds(rec.timeout); - - Debug.Assert(recoverySessionData == null || (requestedFeatures & TdsEnums.FeatureExtension.SessionRecovery) != 0, "Recovery session data without session recovery feature request"); - Debug.Assert(TdsEnums.MAXLEN_HOSTNAME >= rec.hostName.Length, "_workstationId.Length exceeds the max length for this value"); - - Debug.Assert(!rec.useSSPI || (requestedFeatures & TdsEnums.FeatureExtension.FedAuth) == 0, "Cannot use both SSPI and FedAuth"); - Debug.Assert(fedAuthFeatureExtensionData == null || (requestedFeatures & TdsEnums.FeatureExtension.FedAuth) != 0, "fedAuthFeatureExtensionData provided without fed auth feature request"); - Debug.Assert(fedAuthFeatureExtensionData != null || (requestedFeatures & TdsEnums.FeatureExtension.FedAuth) == 0, "Fed Auth feature requested without specifying fedAuthFeatureExtensionData."); - - Debug.Assert(rec.userName == null || (rec.userName != null && TdsEnums.MAXLEN_CLIENTID >= rec.userName.Length), "_userID.Length exceeds the max length for this value"); - Debug.Assert(rec.credential == null || (rec.credential != null && TdsEnums.MAXLEN_CLIENTID >= rec.credential.UserId.Length), "_credential.UserId.Length exceeds the max length for this value"); - - Debug.Assert(rec.password == null || (rec.password != null && TdsEnums.MAXLEN_CLIENTSECRET >= rec.password.Length), "_password.Length exceeds the max length for this value"); - Debug.Assert(rec.credential == null || (rec.credential != null && TdsEnums.MAXLEN_CLIENTSECRET >= rec.credential.Password.Length), "_credential.Password.Length exceeds the max length for this value"); - - Debug.Assert(rec.credential != null || rec.userName != null || rec.password != null, "cannot mix the new secure password system and the connection string based password"); - Debug.Assert(rec.newSecurePassword != null || rec.newPassword != null, "cannot have both new secure change password and string based change password"); - Debug.Assert(TdsEnums.MAXLEN_APPNAME >= rec.applicationName.Length, "_applicationName.Length exceeds the max length for this value"); - Debug.Assert(TdsEnums.MAXLEN_SERVERNAME >= rec.serverName.Length, "_dataSource.Length exceeds the max length for this value"); - Debug.Assert(TdsEnums.MAXLEN_LANGUAGE >= rec.language.Length, "_currentLanguage .Length exceeds the max length for this value"); - Debug.Assert(TdsEnums.MAXLEN_DATABASE >= rec.database.Length, "_initialCatalog.Length exceeds the max length for this value"); - Debug.Assert(TdsEnums.MAXLEN_ATTACHDBFILE >= rec.attachDBFilename.Length, "_attachDBFileName.Length exceeds the max length for this value"); - - Debug.Assert(_connHandler != null, "SqlConnectionInternalTds handler can not be null at this point."); - _connHandler.TimeoutErrorInternal.EndPhase(SqlConnectionTimeoutErrorPhase.LoginBegin); - _connHandler.TimeoutErrorInternal.SetAndBeginPhase(SqlConnectionTimeoutErrorPhase.ProcessConnectionAuth); - - // get the password up front to use in sspi logic below - byte[] encryptedPassword = null; - byte[] encryptedChangePassword = null; - int encryptedPasswordLengthInBytes; - int encryptedChangePasswordLengthInBytes; - bool useFeatureExt = (requestedFeatures != TdsEnums.FeatureExtension.None); - - string userName; - - if (rec.credential != null) - { - userName = rec.credential.UserId; - encryptedPasswordLengthInBytes = rec.credential.Password.Length * 2; - } - else - { - userName = rec.userName; - encryptedPassword = TdsParserStaticMethods.ObfuscatePassword(rec.password); - encryptedPasswordLengthInBytes = encryptedPassword.Length; // password in clear text is already encrypted and its length is in byte - } - - if (rec.newSecurePassword != null) - { - encryptedChangePasswordLengthInBytes = rec.newSecurePassword.Length * 2; - } - else - { - encryptedChangePassword = TdsParserStaticMethods.ObfuscatePassword(rec.newPassword); - encryptedChangePasswordLengthInBytes = encryptedChangePassword.Length; - } - - // set the message type - _physicalStateObj._outputMessageType = TdsEnums.MT_LOGIN7; - - // length in bytes - int length = TdsEnums.SQL2005_LOG_REC_FIXED_LEN; - - string clientInterfaceName = TdsEnums.SQL_PROVIDER_NAME; - Debug.Assert(TdsEnums.MAXLEN_CLIENTINTERFACE >= clientInterfaceName.Length, "cchCltIntName can specify at most 128 unicode characters. See Tds spec"); - - // add up variable-len portions (multiply by 2 for byte len of char strings) - // - checked - { - length += (rec.hostName.Length + rec.applicationName.Length + - rec.serverName.Length + clientInterfaceName.Length + - rec.language.Length + rec.database.Length + - rec.attachDBFilename.Length) * 2; - if (useFeatureExt) - { - length += 4; - } - } - - // allocate memory for SSPI variables - byte[] rentedSSPIBuff = null; - byte[] outSSPIBuff = null; - uint outSSPILength = 0; - - // only add lengths of password and username if not using SSPI or requesting federated authentication info - if (!rec.useSSPI && !(_connHandler._federatedAuthenticationInfoRequested || _connHandler._federatedAuthenticationRequested)) - { - checked - { - length += (userName.Length * 2) + encryptedPasswordLengthInBytes - + encryptedChangePasswordLengthInBytes; - } - } - else - { - if (rec.useSSPI) - { - // now allocate proper length of buffer, and set length - rentedSSPIBuff = ArrayPool.Shared.Rent((int)s_maxSSPILength); - outSSPIBuff = rentedSSPIBuff; - outSSPILength = s_maxSSPILength; - - // Call helper function for SSPI data and actual length. - // Since we don't have SSPI data from the server, send null for the - // byte[] buffer and 0 for the int length. - Debug.Assert(SniContext.Snix_Login == _physicalStateObj.SniContext, $"Unexpected SniContext. Expecting Snix_Login, actual value is '{_physicalStateObj.SniContext}'"); - _physicalStateObj.SniContext = SniContext.Snix_LoginSspi; - - SSPIData(null, 0, ref outSSPIBuff, ref outSSPILength); - - if (outSSPILength > int.MaxValue) - { - throw SQL.InvalidSSPIPacketSize(); // SqlBu 332503 - } - _physicalStateObj.SniContext = SniContext.Snix_Login; - - checked - { - length += (int)outSSPILength; - } - } - } - - int feOffset = length; - // calculate and reserve the required bytes for the featureEx - length = ApplyFeatureExData(requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData, useFeatureExt, length); - - WriteLoginData(rec, - requestedFeatures, - recoverySessionData, - fedAuthFeatureExtensionData, - encrypt, - encryptedPassword, - encryptedChangePassword, - encryptedPasswordLengthInBytes, - encryptedChangePasswordLengthInBytes, - useFeatureExt, - userName, - length, - feOffset, - clientInterfaceName, - outSSPIBuff, - outSSPILength); - - if (rentedSSPIBuff != null) - { - ArrayPool.Shared.Return(rentedSSPIBuff, clearArray: true); - } - - _physicalStateObj.WritePacket(TdsEnums.HARDFLUSH); - _physicalStateObj.ResetSecurePasswordsInformation(); - _physicalStateObj.HasPendingData = true; - _physicalStateObj._messageStatus = 0; - }// tdsLogin - private void WriteLoginData(SqlLogin rec, TdsEnums.FeatureExtension requestedFeatures, SessionData recoverySessionData, @@ -8617,81 +8458,6 @@ internal void SendFedAuthToken(SqlFedAuthToken fedAuthToken) _connHandler._federatedAuthenticationRequested = true; } - private void SSPIData(byte[] receivedBuff, uint receivedLength, ref byte[] sendBuff, ref uint sendLength) - { - if (TdsParserStateObjectFactory.UseManagedSNI) - { - try - { - _physicalStateObj.GenerateSspiClientContext(receivedBuff, receivedLength, ref sendBuff, ref sendLength, _sniSpnBuffer); - } - catch (Exception e) - { - SSPIError(e.Message + Environment.NewLine + e.StackTrace, TdsEnums.GEN_CLIENT_CONTEXT); - } - } - else - { - if (receivedBuff == null) - { - // if we do not have SSPI data coming from server, send over 0's for pointer and length - receivedLength = 0; - } - - // we need to respond to the server's message with SSPI data - if (0 != _physicalStateObj.GenerateSspiClientContext(receivedBuff, receivedLength, ref sendBuff, ref sendLength, _sniSpnBuffer)) - { - SSPIError(SQLMessage.SSPIGenerateError(), TdsEnums.GEN_CLIENT_CONTEXT); - } - } - } - - private void ProcessSSPI(int receivedLength) - { - SniContext outerContext = _physicalStateObj.SniContext; - _physicalStateObj.SniContext = SniContext.Snix_ProcessSspi; - // allocate received buffer based on length from SSPI message - byte[] receivedBuff = ArrayPool.Shared.Rent(receivedLength); - - // read SSPI data received from server - Debug.Assert(_physicalStateObj._syncOverAsync, "Should not attempt pends in a synchronous call"); - bool result = _physicalStateObj.TryReadByteArray(receivedBuff, receivedLength); - if (!result) - { - throw SQL.SynchronousCallMayNotPend(); - } - - // allocate send buffer and initialize length - byte[] rentedSendBuff = ArrayPool.Shared.Rent((int)s_maxSSPILength); - byte[] sendBuff = rentedSendBuff; - uint sendLength = s_maxSSPILength; - - // make call for SSPI data - SSPIData(receivedBuff, (uint)receivedLength, ref sendBuff, ref sendLength); - - // DO NOT SEND LENGTH - TDS DOC INCORRECT! JUST SEND SSPI DATA! - _physicalStateObj.WriteByteArray(sendBuff, (int)sendLength, 0); - - ArrayPool.Shared.Return(rentedSendBuff, clearArray: true); - ArrayPool.Shared.Return(receivedBuff, clearArray: true); - - // set message type so server knows its a SSPI response - _physicalStateObj._outputMessageType = TdsEnums.MT_SSPI; - - // send to server - _physicalStateObj.WritePacket(TdsEnums.HARDFLUSH); - _physicalStateObj.SniContext = outerContext; - } - - private void SSPIError(string error, string procedure) - { - Debug.Assert(!string.IsNullOrEmpty(procedure), "TdsParser.SSPIError called with an empty or null procedure string"); - Debug.Assert(!string.IsNullOrEmpty(error), "TdsParser.SSPIError called with an empty or null error string"); - - _physicalStateObj.AddError(new SqlError(0, (byte)0x00, (byte)TdsEnums.MIN_ERROR_CLASS, _server, error, procedure, 0)); - ThrowExceptionAndWarning(_physicalStateObj); - } - internal byte[] GetDTCAddress(int timeout, TdsParserStateObject stateObj) { // If this fails, the server will return a server error - Sameet Agarwal confirmed. @@ -9240,7 +9006,7 @@ internal Task TdsExecuteRPC(SqlCommand cmd, IList<_SqlRPC> rpcArray, int timeout { // Throw an exception if ForceColumnEncryption is set on a parameter and the ColumnEncryption is not enabled on SqlConnection or SqlCommand if ( - !(cmd.ColumnEncryptionSetting == SqlCommandColumnEncryptionSetting.Enabled + !(cmd.ColumnEncryptionSetting == SqlCommandColumnEncryptionSetting.Enabled || (cmd.ColumnEncryptionSetting == SqlCommandColumnEncryptionSetting.UseConnectionSetting && cmd.Connection.IsColumnEncryptionSettingEnabled))) { diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs index 6c32e9594d..3cb7ee2c94 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs @@ -51,7 +51,7 @@ internal TdsParserStateObject(TdsParser parser, TdsParserStateObject physicalCon AddError(parser.ProcessSNIError(this)); ThrowExceptionAndWarning(); } - + // we post a callback that represents the call to dispose; once the // object is disposed, the next callback will cause the GC Handle to // be released. @@ -64,6 +64,7 @@ internal TdsParserStateObject(TdsParser parser, TdsParserStateObject physicalCon //////////////// internal abstract uint DisableSsl(); + internal abstract SSPIContextProvider CreateSSPIContextProvider(); internal abstract uint EnableMars(ref uint info); @@ -72,6 +73,8 @@ internal abstract uint Status get; } + internal abstract Guid? SessionId { get; } + internal abstract SessionHandle SessionHandle { get; @@ -253,8 +256,6 @@ internal abstract void CreatePhysicalSNIHandle( protected abstract void RemovePacketFromPendingList(PacketHandle pointer); - internal abstract uint GenerateSspiClientContext(byte[] receivedBuff, uint receivedLength, ref byte[] sendBuff, ref uint sendLength, byte[][] _sniSpnBuffer); - internal int DecrementPendingCallbacks(bool release) { int remaining = Interlocked.Decrement(ref _pendingCallbacks); diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs index 1e0141dd58..7d879ae4d5 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs @@ -21,11 +21,7 @@ internal sealed class TdsParserStateObjectManaged : TdsParserStateObject { private SNIMarsConnection? _marsConnection; private SNIHandle? _sessionHandle; -#if NET7_0_OR_GREATER - private NegotiateAuthentication? _negotiateAuth = null; -#else - private SspiClientContextStatus? _sspiClientContextStatus; -#endif + public TdsParserStateObjectManaged(TdsParser parser) : base(parser) { } internal TdsParserStateObjectManaged(TdsParser parser, TdsParserStateObject physicalConnection, bool async) : @@ -232,6 +228,8 @@ internal override PacketHandle ReadSyncOverAsync(int timeoutRemaining, out uint protected override PacketHandle EmptyReadPacket => PacketHandle.FromManagedPacket(null); + internal override Guid? SessionId => _sessionHandle?.ConnectionId; + internal override bool IsPacketEmpty(PacketHandle packet) => packet.ManagedPacket == null; internal override void ReleasePacket(PacketHandle syncReadPacket) @@ -389,30 +387,6 @@ internal override uint SetConnectionBufferSize(ref uint unsignedPacketSize) return TdsEnums.SNI_SUCCESS; } - internal override uint GenerateSspiClientContext(byte[] receivedBuff, - uint receivedLength, - ref byte[] sendBuff, - ref uint sendLength, - byte[][] _sniSpnBuffer) - { -#if NET7_0_OR_GREATER - _negotiateAuth ??= new(new NegotiateAuthenticationClientOptions { Package = "Negotiate", TargetName = Encoding.Unicode.GetString(_sniSpnBuffer[0]) }); - sendBuff = _negotiateAuth.GetOutgoingBlob(receivedBuff, out NegotiateAuthenticationStatusCode statusCode)!; - SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObjectManaged.GenerateSspiClientContext | Info | Session Id {0}, StatusCode={1}", _sessionHandle?.ConnectionId, statusCode); - if (statusCode is not NegotiateAuthenticationStatusCode.Completed and not NegotiateAuthenticationStatusCode.ContinueNeeded) - { - throw new InvalidOperationException(SQLMessage.SSPIGenerateError() + Environment.NewLine + statusCode); - } -#else - _sspiClientContextStatus ??= new SspiClientContextStatus(); - - SNIProxy.GenSspiClientContext(_sspiClientContextStatus, receivedBuff, ref sendBuff, _sniSpnBuffer); - SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObjectManaged.GenerateSspiClientContext | Info | Session Id {0}", _sessionHandle?.ConnectionId); -#endif - sendLength = (uint)(sendBuff != null ? sendBuff.Length : 0); - return 0; - } - internal override uint WaitForSSLHandShakeToComplete(out int protocolVersion) { protocolVersion = GetSessionSNIHandleHandleOrThrow().ProtocolVersion; @@ -432,5 +406,12 @@ private SNIHandle GetSessionSNIHandleHandleOrThrow() [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] // this forces the exception throwing code not to be inlined for performance private void ThrowClosedConnection() => throw ADP.ClosedConnectionError(); + + internal override SSPIContextProvider CreateSSPIContextProvider() +#if NET7_0_OR_GREATER + => new NegotiateSSPIContextProvider(); +#else + => new ManagedSSPIContextProvider(); +#endif } } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs index 59776956a1..f66f581523 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs @@ -167,7 +167,7 @@ internal override void CreatePhysicalSNIHandle( byte[] srvSPN = Encoding.Unicode.GetBytes(serverSPN); Trace.Assert(srvSPN.Length <= SNINativeMethodWrapper.SniMaxComposedSpnLength, "Length of the provided SPN exceeded the buffer size."); spnBuffer[0] = srvSPN; - SqlClientEventSource.Log.TryTraceEvent("<{0}.{1}|SEC> Server SPN `{2}` from the connection string is used.",nameof(TdsParserStateObjectNative), nameof(CreatePhysicalSNIHandle), serverSPN); + SqlClientEventSource.Log.TryTraceEvent("<{0}.{1}|SEC> Server SPN `{2}` from the connection string is used.", nameof(TdsParserStateObjectNative), nameof(CreatePhysicalSNIHandle), serverSPN); } else { @@ -272,6 +272,8 @@ internal override PacketHandle ReadSyncOverAsync(int timeoutRemaining, out uint protected override PacketHandle EmptyReadPacket => PacketHandle.FromNativePointer(default); + internal override Guid? SessionId => default; + internal override bool IsPacketEmpty(PacketHandle readPacket) { Debug.Assert(readPacket.Type == PacketHandle.NativePointerType || readPacket.Type == 0, "unexpected packet type when requiring NativePointer"); @@ -398,9 +400,6 @@ internal override uint EnableSsl(ref uint info, bool tlsFirst, string serverCert internal override uint SetConnectionBufferSize(ref uint unsignedPacketSize) => SNINativeMethodWrapper.SNISetInfo(Handle, SNINativeMethodWrapper.QTypes.SNI_QUERY_CONN_BUFSIZE, ref unsignedPacketSize); - internal override uint GenerateSspiClientContext(byte[] receivedBuff, uint receivedLength, ref byte[] sendBuff, ref uint sendLength, byte[][] _sniSpnBuffer) - => SNINativeMethodWrapper.SNISecGenClientContext(Handle, receivedBuff, receivedLength, sendBuff, ref sendLength, _sniSpnBuffer[0]); - internal override uint WaitForSSLHandShakeToComplete(out int protocolVersion) { uint returnValue = SNINativeMethodWrapper.SNIWaitForSSLHandshakeToComplete(Handle, GetTimeoutRemaining(), out uint nativeProtocolVersion); @@ -451,6 +450,8 @@ internal override void DisposePacketCache() } } + internal override SSPIContextProvider CreateSSPIContextProvider() => new NativeSSPIContextProvider(); + internal sealed class WritePacketCache : IDisposable { private bool _disposed; 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 f17622f303..13e41d05b5 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -146,6 +146,21 @@ Microsoft\Data\ProviderBase\TimeoutTimer.cs + + Microsoft\Data\SqlClient\SSPI\ManagedSSPIContextProvider.cs + + + Microsoft\Data\SqlClient\SSPI\NativeSSPIContextProvider.cs + + + Microsoft\Data\SqlClient\SSPI\NegotiateSSPIContextProvider.cs + + + Microsoft\Data\SqlClient\SSPI\SSPIContextProvider.cs + + + Microsoft\Data\SqlClient\TdsParser.cs + Microsoft\Data\Sql\SqlDataSourceEnumerator.cs 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 7a9bbfdfd3..6d49a79051 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 @@ -28,14 +28,15 @@ namespace Microsoft.Data.SqlClient { - // The TdsParser Object controls reading/writing to the netlib, parsing the tds, // and surfacing objects to the user. - sealed internal class TdsParser + sealed internal partial class TdsParser { private static int _objectTypeCount; // EventSource Counter private readonly SqlClientLogger _logger = new SqlClientLogger(); + private SSPIContextProvider _authenticationProvider; + internal readonly int _objectID = System.Threading.Interlocked.Increment(ref _objectTypeCount); static Task completedTask; @@ -236,16 +237,9 @@ internal static void Assert(string message) // NIC address caching private static byte[] s_nicAddress; // cache the NIC address from the registry - // SSPI variables - private static bool s_fSSPILoaded = false; // bool to indicate whether library has been loaded - - private volatile static UInt32 s_maxSSPILength = 0; // variable to hold max SSPI data size, keep for token from server - // textptr sequence private static readonly byte[] s_longDataHeader = { 0x10, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; - private static object s_tdsParserLock = new object(); - // Various other statics private const int ATTENTION_TIMEOUT = 5000; // internal attention timeout, in ticks @@ -549,7 +543,8 @@ internal void Connect(ServerInfo serverInfo, // AD Integrated behaves like Windows integrated when connecting to a non-fedAuth server if (integratedSecurity || authType == SqlAuthenticationMethod.ActiveDirectoryIntegrated) { - LoadSSPILibrary(); + _authenticationProvider = _physicalStateObj.CreateSSPIContextProvider(); + if (!string.IsNullOrEmpty(serverInfo.ServerSPN)) { // Native SNI requires the Unicode encoding and any other encoding like UTF8 breaks the code. @@ -567,7 +562,9 @@ internal void Connect(ServerInfo serverInfo, } else { + _authenticationProvider = null; _sniSpnBuffer = null; + switch (authType) { case SqlAuthenticationMethod.ActiveDirectoryPassword: @@ -655,6 +652,8 @@ internal void Connect(ServerInfo serverInfo, FQDNforDNSCache, hostNameInCertificate); + _authenticationProvider?.Initialize(serverInfo, _physicalStateObj, this); + if (TdsEnums.SNI_SUCCESS != _physicalStateObj.Status) { _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); @@ -760,6 +759,8 @@ internal void Connect(ServerInfo serverInfo, serverInfo.ResolvedServerName, hostNameInCertificate); + _authenticationProvider?.Initialize(serverInfo, _physicalStateObj, this); + if (TdsEnums.SNI_SUCCESS != _physicalStateObj.Status) { _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); @@ -2547,7 +2548,7 @@ internal bool TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataRead (error.Class <= TdsEnums.MAX_USER_CORRECTABLE_ERROR_CLASS)) { // Fire SqlInfoMessage here - FireInfoMessageEvent(connection,cmdHandler, stateObj, error); + FireInfoMessageEvent(connection, cmdHandler, stateObj, error); } else { @@ -8904,205 +8905,6 @@ internal int WriteFedAuthFeatureRequest(FederatedAuthenticationFeatureExtensionD return len; } - internal void TdsLogin(SqlLogin rec, - TdsEnums.FeatureExtension requestedFeatures, - SessionData recoverySessionData, - FederatedAuthenticationFeatureExtensionData fedAuthFeatureExtensionData, - SqlClientOriginalNetworkAddressInfo originalNetworkAddressInfo, - SqlConnectionEncryptOption encrypt) - { - _physicalStateObj.SetTimeoutSeconds(rec.timeout); - - Debug.Assert(recoverySessionData == null || (requestedFeatures & TdsEnums.FeatureExtension.SessionRecovery) != 0, "Recovery session data without session recovery feature request"); - Debug.Assert(TdsEnums.MAXLEN_HOSTNAME >= rec.hostName.Length, "_workstationId.Length exceeds the max length for this value"); - - Debug.Assert(!(rec.useSSPI && _connHandler._fedAuthRequired), "Cannot use SSPI when server has responded 0x01 for FedAuthRequired PreLogin Option."); - Debug.Assert(!rec.useSSPI || (requestedFeatures & TdsEnums.FeatureExtension.FedAuth) == 0, "Cannot use both SSPI and FedAuth"); - Debug.Assert(fedAuthFeatureExtensionData == null || (requestedFeatures & TdsEnums.FeatureExtension.FedAuth) != 0, "fedAuthFeatureExtensionData provided without fed auth feature request"); - Debug.Assert(fedAuthFeatureExtensionData != null || (requestedFeatures & TdsEnums.FeatureExtension.FedAuth) == 0, "Fed Auth feature requested without specifying fedAuthFeatureExtensionData."); - - Debug.Assert(rec.userName == null || (rec.userName != null && TdsEnums.MAXLEN_CLIENTID >= rec.userName.Length), "_userID.Length exceeds the max length for this value"); - Debug.Assert(rec.credential == null || (rec.credential != null && TdsEnums.MAXLEN_CLIENTID >= rec.credential.UserId.Length), "_credential.UserId.Length exceeds the max length for this value"); - - Debug.Assert(rec.password == null || (rec.password != null && TdsEnums.MAXLEN_CLIENTSECRET >= rec.password.Length), "_password.Length exceeds the max length for this value"); - Debug.Assert(rec.credential == null || (rec.credential != null && TdsEnums.MAXLEN_CLIENTSECRET >= rec.credential.Password.Length), "_credential.Password.Length exceeds the max length for this value"); - - Debug.Assert(rec.credential != null || rec.userName != null || rec.password != null, "cannot mix the new secure password system and the connection string based password"); - Debug.Assert(rec.newSecurePassword != null || rec.newPassword != null, "cannot have both new secure change password and string based change password"); - Debug.Assert(TdsEnums.MAXLEN_APPNAME >= rec.applicationName.Length, "_applicationName.Length exceeds the max length for this value"); - Debug.Assert(TdsEnums.MAXLEN_SERVERNAME >= rec.serverName.Length, "_dataSource.Length exceeds the max length for this value"); - Debug.Assert(TdsEnums.MAXLEN_LANGUAGE >= rec.language.Length, "_currentLanguage .Length exceeds the max length for this value"); - Debug.Assert(TdsEnums.MAXLEN_DATABASE >= rec.database.Length, "_initialCatalog.Length exceeds the max length for this value"); - Debug.Assert(TdsEnums.MAXLEN_ATTACHDBFILE >= rec.attachDBFilename.Length, "_attachDBFileName.Length exceeds the max length for this value"); - - Debug.Assert(_connHandler != null, "SqlConnectionInternalTds handler can not be null at this point."); - _connHandler.TimeoutErrorInternal.EndPhase(SqlConnectionTimeoutErrorPhase.LoginBegin); - _connHandler.TimeoutErrorInternal.SetAndBeginPhase(SqlConnectionTimeoutErrorPhase.ProcessConnectionAuth); - - // Add CTAIP Provider - // - if (originalNetworkAddressInfo != null) - { - SNINativeMethodWrapper.CTAIPProviderInfo cauthInfo = new SNINativeMethodWrapper.CTAIPProviderInfo(); - cauthInfo.originalNetworkAddress = originalNetworkAddressInfo.Address.GetAddressBytes(); - cauthInfo.fromDataSecurityProxy = originalNetworkAddressInfo.IsFromDataSecurityProxy; - - UInt32 error = SNINativeMethodWrapper.SNIAddProvider(_physicalStateObj.Handle, SNINativeMethodWrapper.ProviderEnum.CTAIP_PROV, cauthInfo); - if (error != TdsEnums.SNI_SUCCESS) - { - _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); - ThrowExceptionAndWarning(_physicalStateObj); - } - - try - { } // EmptyTry/Finally to avoid FXCop violation - finally - { - _physicalStateObj.ClearAllWritePackets(); - } - } - - // get the password up front to use in sspi logic below - byte[] encryptedPassword = null; - byte[] encryptedChangePassword = null; - int encryptedPasswordLengthInBytes; - int encryptedChangePasswordLengthInBytes; - bool useFeatureExt = (requestedFeatures != TdsEnums.FeatureExtension.None); - - string userName; - - if (rec.credential != null) - { - userName = rec.credential.UserId; - encryptedPasswordLengthInBytes = rec.credential.Password.Length * 2; - } - else - { - userName = rec.userName; - encryptedPassword = TdsParserStaticMethods.ObfuscatePassword(rec.password); - encryptedPasswordLengthInBytes = encryptedPassword.Length; // password in clear text is already encrypted and its length is in byte - } - - if (rec.newSecurePassword != null) - { - encryptedChangePasswordLengthInBytes = rec.newSecurePassword.Length * 2; - } - else - { - encryptedChangePassword = TdsParserStaticMethods.ObfuscatePassword(rec.newPassword); - encryptedChangePasswordLengthInBytes = encryptedChangePassword.Length; - } - - - // set the message type - _physicalStateObj._outputMessageType = TdsEnums.MT_LOGIN7; - - // length in bytes - int length = TdsEnums.SQL2005_LOG_REC_FIXED_LEN; - - string clientInterfaceName = TdsEnums.SQL_PROVIDER_NAME; - Debug.Assert(TdsEnums.MAXLEN_CLIENTINTERFACE >= clientInterfaceName.Length, "cchCltIntName can specify at most 128 unicode characters. See Tds spec"); - - // add up variable-len portions (multiply by 2 for byte len of char strings) - // - checked - { - length += (rec.hostName.Length + rec.applicationName.Length + - rec.serverName.Length + clientInterfaceName.Length + - rec.language.Length + rec.database.Length + - rec.attachDBFilename.Length) * 2; - if (useFeatureExt) - { - length += 4; - } - } - - // allocate memory for SSPI variables - byte[] outSSPIBuff = null; - UInt32 outSSPILength = 0; - - // only add lengths of password and username if not using SSPI or requesting federated authentication info - if (!rec.useSSPI && !(_connHandler._federatedAuthenticationInfoRequested || _connHandler._federatedAuthenticationRequested)) - { - checked - { - length += (userName.Length * 2) + encryptedPasswordLengthInBytes - + encryptedChangePasswordLengthInBytes; - } - } - else - { - if (rec.useSSPI) - { - // now allocate proper length of buffer, and set length - outSSPIBuff = new byte[s_maxSSPILength]; - outSSPILength = s_maxSSPILength; - - // Call helper function for SSPI data and actual length. - // Since we don't have SSPI data from the server, send null for the - // byte[] buffer and 0 for the int length. - Debug.Assert(SniContext.Snix_Login == _physicalStateObj.SniContext, $"Unexpected SniContext. Expecting Snix_Login, actual value is '{_physicalStateObj.SniContext}'"); - _physicalStateObj.SniContext = SniContext.Snix_LoginSspi; - SSPIData(null, 0, outSSPIBuff, ref outSSPILength); - if (outSSPILength > int.MaxValue) - { - throw SQL.InvalidSSPIPacketSize(); // SqlBu 332503 - } - _physicalStateObj.SniContext = SniContext.Snix_Login; - - checked - { - length += (int)outSSPILength; - } - } - } - - int feOffset = length; - // calculate and reserve the required bytes for the featureEx - length = ApplyFeatureExData(requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData, useFeatureExt, length); - - WriteLoginData(rec, - requestedFeatures, - recoverySessionData, - fedAuthFeatureExtensionData, - encrypt, - encryptedPassword, - encryptedChangePassword, - encryptedPasswordLengthInBytes, - encryptedChangePasswordLengthInBytes, - useFeatureExt, - userName, - length, - feOffset, - clientInterfaceName, - outSSPIBuff, - outSSPILength); - - _physicalStateObj.WritePacket(TdsEnums.HARDFLUSH); - _physicalStateObj.ResetSecurePasswordsInformation(); // Password information is needed only from Login process; done with writing login packet and should clear information - _physicalStateObj.HasPendingData = true; - _physicalStateObj._messageStatus = 0; - - // Remvove CTAIP Provider after login record is sent. - // - if (originalNetworkAddressInfo != null) - { - UInt32 error = SNINativeMethodWrapper.SNIRemoveProvider(_physicalStateObj.Handle, SNINativeMethodWrapper.ProviderEnum.CTAIP_PROV); - if (error != TdsEnums.SNI_SUCCESS) - { - _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); - ThrowExceptionAndWarning(_physicalStateObj); - } - - try - { } // EmptyTry/Finally to avoid FXCop violation - finally - { - _physicalStateObj.ClearAllWritePackets(); - } - } - }// tdsLogin - private void WriteLoginData(SqlLogin rec, TdsEnums.FeatureExtension requestedFeatures, SessionData recoverySessionData, @@ -9472,94 +9274,6 @@ internal void SendFedAuthToken(SqlFedAuthToken fedAuthToken) _connHandler._federatedAuthenticationRequested = true; } - private void SSPIData(byte[] receivedBuff, UInt32 receivedLength, byte[] sendBuff, ref UInt32 sendLength) - { - SNISSPIData(receivedBuff, receivedLength, sendBuff, ref sendLength); - } - - private void SNISSPIData(byte[] receivedBuff, UInt32 receivedLength, byte[] sendBuff, ref UInt32 sendLength) - { - if (receivedBuff == null) - { - // we do not have SSPI data coming from server, so send over 0's for pointer and length - receivedLength = 0; - } - // we need to respond to the server's message with SSPI data - if (0 != SNINativeMethodWrapper.SNISecGenClientContext(_physicalStateObj.Handle, receivedBuff, receivedLength, sendBuff, ref sendLength, _sniSpnBuffer)) - { - SSPIError(SQLMessage.SSPIGenerateError(), TdsEnums.GEN_CLIENT_CONTEXT); - } - } - - - private void ProcessSSPI(int receivedLength) - { - SniContext outerContext = _physicalStateObj.SniContext; - _physicalStateObj.SniContext = SniContext.Snix_ProcessSspi; - // allocate received buffer based on length from SSPI message - byte[] receivedBuff = new byte[receivedLength]; - - // read SSPI data received from server - Debug.Assert(_physicalStateObj._syncOverAsync, "Should not attempt pends in a synchronous call"); - bool result = _physicalStateObj.TryReadByteArray(receivedBuff, receivedLength); - if (!result) - { throw SQL.SynchronousCallMayNotPend(); } - - // allocate send buffer and initialize length - byte[] sendBuff = new byte[s_maxSSPILength]; - UInt32 sendLength = s_maxSSPILength; - - // make call for SSPI data - SSPIData(receivedBuff, (UInt32)receivedLength, sendBuff, ref sendLength); - - // DO NOT SEND LENGTH - TDS DOC INCORRECT! JUST SEND SSPI DATA! - _physicalStateObj.WriteByteArray(sendBuff, (int)sendLength, 0); - - // set message type so server knows its a SSPI response - _physicalStateObj._outputMessageType = TdsEnums.MT_SSPI; - - // send to server - _physicalStateObj.WritePacket(TdsEnums.HARDFLUSH); - _physicalStateObj.SniContext = outerContext; - } - - private void SSPIError(string error, string procedure) - { - Debug.Assert(!ADP.IsEmpty(procedure), "TdsParser.SSPIError called with an empty or null procedure string"); - Debug.Assert(!ADP.IsEmpty(error), "TdsParser.SSPIError called with an empty or null error string"); - - _physicalStateObj.AddError(new SqlError(0, (byte)0x00, (byte)TdsEnums.MIN_ERROR_CLASS, _server, error, procedure, 0)); - ThrowExceptionAndWarning(_physicalStateObj); - } - - private void LoadSSPILibrary() - { - // Outer check so we don't acquire lock once once it's loaded. - if (!s_fSSPILoaded) - { - lock (s_tdsParserLock) - { - // re-check inside lock - if (!s_fSSPILoaded) - { - // use local for ref param to defer setting s_maxSSPILength until we know the call succeeded. - UInt32 maxLength = 0; - if (0 != SNINativeMethodWrapper.SNISecInitPackage(ref maxLength)) - SSPIError(SQLMessage.SSPIInitializeError(), TdsEnums.INIT_SSPI_PACKAGE); - - s_maxSSPILength = maxLength; - s_fSSPILoaded = true; - - } - } - } - - if (s_maxSSPILength > Int32.MaxValue) - { - throw SQL.InvalidSSPIPacketSize(); // SqlBu 332503 - } - } - internal byte[] GetDTCAddress(int timeout, TdsParserStateObject stateObj) { // If this fails, the server will return a server error - Sameet Agarwal confirmed. @@ -10123,7 +9837,7 @@ internal Task TdsExecuteRPC(SqlCommand cmd, IList<_SqlRPC> rpcArray, int timeout { // Throw an exception if ForceColumnEncryption is set on a parameter and the ColumnEncryption is not enabled on SqlConnection or SqlCommand if ( - !(cmd.ColumnEncryptionSetting == SqlCommandColumnEncryptionSetting.Enabled + !(cmd.ColumnEncryptionSetting == SqlCommandColumnEncryptionSetting.Enabled || (cmd.ColumnEncryptionSetting == SqlCommandColumnEncryptionSetting.UseConnectionSetting && cmd.Connection.IsColumnEncryptionSettingEnabled)) ) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.netfx.cs index 7c7b9933db..ae8cd1070b 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.netfx.cs @@ -80,6 +80,8 @@ internal TdsParserStateObject(TdsParser parser, SNIHandle physicalConnection, bo _lastSuccessfulIOTimer = parser._physicalStateObj._lastSuccessfulIOTimer; } + internal SSPIContextProvider CreateSSPIContextProvider() => new NativeSSPIContextProvider(); + //////////////// // Properties // //////////////// @@ -90,7 +92,7 @@ internal SNIHandle Handle return _sessionHandle; } } - + internal uint Status { get diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/ManagedSSPIContextProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/ManagedSSPIContextProvider.cs new file mode 100644 index 0000000000..25fb2cadae --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/ManagedSSPIContextProvider.cs @@ -0,0 +1,23 @@ +#if !NETFRAMEWORK && !NET7_0_OR_GREATER + +using Microsoft.Data.SqlClient.SNI; + +#nullable enable + +namespace Microsoft.Data.SqlClient +{ + internal sealed class ManagedSSPIContextProvider : SSPIContextProvider + { + private SspiClientContextStatus? _sspiClientContextStatus; + + internal override void GenerateSspiClientContext(byte[] receivedBuff, uint receivedLength, ref byte[] sendBuff, ref uint sendLength, byte[][] _sniSpnBuffer) + { + _sspiClientContextStatus ??= new SspiClientContextStatus(); + + SNIProxy.GenSspiClientContext(_sspiClientContextStatus, receivedBuff, ref sendBuff, _sniSpnBuffer); + SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObjectManaged.GenerateSspiClientContext | Info | Session Id {0}", _physicalStateObj.SessionId); + sendLength = (uint)(sendBuff != null ? sendBuff.Length : 0); + } + } +} +#endif diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NativeSSPIContextProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NativeSSPIContextProvider.cs new file mode 100644 index 0000000000..718608d136 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NativeSSPIContextProvider.cs @@ -0,0 +1,68 @@ +using System; +using System.Diagnostics; + +#nullable enable + +namespace Microsoft.Data.SqlClient +{ + internal sealed class NativeSSPIContextProvider : SSPIContextProvider + { + private static readonly object s_tdsParserLock = new(); + + // bool to indicate whether library has been loaded + private static bool s_fSSPILoaded; + + // variable to hold max SSPI data size, keep for token from server + private volatile static uint s_maxSSPILength; + + internal override uint MaxSSPILength => s_maxSSPILength; + + private protected override void Initialize() + { + LoadSSPILibrary(); + } + + private void LoadSSPILibrary() + { + // Outer check so we don't acquire lock once it's loaded. + if (!s_fSSPILoaded) + { + lock (s_tdsParserLock) + { + // re-check inside lock + if (!s_fSSPILoaded) + { + // use local for ref param to defer setting s_maxSSPILength until we know the call succeeded. + uint maxLength = 0; + + if (0 != SNINativeMethodWrapper.SNISecInitPackage(ref maxLength)) + SSPIError(SQLMessage.SSPIInitializeError(), TdsEnums.INIT_SSPI_PACKAGE); + + s_maxSSPILength = maxLength; + s_fSSPILoaded = true; + } + } + } + + if (s_maxSSPILength > int.MaxValue) + { + throw SQL.InvalidSSPIPacketSize(); // SqlBu 332503 + } + } + + internal override void GenerateSspiClientContext(byte[] receivedBuff, uint receivedLength, ref byte[] sendBuff, ref uint sendLength, byte[][] _sniSpnBuffer) + { +#if NETFRAMEWORK + SNIHandle handle = _physicalStateObj.Handle; +#else + Debug.Assert(_physicalStateObj.SessionHandle.Type == SessionHandle.NativeHandleType); + SNIHandle handle = _physicalStateObj.SessionHandle.NativeHandle; +#endif + if (0 != SNINativeMethodWrapper.SNISecGenClientContext(handle, receivedBuff, receivedLength, sendBuff, ref sendLength, _sniSpnBuffer[0])) + { + throw new InvalidOperationException(SQLMessage.SSPIGenerateError()); + } + } + } +} + diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NegotiateSSPIContextProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NegotiateSSPIContextProvider.cs new file mode 100644 index 0000000000..bc11cf29c9 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/NegotiateSSPIContextProvider.cs @@ -0,0 +1,28 @@ +#if NET7_0_OR_GREATER + +using System; +using System.Text; +using System.Net.Security; + +#nullable enable + +namespace Microsoft.Data.SqlClient +{ + internal sealed class NegotiateSSPIContextProvider : SSPIContextProvider + { + private NegotiateAuthentication? _negotiateAuth = null; + + internal override void GenerateSspiClientContext(byte[] receivedBuff, uint receivedLength, ref byte[] sendBuff, ref uint sendLength, byte[][] _sniSpnBuffer) + { + _negotiateAuth ??= new(new NegotiateAuthenticationClientOptions { Package = "Negotiate", TargetName = Encoding.Unicode.GetString(_sniSpnBuffer[0]) }); + sendBuff = _negotiateAuth.GetOutgoingBlob(receivedBuff, out NegotiateAuthenticationStatusCode statusCode)!; + SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObjectManaged.GenerateSspiClientContext | Info | Session Id {0}, StatusCode={1}", _physicalStateObj.SessionId, statusCode); + if (statusCode is not NegotiateAuthenticationStatusCode.Completed and not NegotiateAuthenticationStatusCode.ContinueNeeded) + { + throw new InvalidOperationException(SQLMessage.SSPIGenerateError() + Environment.NewLine + statusCode); + } + sendLength = (uint)(sendBuff != null ? sendBuff.Length : 0); + } + } +} +#endif diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SSPIContextProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SSPIContextProvider.cs new file mode 100644 index 0000000000..fa8c27e3ab --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SSPI/SSPIContextProvider.cs @@ -0,0 +1,56 @@ +using System; +using System.Diagnostics; +using Microsoft.Data.Common; + +#nullable enable + +namespace Microsoft.Data.SqlClient +{ + internal abstract class SSPIContextProvider + { + private TdsParser _parser = null!; + private ServerInfo _serverInfo = null!; + private protected TdsParserStateObject _physicalStateObj = null!; + + internal virtual uint MaxSSPILength => 4096; // TODO: what is a good default here? + + internal void Initialize(ServerInfo serverInfo, TdsParserStateObject physicalStateObj, TdsParser parser) + { + _parser = parser; + _physicalStateObj = physicalStateObj; + _serverInfo = serverInfo; + + Initialize(); + } + + private protected virtual void Initialize() + { + } + + internal abstract void GenerateSspiClientContext(byte[] receivedBuff, uint receivedLength, ref byte[] sendBuff, ref uint sendLength, byte[][] _sniSpnBuffer); + + internal void SSPIData(byte[] receivedBuff, UInt32 receivedLength, ref byte[] sendBuff, ref UInt32 sendLength, byte[] sniSpnBuffer) + => SSPIData(receivedBuff, receivedLength, ref sendBuff, ref sendLength, new[] { sniSpnBuffer }); + + internal void SSPIData(byte[] receivedBuff, UInt32 receivedLength, ref byte[] sendBuff, ref UInt32 sendLength, byte[][] sniSpnBuffer) + { + try + { + GenerateSspiClientContext(receivedBuff, receivedLength, ref sendBuff, ref sendLength, sniSpnBuffer); + } + catch (Exception e) + { + SSPIError(e.Message + Environment.NewLine + e.StackTrace, TdsEnums.GEN_CLIENT_CONTEXT); + } + } + + protected void SSPIError(string error, string procedure) + { + Debug.Assert(!ADP.IsEmpty(procedure), "TdsParser.SSPIError called with an empty or null procedure string"); + Debug.Assert(!ADP.IsEmpty(error), "TdsParser.SSPIError called with an empty or null error string"); + + _physicalStateObj.AddError(new SqlError(0, (byte)0x00, (byte)TdsEnums.MIN_ERROR_CLASS, _serverInfo.ResolvedServerName, error, procedure, 0)); + _parser.ThrowExceptionAndWarning(_physicalStateObj); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs new file mode 100644 index 0000000000..4366b4665a --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -0,0 +1,265 @@ +using System; +using System.Buffers; +using System.Diagnostics; + +#nullable enable + +namespace Microsoft.Data.SqlClient +{ + internal partial class TdsParser + { + internal void ProcessSSPI(int receivedLength) + { + Debug.Assert(_authenticationProvider is not null); + + SniContext outerContext = _physicalStateObj.SniContext; + _physicalStateObj.SniContext = SniContext.Snix_ProcessSspi; + // allocate received buffer based on length from SSPI message + byte[] receivedBuff = ArrayPool.Shared.Rent(receivedLength); + + // read SSPI data received from server + Debug.Assert(_physicalStateObj._syncOverAsync, "Should not attempt pends in a synchronous call"); + bool result = _physicalStateObj.TryReadByteArray(receivedBuff, receivedLength); + if (!result) + { + throw SQL.SynchronousCallMayNotPend(); + } + + // allocate send buffer and initialize length + byte[] rentedSendBuff = ArrayPool.Shared.Rent((int)_authenticationProvider!.MaxSSPILength); + byte[] sendBuff = rentedSendBuff; // need to track these separately in case someone updates the ref parameter + uint sendLength = _authenticationProvider.MaxSSPILength; + + // make call for SSPI data + _authenticationProvider.SSPIData(receivedBuff, (uint)receivedLength, ref sendBuff, ref sendLength, _sniSpnBuffer); + + // DO NOT SEND LENGTH - TDS DOC INCORRECT! JUST SEND SSPI DATA! + _physicalStateObj.WriteByteArray(sendBuff, (int)sendLength, 0); + + ArrayPool.Shared.Return(rentedSendBuff, clearArray: true); + ArrayPool.Shared.Return(receivedBuff, clearArray: true); + + // set message type so server knows its a SSPI response + _physicalStateObj._outputMessageType = TdsEnums.MT_SSPI; + + // send to server + _physicalStateObj.WritePacket(TdsEnums.HARDFLUSH); + _physicalStateObj.SniContext = outerContext; + } + +#nullable disable + + internal void TdsLogin( + SqlLogin rec, + TdsEnums.FeatureExtension requestedFeatures, + SessionData recoverySessionData, + FederatedAuthenticationFeatureExtensionData fedAuthFeatureExtensionData, +#if NETFRAMEWORK + SqlClientOriginalNetworkAddressInfo originalNetworkAddressInfo, +#endif + SqlConnectionEncryptOption encrypt) + { + _physicalStateObj.SetTimeoutSeconds(rec.timeout); + + Debug.Assert(recoverySessionData == null || (requestedFeatures & TdsEnums.FeatureExtension.SessionRecovery) != 0, "Recovery session data without session recovery feature request"); + Debug.Assert(TdsEnums.MAXLEN_HOSTNAME >= rec.hostName.Length, "_workstationId.Length exceeds the max length for this value"); + + Debug.Assert(!(rec.useSSPI && _connHandler._fedAuthRequired), "Cannot use SSPI when server has responded 0x01 for FedAuthRequired PreLogin Option."); + Debug.Assert(!rec.useSSPI || (requestedFeatures & TdsEnums.FeatureExtension.FedAuth) == 0, "Cannot use both SSPI and FedAuth"); + Debug.Assert(fedAuthFeatureExtensionData == null || (requestedFeatures & TdsEnums.FeatureExtension.FedAuth) != 0, "fedAuthFeatureExtensionData provided without fed auth feature request"); + Debug.Assert(fedAuthFeatureExtensionData != null || (requestedFeatures & TdsEnums.FeatureExtension.FedAuth) == 0, "Fed Auth feature requested without specifying fedAuthFeatureExtensionData."); + + Debug.Assert(rec.userName == null || (rec.userName != null && TdsEnums.MAXLEN_CLIENTID >= rec.userName.Length), "_userID.Length exceeds the max length for this value"); + Debug.Assert(rec.credential == null || (rec.credential != null && TdsEnums.MAXLEN_CLIENTID >= rec.credential.UserId.Length), "_credential.UserId.Length exceeds the max length for this value"); + + Debug.Assert(rec.password == null || (rec.password != null && TdsEnums.MAXLEN_CLIENTSECRET >= rec.password.Length), "_password.Length exceeds the max length for this value"); + Debug.Assert(rec.credential == null || (rec.credential != null && TdsEnums.MAXLEN_CLIENTSECRET >= rec.credential.Password.Length), "_credential.Password.Length exceeds the max length for this value"); + + Debug.Assert(rec.credential != null || rec.userName != null || rec.password != null, "cannot mix the new secure password system and the connection string based password"); + Debug.Assert(rec.newSecurePassword != null || rec.newPassword != null, "cannot have both new secure change password and string based change password"); + Debug.Assert(TdsEnums.MAXLEN_APPNAME >= rec.applicationName.Length, "_applicationName.Length exceeds the max length for this value"); + Debug.Assert(TdsEnums.MAXLEN_SERVERNAME >= rec.serverName.Length, "_dataSource.Length exceeds the max length for this value"); + Debug.Assert(TdsEnums.MAXLEN_LANGUAGE >= rec.language.Length, "_currentLanguage .Length exceeds the max length for this value"); + Debug.Assert(TdsEnums.MAXLEN_DATABASE >= rec.database.Length, "_initialCatalog.Length exceeds the max length for this value"); + Debug.Assert(TdsEnums.MAXLEN_ATTACHDBFILE >= rec.attachDBFilename.Length, "_attachDBFileName.Length exceeds the max length for this value"); + + Debug.Assert(_connHandler != null, "SqlConnectionInternalTds handler can not be null at this point."); + _connHandler!.TimeoutErrorInternal.EndPhase(SqlConnectionTimeoutErrorPhase.LoginBegin); + _connHandler.TimeoutErrorInternal.SetAndBeginPhase(SqlConnectionTimeoutErrorPhase.ProcessConnectionAuth); + +#if NETFRAMEWORK + // Add CTAIP Provider + // + if (originalNetworkAddressInfo != null) + { + SNINativeMethodWrapper.CTAIPProviderInfo cauthInfo = new SNINativeMethodWrapper.CTAIPProviderInfo(); + cauthInfo.originalNetworkAddress = originalNetworkAddressInfo.Address.GetAddressBytes(); + cauthInfo.fromDataSecurityProxy = originalNetworkAddressInfo.IsFromDataSecurityProxy; + + UInt32 error = SNINativeMethodWrapper.SNIAddProvider(_physicalStateObj.Handle, SNINativeMethodWrapper.ProviderEnum.CTAIP_PROV, cauthInfo); + if (error != TdsEnums.SNI_SUCCESS) + { + _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); + ThrowExceptionAndWarning(_physicalStateObj); + } + + try + { } // EmptyTry/Finally to avoid FXCop violation + finally + { + _physicalStateObj.ClearAllWritePackets(); + } + } +#endif + + // get the password up front to use in sspi logic below + byte[] encryptedPassword = null; + byte[] encryptedChangePassword = null; + int encryptedPasswordLengthInBytes; + int encryptedChangePasswordLengthInBytes; + bool useFeatureExt = (requestedFeatures != TdsEnums.FeatureExtension.None); + + string userName; + + if (rec.credential != null) + { + userName = rec.credential.UserId; + encryptedPasswordLengthInBytes = rec.credential.Password.Length * 2; + } + else + { + userName = rec.userName; + encryptedPassword = TdsParserStaticMethods.ObfuscatePassword(rec.password); + encryptedPasswordLengthInBytes = encryptedPassword.Length; // password in clear text is already encrypted and its length is in byte + } + + if (rec.newSecurePassword != null) + { + encryptedChangePasswordLengthInBytes = rec.newSecurePassword.Length * 2; + } + else + { + encryptedChangePassword = TdsParserStaticMethods.ObfuscatePassword(rec.newPassword); + encryptedChangePasswordLengthInBytes = encryptedChangePassword.Length; + } + + // set the message type + _physicalStateObj._outputMessageType = TdsEnums.MT_LOGIN7; + + // length in bytes + int length = TdsEnums.SQL2005_LOG_REC_FIXED_LEN; + + string clientInterfaceName = TdsEnums.SQL_PROVIDER_NAME; + Debug.Assert(TdsEnums.MAXLEN_CLIENTINTERFACE >= clientInterfaceName.Length, "cchCltIntName can specify at most 128 unicode characters. See Tds spec"); + + // add up variable-len portions (multiply by 2 for byte len of char strings) + // + checked + { + length += (rec.hostName.Length + rec.applicationName.Length + + rec.serverName.Length + clientInterfaceName.Length + + rec.language.Length + rec.database.Length + + rec.attachDBFilename.Length) * 2; + if (useFeatureExt) + { + length += 4; + } + } + + // allocate memory for SSPI variables + byte[] rentedSSPIBuff = null; + byte[] outSSPIBuff = null; // track the rented buffer as a separate variable in case it is updated via the ref parameter + uint outSSPILength = 0; + + // only add lengths of password and username if not using SSPI or requesting federated authentication info + if (!rec.useSSPI && !(_connHandler._federatedAuthenticationInfoRequested || _connHandler._federatedAuthenticationRequested)) + { + checked + { + length += (userName.Length * 2) + encryptedPasswordLengthInBytes + + encryptedChangePasswordLengthInBytes; + } + } + else + { + if (rec.useSSPI) + { + // now allocate proper length of buffer, and set length + outSSPILength = _authenticationProvider.MaxSSPILength; + rentedSSPIBuff = ArrayPool.Shared.Rent((int)outSSPILength); + outSSPIBuff = rentedSSPIBuff; + + // Call helper function for SSPI data and actual length. + // Since we don't have SSPI data from the server, send null for the + // byte[] buffer and 0 for the int length. + Debug.Assert(SniContext.Snix_Login == _physicalStateObj.SniContext, $"Unexpected SniContext. Expecting Snix_Login, actual value is '{_physicalStateObj.SniContext}'"); + _physicalStateObj.SniContext = SniContext.Snix_LoginSspi; + _authenticationProvider.SSPIData(Array.Empty(), 0, ref outSSPIBuff, ref outSSPILength, _sniSpnBuffer); + + if (outSSPILength > int.MaxValue) + { + throw SQL.InvalidSSPIPacketSize(); // SqlBu 332503 + } + _physicalStateObj.SniContext = SniContext.Snix_Login; + + checked + { + length += (int)outSSPILength; + } + } + } + + int feOffset = length; + // calculate and reserve the required bytes for the featureEx + length = ApplyFeatureExData(requestedFeatures, recoverySessionData, fedAuthFeatureExtensionData, useFeatureExt, length); + + WriteLoginData(rec, + requestedFeatures, + recoverySessionData, + fedAuthFeatureExtensionData, + encrypt, + encryptedPassword, + encryptedChangePassword, + encryptedPasswordLengthInBytes, + encryptedChangePasswordLengthInBytes, + useFeatureExt, + userName, + length, + feOffset, + clientInterfaceName, + outSSPIBuff, + outSSPILength); + + if (rentedSSPIBuff != null) + { + ArrayPool.Shared.Return(rentedSSPIBuff, clearArray: true); + } + + _physicalStateObj.WritePacket(TdsEnums.HARDFLUSH); + _physicalStateObj.ResetSecurePasswordsInformation(); // Password information is needed only from Login process; done with writing login packet and should clear information + _physicalStateObj.HasPendingData = true; + _physicalStateObj._messageStatus = 0; + +#if NETFRAMEWORK + // Remvove CTAIP Provider after login record is sent. + // + if (originalNetworkAddressInfo != null) + { + UInt32 error = SNINativeMethodWrapper.SNIRemoveProvider(_physicalStateObj.Handle, SNINativeMethodWrapper.ProviderEnum.CTAIP_PROV); + if (error != TdsEnums.SNI_SUCCESS) + { + _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); + ThrowExceptionAndWarning(_physicalStateObj); + } + + try + { } // EmptyTry/Finally to avoid FXCop violation + finally + { + _physicalStateObj.ClearAllWritePackets(); + } + } +#endif + }// tdsLogin + } +}