_NegoState.cs source code in C# .NET

Source code for the .NET framework in C#

                        

Code:

/ Dotnetfx_Vista_SP2 / Dotnetfx_Vista_SP2 / 8.0.50727.4016 / DEVDIV / depot / DevDiv / releases / whidbey / NetFxQFE / ndp / fx / src / Net / System / Net / SecureProtocols / _NegoState.cs / 1 / _NegoState.cs

                            /*++ 
Copyright (c) Microsoft Corporation

Module Name:
 
    _NegoState.cs
 
Abstract: 
        The internal class is used by Negotiate Client&Server and by (internal) NegoStream.
        It encapsulates security context and does the real work in authentication and 
        user data encryption with NEGO SSPI package.

Author:
    Alexei Vopilov    12-Aug-2003 

Revision History: 
    12-Aug-2003 New design that has obsoleted Authenticator class 

--*/ 

namespace System.Net.Security {
    using System;
    using System.Net; 
    using System.IO;
    using System.Security; 
    using System.Security.Principal; 
    using System.Security.Permissions;
    using System.Threading; 
    using System.ComponentModel;
    using System.Security.Authentication;

    // 
    // The class maintains the state of the authentication process and the security context.
    // On the success it returns the remote side identites created as the result 
    // of authentication. 
    //
    internal class NegoState { 

        private const int ERROR_TRUST_FAILURE = 1790;   //used to serialize protectionLevel or impersonationLevel mismatch error to the remote side

        private static readonly byte[]  _EmptyMessage = new byte[0]; 
        private static readonly AsyncCallback   _ReadCallback   = new AsyncCallback(ReadCallback);
        private static readonly AsyncCallback   _WriteCallback  = new AsyncCallback(WriteCallback); 
 
        private Stream              _InnerStream;
        private bool                _LeaveStreamOpen; 

        private Exception           _Exception;

        private StreamFramer        _Framer; 
        private NTAuthentication    _Context;
 
        private int                 _NestedAuth; 

        internal const int          c_MaxReadFrameSize   = 64*1024; 
        internal const int          c_MaxWriteDataSize   = 63*1024; //we give 1k for the framing and trailer that is laways less as per SSPI.

        private  bool                       _CanRetryAuthentication;
        private  ProtectionLevel            _ExpectedProtectionLevel; 
        private  TokenImpersonationLevel    _ExpectedImpersonationLevel;
        private  uint                       _WriteSequenceNumber; 
        private  uint                       _ReadSequenceNumber; 

 

        // SSPI does not send a server ack on successfull auth.
        // So, this is a state variable used to gracefully handle auth confirmation
        private bool _RemoteOk = false; 

        // 
        // 
        //
        internal NegoState(Stream innerStream, bool leaveStreamOpen) { 
            if (innerStream==null) {
                throw new ArgumentNullException("stream");
            }
            _InnerStream = innerStream; 
            _LeaveStreamOpen = leaveStreamOpen;
        } 
        // 
        internal static string DefaultPackage {
            get { 
                return ComNetOS.IsWin9x? NegotiationInfoClass.NTLM: NegotiationInfoClass.Negotiate;
            }
        }
        // 
        //
        // 
        internal void ValidateCreateContext( 
                                            string package,
                                            bool isServer, 
                                            NetworkCredential credential,
                                            string servicePrincipalName,
                                            ProtectionLevel         protectionLevel,
                                            TokenImpersonationLevel impersonationLevel 
                                            )
        { 
 
            if (_Exception != null && !_CanRetryAuthentication) {
                throw _Exception; 
            }

            if (_Context != null && _Context.IsValidContext) {
                throw new InvalidOperationException(SR.GetString(SR.net_auth_reauth)); 
            }
 
            if (credential == null) { 
                throw new ArgumentNullException("credential");
            } 

            if (servicePrincipalName == null) {
                throw new ArgumentNullException("servicePrincipalName");
            } 

            if (ComNetOS.IsWin9x && protectionLevel != ProtectionLevel.None) 
            { 
                throw new NotSupportedException(SR.GetString(SR.net_auth_no_protection_on_win9x));
            } 

            if (impersonationLevel != TokenImpersonationLevel.Identification &&
                impersonationLevel != TokenImpersonationLevel.Impersonation &&
                impersonationLevel != TokenImpersonationLevel.Delegation) 
            {
                throw new ArgumentOutOfRangeException("impersonationLevel", impersonationLevel.ToString(), SR.GetString(SR.net_auth_supported_impl_levels)); 
            } 

            if (_Context != null && IsServer != isServer) { 
                throw new InvalidOperationException(SR.GetString(SR.net_auth_client_server));
            }

            _Exception = null; 
            _RemoteOk = false;
            _Framer = new StreamFramer(_InnerStream); 
            _Framer.WriteHeader.MessageId = FrameHeader.HandshakeId; 

            _ExpectedProtectionLevel    = protectionLevel; 
            _ExpectedImpersonationLevel = isServer? impersonationLevel: TokenImpersonationLevel.None;
            _WriteSequenceNumber        = 0;
            _ReadSequenceNumber         = 0;
 
            ContextFlags flags = ContextFlags.Connection;
 
            // A workaround for the client when talking to Win9x on the server side 
            if (protectionLevel == ProtectionLevel.None && !isServer)
            { 
                package = NegotiationInfoClass.NTLM;
            }

            else if (protectionLevel == ProtectionLevel.EncryptAndSign) 
            {
                flags |= ContextFlags.Confidentiality; 
            } 
            else if (protectionLevel == ProtectionLevel.Sign)
            { 
                // Assuming user expects NT4 SP4 and above
                flags |= ContextFlags.ReplayDetect | ContextFlags.SequenceDetect | ContextFlags.InitIntegrity;
            }
 
            if (!isServer)
            { 
                // According to lzhu server side should not request any of these flags 
                if (protectionLevel != ProtectionLevel.None)                        {flags |= ContextFlags.MutualAuth;}
                if (impersonationLevel == TokenImpersonationLevel.Identification)   {flags |= ContextFlags.InitIdentify;} 
                if (impersonationLevel == TokenImpersonationLevel.Delegation)       {flags |= ContextFlags.Delegate;}
            }

            _CanRetryAuthentication = false; 

            // 
            // Security: We used to rely on NetworkCredential class to demand permission 
            //           Switched over to explicit ControlPrincipalPermission demand (except for DefaultCredential case)
            //           The mitigated attack is brute-force pasword guessing through SSPI. 
            if (!(credential is SystemNetworkCredential))
                ExceptionHelper.ControlPrincipalPermission.Demand();

            try { 
                //
                _Context = new NTAuthentication(isServer, package, credential, servicePrincipalName, flags); 
            } 
            catch (Win32Exception e)
            { 
                throw new AuthenticationException(SR.GetString(SR.net_auth_SSPI), e);
            }
        }
 
        //
        // 
        private Exception SetException(Exception e) 
        {
            if (_Exception == null || !(_Exception is ObjectDisposedException)) 
            {
                _Exception = e;
            }
            if (_Exception != null && _Context != null) { 
                _Context.CloseContext();
            } 
            return _Exception; 
        }
 
        //
        // General informational properties
        //
 
        //
        // 
        internal bool IsAuthenticated { 
            get {
                return _Context != null && HandshakeComplete && _Exception == null && _RemoteOk; 
            }
        }
        //
        // 
        //
        internal  bool IsMutuallyAuthenticated { 
            get { 
                if (!IsAuthenticated)
                    return false; 

                // On Win9x it is alwasy false
                if (ComNetOS.IsWin9x)
                    return false; 

                // Suppressing for NTLM since SSPI does not return correct value int the context flags. 
                if (_Context.IsNTLM) 
                    return false;
 
                return _Context.IsMutualAuthFlag;
            }
        }
        // 
        internal  bool IsEncrypted {
            get { 
                return IsAuthenticated && _Context.IsConfidentialityFlag; 
            }
        } 
        //
        internal  bool IsSigned {
            get {
                return IsAuthenticated && (_Context.IsIntegrityFlag || _Context.IsConfidentialityFlag); 
            }
        } 
        // 
        internal bool IsServer {
            get { 
                return _Context != null && _Context.IsServer;
            }
        }
        // 
        // NEGO specific informational properties
        // 
 
        internal bool CanGetSecureStream {
            get { 
                return (_Context.IsConfidentialityFlag || _Context.IsIntegrityFlag);
            }
        }
        // 
        //
        // 
        internal TokenImpersonationLevel AllowedImpersonation { 
            get {
                CheckThrow(true); 
                return PrivateImpersonationLevel;
            }
        }
        // 
        private TokenImpersonationLevel PrivateImpersonationLevel {
            get { 
                // according to lzhu we should suppress dlegate flag in NTLM case 
                return  (_Context.IsDelegationFlag && _Context.ProtocolName != NegotiationInfoClass.NTLM) ? TokenImpersonationLevel.Delegation
                        :_Context.IsIdentifyFlag?   TokenImpersonationLevel.Identification 
                        :(ComNetOS.IsWin9x && _Context.IsServer) ? TokenImpersonationLevel.Identification  //Win9x workaround.
                        :TokenImpersonationLevel.Impersonation;
            }
        } 
        //
        // 
        // 
        private bool HandshakeComplete {
            get { 
                return _Context.IsCompleted && _Context.IsValidContext;
            }
        }
        // 
        // Note that method will demand PrincipalControlPermission
        // which essentially means demanding full trust 
        // 
        internal IIdentity GetIdentity() {
 
            CheckThrow(true);

            IIdentity result = null;
            string name = _Context.IsServer? _Context.AssociatedName: _Context.Spn; 
            string protocol = "NTLM";
 
            if (!ComNetOS.IsWin9x) { 
                protocol = _Context.ProtocolName;
            } 

            if (_Context.IsServer && !ComNetOS.IsWin9x) {
                SafeCloseHandle token = null;
                try { 
                    token = _Context.GetContextToken();
                    string authtype = _Context.ProtocolName; 
                    result = new WindowsIdentity(token.DangerousGetHandle(), authtype, WindowsAccountType.Normal, true); 
                    return result;
                } 
                catch (SecurityException) {
                    //ignore and construct generic Identity if failed due to security problem
                }
                finally { 
                    if (token != null) {
                        token.Close(); 
                    } 
                }
            } 
            // on the client we don't have access to the remote side identity.
            result = new GenericIdentity(name, protocol);
            return result;
        } 

        // 
        // Methods 
        //
 
        //
        //
        internal void CheckThrow(bool authSucessCheck) {
            if (_Exception != null) { 
                throw _Exception;
            } 
            if (authSucessCheck && !IsAuthenticated) { 
                throw new InvalidOperationException(SR.GetString(SR.net_auth_noauth));
            } 
        }
        //
        // This is to not depend on GC&SafeHandle class if the context is not needed anymore.
        // 
        internal void Close() {
            // Mark this instance as disposed 
            _Exception = new ObjectDisposedException("NegotiateStream"); 
            if (_Context != null) {
                _Context.CloseContext(); 
            }
        }
        //
        // 
        //
        internal void ProcessAuthentication(LazyAsyncResult lazyResult) 
        { 
            CheckThrow(false);
            if (Interlocked.Exchange(ref _NestedAuth, 1) == 1) { 
                throw new InvalidOperationException(SR.GetString(SR.net_io_invalidnestedcall, lazyResult==null?"BeginAuthenticate":"Authenticate", "authenticate"));
            }

            try { 
                if (_Context.IsServer)
                { 
                    // Listen for a client blob 
                    StartReceiveBlob(lazyResult);
                } 
                else
                {
                    // we start with the first blob
                    StartSendBlob(null, lazyResult); 
                }
            } 
            catch (Exception e) 
            {
                // Roundtrip it through the SetException() 
                e = SetException(e);
                throw e;
            }
            catch { 
                // Roundtrip it through the SetException()
                Exception e = SetException(new Exception(SR.GetString(SR.net_nonClsCompliantException))); 
                throw e; 
            }
            finally 
            {
                if (lazyResult == null || _Exception != null) {
                    _NestedAuth = 0;
                } 
            }
        } 
        // 
        //
        // 
        internal void EndProcessAuthentication(IAsyncResult result)
        {
            if (result == null)
            { 
                throw new ArgumentNullException("asyncResult");
            } 
 
            LazyAsyncResult lazyResult = result as LazyAsyncResult;
            if (lazyResult == null) 
            {
                throw new ArgumentException(SR.GetString(SR.net_io_async_result, result.GetType().FullName), "asyncResult");
            }
 
            if (Interlocked.Exchange(ref _NestedAuth, 0) == 0)
            { 
                throw new InvalidOperationException(SR.GetString(SR.net_io_invalidendcall, "EndAuthenticate")); 
            }
 
            // No "artificial" timeouts implemented so far, InnerStream controls that.
            lazyResult.InternalWaitForCompletion();

            Exception e = lazyResult.Result as Exception; 

            if (e != null) 
            { 
                // Roundtrip it through the SetException()
                e = SetException(e); 
                throw e;
            }

        } 

        // 
        // Client side starts here, but server also loops through this method 
        //
        private void StartSendBlob(byte[] message, LazyAsyncResult lazyResult) 
        {
            Win32Exception win32exception = null;
            if (message != _EmptyMessage)
            { 
                message = GetOutgoingBlob(message, ref win32exception);
            } 
 
            if (win32exception != null)
            { 
                // signal remote side on a failed attempt
                StartSendAuthResetSignal(lazyResult, message, win32exception);
                return;
            } 

            if (HandshakeComplete) 
            { 

                if (PrivateImpersonationLevel < _ExpectedImpersonationLevel) 
                {
                    Exception exception = new AuthenticationException(SR.GetString(SR.net_auth_context_expectation, _ExpectedImpersonationLevel.ToString(), PrivateImpersonationLevel.ToString()));
                    int statusCode = ERROR_TRUST_FAILURE;
                    message = new byte[8];  //sizeof(long) 

                    for (int i = message.Length-1; i >= 0; --i) 
                    { 
                        message[i] = (byte)(statusCode & 0xFF);
                        statusCode = (int) ((uint) statusCode >> 8); 
                    }

                    StartSendAuthResetSignal(lazyResult, message, exception);
                    return; 
                }
 
                ProtectionLevel result = _Context.IsConfidentialityFlag? ProtectionLevel.EncryptAndSign: _Context.IsIntegrityFlag? ProtectionLevel.Sign: ProtectionLevel.None; 

                if (result < _ExpectedProtectionLevel) 
                {
                    Exception exception = new AuthenticationException(SR.GetString(SR.net_auth_context_expectation, result.ToString(), _ExpectedProtectionLevel.ToString()));
                    int statusCode = ERROR_TRUST_FAILURE;
                    message = new byte[8];  //sizeof(long) 

                    for (int i = message.Length-1; i >= 0; --i) 
                    { 
                        message[i] = (byte)(statusCode & 0xFF);
                        statusCode = (int) ((uint) statusCode >> 8); 
                    }

                    StartSendAuthResetSignal(lazyResult, message, exception);
                    return; 
                }
 
                // Signal remote party that we are done 
                _Framer.WriteHeader.MessageId = FrameHeader.HandshakeDoneId;
                if (_Context.IsServer) 
                {
                    // Server may complete now because client SSPI would not complain at this point.
                    _RemoteOk = true;
 
                    // However the client will wait for server to send this ACK
                    //Force signalling server OK to the client 
                    if (message == null) 
                    {
                        message = _EmptyMessage; 
                    }
                }
            }
            else if (message == null || message == _EmptyMessage) { 
                throw new InternalException();
            } 
 
            if (message != null)
            { 
                //even if we are comleted, there could be a blob for sending.
                if (lazyResult == null)
                {
                    _Framer.WriteMessage(message); 
                }
                else 
                { 
                    IAsyncResult ar = _Framer.BeginWriteMessage(message, _WriteCallback, lazyResult);
                    if (!ar.CompletedSynchronously) 
                    {
                        return;
                    }
                    _Framer.EndWriteMessage(ar); 
                }
            } 
            CheckCompletionBeforeNextReceive(lazyResult); 
        }
        // 
        // This will check and logically complete the auth handshake
        //
        private void CheckCompletionBeforeNextReceive(LazyAsyncResult lazyResult)
        { 
            if (HandshakeComplete && _RemoteOk)
            { 
                //we are done with success 
                if (lazyResult != null)
                { 
                    lazyResult.InvokeCallback();
                }
                return;
            } 
            StartReceiveBlob(lazyResult);
        } 
        // 
        // Server side starts here, but client also loops through this method
        // 
        private void StartReceiveBlob(LazyAsyncResult lazyResult)
        {
            byte[] message;
            if (lazyResult == null) 
            {
                message = _Framer.ReadMessage(); 
            } 
            else
            { 
                IAsyncResult ar = _Framer.BeginReadMessage(_ReadCallback, lazyResult);
                if (!ar.CompletedSynchronously)
                {
                    return; 
                }
                message = _Framer.EndReadMessage(ar); 
            } 
            ProcessReceivedBlob(message, lazyResult);
        } 
        //
        //
        //
        private void ProcessReceivedBlob(byte[] message, LazyAsyncResult lazyResult) 
        {
            // This is an EOF otherwise we would get at least *empty* message but not a null one. 
            if (message == null) 
            {
                throw new AuthenticationException(SR.GetString(SR.net_auth_eof), null); 
            }

            //Process Header information
            if (_Framer.ReadHeader.MessageId == FrameHeader.HandshakeErrId) 
            {
                Win32Exception e = null; 
                if (message.Length >= 8)    // sizeof(long) 
                {
                    // Try to recover remote win32 Exception 
                    long error = 0;
                    for (int i = 0; i < 8; ++i)
                        error = (error<<8) + message[i];
                    e = new Win32Exception((int)error); 
                }
                if (e != null) 
                { 
                     if (e.NativeErrorCode == (int)SecurityStatus.LogonDenied)
                        throw new InvalidCredentialException(SR.GetString(SR.net_auth_bad_client_creds), e); 

                     if (e.NativeErrorCode == ERROR_TRUST_FAILURE)
                         throw new AuthenticationException(SR.GetString(SR.net_auth_context_expectation_remote), e);
                 } 

                throw new AuthenticationException(SR.GetString(SR.net_auth_alert), e); 
            } 

            if (_Framer.ReadHeader.MessageId == FrameHeader.HandshakeDoneId) 
            {
                _RemoteOk = true;
            }
            else if (_Framer.ReadHeader.MessageId != FrameHeader.HandshakeId) 
            {
                throw new AuthenticationException(SR.GetString(SR.net_io_header_id, "MessageId", _Framer.ReadHeader.MessageId, FrameHeader.HandshakeId), null); 
            } 
            CheckCompletionBeforeNextSend(message, lazyResult);
        } 
        //
        // This will check and logically complete the auth handshake
        //
        private void CheckCompletionBeforeNextSend(byte[] message, LazyAsyncResult lazyResult) 
        {
            //If we are done don't go into send 
            if (HandshakeComplete) 
            {
                if (!_RemoteOk) 
                {
                    throw new AuthenticationException(SR.GetString(SR.net_io_header_id, "MessageId", _Framer.ReadHeader.MessageId, FrameHeader.HandshakeDoneId), null);
                }
                if (lazyResult != null) 
                {
                    lazyResult.InvokeCallback(); 
                } 
                return;
            } 

            // Not yet done, get a new blob and send it if any
            StartSendBlob(message, lazyResult);
        } 
        //
        //  This is to reset auth state on remote side. 
        //  If this write succeeds we will allow auth retrying. 
        //
        private void StartSendAuthResetSignal(LazyAsyncResult lazyResult, byte[] message, Exception exception) 
        {
            _Framer.WriteHeader.MessageId = FrameHeader.HandshakeErrId;

            Win32Exception win32exception = exception as Win32Exception; 

            if (win32exception != null && win32exception.NativeErrorCode == (int)SecurityStatus.LogonDenied) 
                if (IsServer) 
                    exception = new InvalidCredentialException(SR.GetString(SR.net_auth_bad_client_creds), exception);
                else 
                    exception = new InvalidCredentialException(SR.GetString(SR.net_auth_bad_client_creds_or_target_mismatch), exception);

            if (!(exception is AuthenticationException))
                exception = new AuthenticationException(SR.GetString(SR.net_auth_SSPI), exception); 

            if (lazyResult == null) 
            { 
                _Framer.WriteMessage(message);
            } 
            else
            {
                lazyResult.Result = exception;
                IAsyncResult ar = _Framer.BeginWriteMessage(message, _WriteCallback, lazyResult); 
                if(!ar.CompletedSynchronously)
                { 
                    return; 
                }
                _Framer.EndWriteMessage(ar); 
            }

            _CanRetryAuthentication = true;
            throw exception; 
        }
        // 
        // 
        //
        private static void WriteCallback(IAsyncResult transportResult) 
        {
            GlobalLog.Assert(transportResult.AsyncState is LazyAsyncResult, "WriteCallback|State type is wrong, expected LazyAsyncResult.");
            if (transportResult.CompletedSynchronously)
            { 
                return;
            } 
 
            LazyAsyncResult lazyResult = (LazyAsyncResult) transportResult.AsyncState;
 
            // Async completion
            try
            {
                NegoState authState = (NegoState)lazyResult.AsyncObject; 
                authState._Framer.EndWriteMessage(transportResult);
 
                //special case for an error notification 
                if (lazyResult.Result is Exception)
                { 
                    authState._CanRetryAuthentication = true;
                    throw (Exception)lazyResult.Result;
                }
                authState.CheckCompletionBeforeNextReceive(lazyResult); 
            }
            catch (Exception e) 
            { 
                if (lazyResult.InternalPeekCompleted) {
                    // This will throw on a worker thread. 
                    throw;
                }
                lazyResult.InvokeCallback(e);
            } 
            catch {
                if (lazyResult.InternalPeekCompleted) { 
                    // This will throw on a worker thread. 
                    throw;
                } 
                lazyResult.InvokeCallback(new Exception(SR.GetString(SR.net_nonClsCompliantException)));
            }
        }
        // 
        //
        // 
        private static void ReadCallback(IAsyncResult transportResult) 
        {
            GlobalLog.Assert(transportResult.AsyncState is LazyAsyncResult, "ReadCallback|State type is wrong, expected LazyAsyncResult."); 
            if (transportResult.CompletedSynchronously)
            {
                return;
            } 

            LazyAsyncResult lazyResult = (LazyAsyncResult) transportResult.AsyncState; 
 
            // Async completion
            try 
            {
                NegoState authState = (NegoState)lazyResult.AsyncObject;
                byte[] message = authState._Framer.EndReadMessage(transportResult);
                authState.ProcessReceivedBlob(message, lazyResult); 
            }
            catch (Exception e) 
            { 
                if (lazyResult.InternalPeekCompleted) {
                    // This will throw on a worker thread. 
                    throw;
                }

                lazyResult.InvokeCallback(e); 
            }
            catch { 
                if (lazyResult.InternalPeekCompleted) { 
                    // This will throw on a worker thread.
                    throw; 
                }

                lazyResult.InvokeCallback(new Exception(SR.GetString(SR.net_nonClsCompliantException)));
            } 
        }
        // 
        // 
        //
        private unsafe byte[] GetOutgoingBlob(byte[] incomingBlob, ref Win32Exception e) { 

            SecurityStatus statusCode;
            byte[] message = _Context.GetOutgoingBlob(incomingBlob, false, out statusCode);
 
            if (((int) statusCode & unchecked((int) 0x80000000)) != 0)
            { 
                e = new System.ComponentModel.Win32Exception((int) statusCode); 

                message = new byte[8];  //sizeof(long) 
                for (int i = message.Length-1; i >= 0; --i)
                {
                    message[i] = (byte) ((uint) statusCode & 0xFF);
                    statusCode = (SecurityStatus) ((uint) statusCode >> 8); 
                }
            } 
 
            if (message != null && message.Length == 0) {
                message = _EmptyMessage; 
            }
            return message;
        }
        // 
        //
        // 
        internal int EncryptData(byte[] buffer, int offset, int count, ref byte[] outBuffer) 
        {
            CheckThrow(true); 
            //
            // Well, this is to play by the rules but in reality SSPI seems to ignore this sequence number.
            // Means we could simply pass 0
            // 
            ++_WriteSequenceNumber;
            return _Context.Encrypt(buffer, offset, count, ref outBuffer, _WriteSequenceNumber); 
        } 
        //
        // 
        //
        internal int DecryptData(byte[] buffer, int offset, int count, out int newOffset)
        {
            CheckThrow(true); 
            //
            // Well, this is to play by the rules but in reality SSPI seems to ignore this sequence number. 
            // Means we could simply pass 0 
            //
            ++_ReadSequenceNumber; 
            return _Context.Decrypt(buffer, offset, count, out newOffset, _ReadSequenceNumber);
        }
    }
 
}

// File provided for Reference Use Only by Microsoft Corporation (c) 2007.
// Copyright (c) Microsoft Corporation. All rights reserved.
/*++ 
Copyright (c) Microsoft Corporation

Module Name:
 
    _NegoState.cs
 
Abstract: 
        The internal class is used by Negotiate Client&Server and by (internal) NegoStream.
        It encapsulates security context and does the real work in authentication and 
        user data encryption with NEGO SSPI package.

Author:
    Alexei Vopilov    12-Aug-2003 

Revision History: 
    12-Aug-2003 New design that has obsoleted Authenticator class 

--*/ 

namespace System.Net.Security {
    using System;
    using System.Net; 
    using System.IO;
    using System.Security; 
    using System.Security.Principal; 
    using System.Security.Permissions;
    using System.Threading; 
    using System.ComponentModel;
    using System.Security.Authentication;

    // 
    // The class maintains the state of the authentication process and the security context.
    // On the success it returns the remote side identites created as the result 
    // of authentication. 
    //
    internal class NegoState { 

        private const int ERROR_TRUST_FAILURE = 1790;   //used to serialize protectionLevel or impersonationLevel mismatch error to the remote side

        private static readonly byte[]  _EmptyMessage = new byte[0]; 
        private static readonly AsyncCallback   _ReadCallback   = new AsyncCallback(ReadCallback);
        private static readonly AsyncCallback   _WriteCallback  = new AsyncCallback(WriteCallback); 
 
        private Stream              _InnerStream;
        private bool                _LeaveStreamOpen; 

        private Exception           _Exception;

        private StreamFramer        _Framer; 
        private NTAuthentication    _Context;
 
        private int                 _NestedAuth; 

        internal const int          c_MaxReadFrameSize   = 64*1024; 
        internal const int          c_MaxWriteDataSize   = 63*1024; //we give 1k for the framing and trailer that is laways less as per SSPI.

        private  bool                       _CanRetryAuthentication;
        private  ProtectionLevel            _ExpectedProtectionLevel; 
        private  TokenImpersonationLevel    _ExpectedImpersonationLevel;
        private  uint                       _WriteSequenceNumber; 
        private  uint                       _ReadSequenceNumber; 

 

        // SSPI does not send a server ack on successfull auth.
        // So, this is a state variable used to gracefully handle auth confirmation
        private bool _RemoteOk = false; 

        // 
        // 
        //
        internal NegoState(Stream innerStream, bool leaveStreamOpen) { 
            if (innerStream==null) {
                throw new ArgumentNullException("stream");
            }
            _InnerStream = innerStream; 
            _LeaveStreamOpen = leaveStreamOpen;
        } 
        // 
        internal static string DefaultPackage {
            get { 
                return ComNetOS.IsWin9x? NegotiationInfoClass.NTLM: NegotiationInfoClass.Negotiate;
            }
        }
        // 
        //
        // 
        internal void ValidateCreateContext( 
                                            string package,
                                            bool isServer, 
                                            NetworkCredential credential,
                                            string servicePrincipalName,
                                            ProtectionLevel         protectionLevel,
                                            TokenImpersonationLevel impersonationLevel 
                                            )
        { 
 
            if (_Exception != null && !_CanRetryAuthentication) {
                throw _Exception; 
            }

            if (_Context != null && _Context.IsValidContext) {
                throw new InvalidOperationException(SR.GetString(SR.net_auth_reauth)); 
            }
 
            if (credential == null) { 
                throw new ArgumentNullException("credential");
            } 

            if (servicePrincipalName == null) {
                throw new ArgumentNullException("servicePrincipalName");
            } 

            if (ComNetOS.IsWin9x && protectionLevel != ProtectionLevel.None) 
            { 
                throw new NotSupportedException(SR.GetString(SR.net_auth_no_protection_on_win9x));
            } 

            if (impersonationLevel != TokenImpersonationLevel.Identification &&
                impersonationLevel != TokenImpersonationLevel.Impersonation &&
                impersonationLevel != TokenImpersonationLevel.Delegation) 
            {
                throw new ArgumentOutOfRangeException("impersonationLevel", impersonationLevel.ToString(), SR.GetString(SR.net_auth_supported_impl_levels)); 
            } 

            if (_Context != null && IsServer != isServer) { 
                throw new InvalidOperationException(SR.GetString(SR.net_auth_client_server));
            }

            _Exception = null; 
            _RemoteOk = false;
            _Framer = new StreamFramer(_InnerStream); 
            _Framer.WriteHeader.MessageId = FrameHeader.HandshakeId; 

            _ExpectedProtectionLevel    = protectionLevel; 
            _ExpectedImpersonationLevel = isServer? impersonationLevel: TokenImpersonationLevel.None;
            _WriteSequenceNumber        = 0;
            _ReadSequenceNumber         = 0;
 
            ContextFlags flags = ContextFlags.Connection;
 
            // A workaround for the client when talking to Win9x on the server side 
            if (protectionLevel == ProtectionLevel.None && !isServer)
            { 
                package = NegotiationInfoClass.NTLM;
            }

            else if (protectionLevel == ProtectionLevel.EncryptAndSign) 
            {
                flags |= ContextFlags.Confidentiality; 
            } 
            else if (protectionLevel == ProtectionLevel.Sign)
            { 
                // Assuming user expects NT4 SP4 and above
                flags |= ContextFlags.ReplayDetect | ContextFlags.SequenceDetect | ContextFlags.InitIntegrity;
            }
 
            if (!isServer)
            { 
                // According to lzhu server side should not request any of these flags 
                if (protectionLevel != ProtectionLevel.None)                        {flags |= ContextFlags.MutualAuth;}
                if (impersonationLevel == TokenImpersonationLevel.Identification)   {flags |= ContextFlags.InitIdentify;} 
                if (impersonationLevel == TokenImpersonationLevel.Delegation)       {flags |= ContextFlags.Delegate;}
            }

            _CanRetryAuthentication = false; 

            // 
            // Security: We used to rely on NetworkCredential class to demand permission 
            //           Switched over to explicit ControlPrincipalPermission demand (except for DefaultCredential case)
            //           The mitigated attack is brute-force pasword guessing through SSPI. 
            if (!(credential is SystemNetworkCredential))
                ExceptionHelper.ControlPrincipalPermission.Demand();

            try { 
                //
                _Context = new NTAuthentication(isServer, package, credential, servicePrincipalName, flags); 
            } 
            catch (Win32Exception e)
            { 
                throw new AuthenticationException(SR.GetString(SR.net_auth_SSPI), e);
            }
        }
 
        //
        // 
        private Exception SetException(Exception e) 
        {
            if (_Exception == null || !(_Exception is ObjectDisposedException)) 
            {
                _Exception = e;
            }
            if (_Exception != null && _Context != null) { 
                _Context.CloseContext();
            } 
            return _Exception; 
        }
 
        //
        // General informational properties
        //
 
        //
        // 
        internal bool IsAuthenticated { 
            get {
                return _Context != null && HandshakeComplete && _Exception == null && _RemoteOk; 
            }
        }
        //
        // 
        //
        internal  bool IsMutuallyAuthenticated { 
            get { 
                if (!IsAuthenticated)
                    return false; 

                // On Win9x it is alwasy false
                if (ComNetOS.IsWin9x)
                    return false; 

                // Suppressing for NTLM since SSPI does not return correct value int the context flags. 
                if (_Context.IsNTLM) 
                    return false;
 
                return _Context.IsMutualAuthFlag;
            }
        }
        // 
        internal  bool IsEncrypted {
            get { 
                return IsAuthenticated && _Context.IsConfidentialityFlag; 
            }
        } 
        //
        internal  bool IsSigned {
            get {
                return IsAuthenticated && (_Context.IsIntegrityFlag || _Context.IsConfidentialityFlag); 
            }
        } 
        // 
        internal bool IsServer {
            get { 
                return _Context != null && _Context.IsServer;
            }
        }
        // 
        // NEGO specific informational properties
        // 
 
        internal bool CanGetSecureStream {
            get { 
                return (_Context.IsConfidentialityFlag || _Context.IsIntegrityFlag);
            }
        }
        // 
        //
        // 
        internal TokenImpersonationLevel AllowedImpersonation { 
            get {
                CheckThrow(true); 
                return PrivateImpersonationLevel;
            }
        }
        // 
        private TokenImpersonationLevel PrivateImpersonationLevel {
            get { 
                // according to lzhu we should suppress dlegate flag in NTLM case 
                return  (_Context.IsDelegationFlag && _Context.ProtocolName != NegotiationInfoClass.NTLM) ? TokenImpersonationLevel.Delegation
                        :_Context.IsIdentifyFlag?   TokenImpersonationLevel.Identification 
                        :(ComNetOS.IsWin9x && _Context.IsServer) ? TokenImpersonationLevel.Identification  //Win9x workaround.
                        :TokenImpersonationLevel.Impersonation;
            }
        } 
        //
        // 
        // 
        private bool HandshakeComplete {
            get { 
                return _Context.IsCompleted && _Context.IsValidContext;
            }
        }
        // 
        // Note that method will demand PrincipalControlPermission
        // which essentially means demanding full trust 
        // 
        internal IIdentity GetIdentity() {
 
            CheckThrow(true);

            IIdentity result = null;
            string name = _Context.IsServer? _Context.AssociatedName: _Context.Spn; 
            string protocol = "NTLM";
 
            if (!ComNetOS.IsWin9x) { 
                protocol = _Context.ProtocolName;
            } 

            if (_Context.IsServer && !ComNetOS.IsWin9x) {
                SafeCloseHandle token = null;
                try { 
                    token = _Context.GetContextToken();
                    string authtype = _Context.ProtocolName; 
                    result = new WindowsIdentity(token.DangerousGetHandle(), authtype, WindowsAccountType.Normal, true); 
                    return result;
                } 
                catch (SecurityException) {
                    //ignore and construct generic Identity if failed due to security problem
                }
                finally { 
                    if (token != null) {
                        token.Close(); 
                    } 
                }
            } 
            // on the client we don't have access to the remote side identity.
            result = new GenericIdentity(name, protocol);
            return result;
        } 

        // 
        // Methods 
        //
 
        //
        //
        internal void CheckThrow(bool authSucessCheck) {
            if (_Exception != null) { 
                throw _Exception;
            } 
            if (authSucessCheck && !IsAuthenticated) { 
                throw new InvalidOperationException(SR.GetString(SR.net_auth_noauth));
            } 
        }
        //
        // This is to not depend on GC&SafeHandle class if the context is not needed anymore.
        // 
        internal void Close() {
            // Mark this instance as disposed 
            _Exception = new ObjectDisposedException("NegotiateStream"); 
            if (_Context != null) {
                _Context.CloseContext(); 
            }
        }
        //
        // 
        //
        internal void ProcessAuthentication(LazyAsyncResult lazyResult) 
        { 
            CheckThrow(false);
            if (Interlocked.Exchange(ref _NestedAuth, 1) == 1) { 
                throw new InvalidOperationException(SR.GetString(SR.net_io_invalidnestedcall, lazyResult==null?"BeginAuthenticate":"Authenticate", "authenticate"));
            }

            try { 
                if (_Context.IsServer)
                { 
                    // Listen for a client blob 
                    StartReceiveBlob(lazyResult);
                } 
                else
                {
                    // we start with the first blob
                    StartSendBlob(null, lazyResult); 
                }
            } 
            catch (Exception e) 
            {
                // Roundtrip it through the SetException() 
                e = SetException(e);
                throw e;
            }
            catch { 
                // Roundtrip it through the SetException()
                Exception e = SetException(new Exception(SR.GetString(SR.net_nonClsCompliantException))); 
                throw e; 
            }
            finally 
            {
                if (lazyResult == null || _Exception != null) {
                    _NestedAuth = 0;
                } 
            }
        } 
        // 
        //
        // 
        internal void EndProcessAuthentication(IAsyncResult result)
        {
            if (result == null)
            { 
                throw new ArgumentNullException("asyncResult");
            } 
 
            LazyAsyncResult lazyResult = result as LazyAsyncResult;
            if (lazyResult == null) 
            {
                throw new ArgumentException(SR.GetString(SR.net_io_async_result, result.GetType().FullName), "asyncResult");
            }
 
            if (Interlocked.Exchange(ref _NestedAuth, 0) == 0)
            { 
                throw new InvalidOperationException(SR.GetString(SR.net_io_invalidendcall, "EndAuthenticate")); 
            }
 
            // No "artificial" timeouts implemented so far, InnerStream controls that.
            lazyResult.InternalWaitForCompletion();

            Exception e = lazyResult.Result as Exception; 

            if (e != null) 
            { 
                // Roundtrip it through the SetException()
                e = SetException(e); 
                throw e;
            }

        } 

        // 
        // Client side starts here, but server also loops through this method 
        //
        private void StartSendBlob(byte[] message, LazyAsyncResult lazyResult) 
        {
            Win32Exception win32exception = null;
            if (message != _EmptyMessage)
            { 
                message = GetOutgoingBlob(message, ref win32exception);
            } 
 
            if (win32exception != null)
            { 
                // signal remote side on a failed attempt
                StartSendAuthResetSignal(lazyResult, message, win32exception);
                return;
            } 

            if (HandshakeComplete) 
            { 

                if (PrivateImpersonationLevel < _ExpectedImpersonationLevel) 
                {
                    Exception exception = new AuthenticationException(SR.GetString(SR.net_auth_context_expectation, _ExpectedImpersonationLevel.ToString(), PrivateImpersonationLevel.ToString()));
                    int statusCode = ERROR_TRUST_FAILURE;
                    message = new byte[8];  //sizeof(long) 

                    for (int i = message.Length-1; i >= 0; --i) 
                    { 
                        message[i] = (byte)(statusCode & 0xFF);
                        statusCode = (int) ((uint) statusCode >> 8); 
                    }

                    StartSendAuthResetSignal(lazyResult, message, exception);
                    return; 
                }
 
                ProtectionLevel result = _Context.IsConfidentialityFlag? ProtectionLevel.EncryptAndSign: _Context.IsIntegrityFlag? ProtectionLevel.Sign: ProtectionLevel.None; 

                if (result < _ExpectedProtectionLevel) 
                {
                    Exception exception = new AuthenticationException(SR.GetString(SR.net_auth_context_expectation, result.ToString(), _ExpectedProtectionLevel.ToString()));
                    int statusCode = ERROR_TRUST_FAILURE;
                    message = new byte[8];  //sizeof(long) 

                    for (int i = message.Length-1; i >= 0; --i) 
                    { 
                        message[i] = (byte)(statusCode & 0xFF);
                        statusCode = (int) ((uint) statusCode >> 8); 
                    }

                    StartSendAuthResetSignal(lazyResult, message, exception);
                    return; 
                }
 
                // Signal remote party that we are done 
                _Framer.WriteHeader.MessageId = FrameHeader.HandshakeDoneId;
                if (_Context.IsServer) 
                {
                    // Server may complete now because client SSPI would not complain at this point.
                    _RemoteOk = true;
 
                    // However the client will wait for server to send this ACK
                    //Force signalling server OK to the client 
                    if (message == null) 
                    {
                        message = _EmptyMessage; 
                    }
                }
            }
            else if (message == null || message == _EmptyMessage) { 
                throw new InternalException();
            } 
 
            if (message != null)
            { 
                //even if we are comleted, there could be a blob for sending.
                if (lazyResult == null)
                {
                    _Framer.WriteMessage(message); 
                }
                else 
                { 
                    IAsyncResult ar = _Framer.BeginWriteMessage(message, _WriteCallback, lazyResult);
                    if (!ar.CompletedSynchronously) 
                    {
                        return;
                    }
                    _Framer.EndWriteMessage(ar); 
                }
            } 
            CheckCompletionBeforeNextReceive(lazyResult); 
        }
        // 
        // This will check and logically complete the auth handshake
        //
        private void CheckCompletionBeforeNextReceive(LazyAsyncResult lazyResult)
        { 
            if (HandshakeComplete && _RemoteOk)
            { 
                //we are done with success 
                if (lazyResult != null)
                { 
                    lazyResult.InvokeCallback();
                }
                return;
            } 
            StartReceiveBlob(lazyResult);
        } 
        // 
        // Server side starts here, but client also loops through this method
        // 
        private void StartReceiveBlob(LazyAsyncResult lazyResult)
        {
            byte[] message;
            if (lazyResult == null) 
            {
                message = _Framer.ReadMessage(); 
            } 
            else
            { 
                IAsyncResult ar = _Framer.BeginReadMessage(_ReadCallback, lazyResult);
                if (!ar.CompletedSynchronously)
                {
                    return; 
                }
                message = _Framer.EndReadMessage(ar); 
            } 
            ProcessReceivedBlob(message, lazyResult);
        } 
        //
        //
        //
        private void ProcessReceivedBlob(byte[] message, LazyAsyncResult lazyResult) 
        {
            // This is an EOF otherwise we would get at least *empty* message but not a null one. 
            if (message == null) 
            {
                throw new AuthenticationException(SR.GetString(SR.net_auth_eof), null); 
            }

            //Process Header information
            if (_Framer.ReadHeader.MessageId == FrameHeader.HandshakeErrId) 
            {
                Win32Exception e = null; 
                if (message.Length >= 8)    // sizeof(long) 
                {
                    // Try to recover remote win32 Exception 
                    long error = 0;
                    for (int i = 0; i < 8; ++i)
                        error = (error<<8) + message[i];
                    e = new Win32Exception((int)error); 
                }
                if (e != null) 
                { 
                     if (e.NativeErrorCode == (int)SecurityStatus.LogonDenied)
                        throw new InvalidCredentialException(SR.GetString(SR.net_auth_bad_client_creds), e); 

                     if (e.NativeErrorCode == ERROR_TRUST_FAILURE)
                         throw new AuthenticationException(SR.GetString(SR.net_auth_context_expectation_remote), e);
                 } 

                throw new AuthenticationException(SR.GetString(SR.net_auth_alert), e); 
            } 

            if (_Framer.ReadHeader.MessageId == FrameHeader.HandshakeDoneId) 
            {
                _RemoteOk = true;
            }
            else if (_Framer.ReadHeader.MessageId != FrameHeader.HandshakeId) 
            {
                throw new AuthenticationException(SR.GetString(SR.net_io_header_id, "MessageId", _Framer.ReadHeader.MessageId, FrameHeader.HandshakeId), null); 
            } 
            CheckCompletionBeforeNextSend(message, lazyResult);
        } 
        //
        // This will check and logically complete the auth handshake
        //
        private void CheckCompletionBeforeNextSend(byte[] message, LazyAsyncResult lazyResult) 
        {
            //If we are done don't go into send 
            if (HandshakeComplete) 
            {
                if (!_RemoteOk) 
                {
                    throw new AuthenticationException(SR.GetString(SR.net_io_header_id, "MessageId", _Framer.ReadHeader.MessageId, FrameHeader.HandshakeDoneId), null);
                }
                if (lazyResult != null) 
                {
                    lazyResult.InvokeCallback(); 
                } 
                return;
            } 

            // Not yet done, get a new blob and send it if any
            StartSendBlob(message, lazyResult);
        } 
        //
        //  This is to reset auth state on remote side. 
        //  If this write succeeds we will allow auth retrying. 
        //
        private void StartSendAuthResetSignal(LazyAsyncResult lazyResult, byte[] message, Exception exception) 
        {
            _Framer.WriteHeader.MessageId = FrameHeader.HandshakeErrId;

            Win32Exception win32exception = exception as Win32Exception; 

            if (win32exception != null && win32exception.NativeErrorCode == (int)SecurityStatus.LogonDenied) 
                if (IsServer) 
                    exception = new InvalidCredentialException(SR.GetString(SR.net_auth_bad_client_creds), exception);
                else 
                    exception = new InvalidCredentialException(SR.GetString(SR.net_auth_bad_client_creds_or_target_mismatch), exception);

            if (!(exception is AuthenticationException))
                exception = new AuthenticationException(SR.GetString(SR.net_auth_SSPI), exception); 

            if (lazyResult == null) 
            { 
                _Framer.WriteMessage(message);
            } 
            else
            {
                lazyResult.Result = exception;
                IAsyncResult ar = _Framer.BeginWriteMessage(message, _WriteCallback, lazyResult); 
                if(!ar.CompletedSynchronously)
                { 
                    return; 
                }
                _Framer.EndWriteMessage(ar); 
            }

            _CanRetryAuthentication = true;
            throw exception; 
        }
        // 
        // 
        //
        private static void WriteCallback(IAsyncResult transportResult) 
        {
            GlobalLog.Assert(transportResult.AsyncState is LazyAsyncResult, "WriteCallback|State type is wrong, expected LazyAsyncResult.");
            if (transportResult.CompletedSynchronously)
            { 
                return;
            } 
 
            LazyAsyncResult lazyResult = (LazyAsyncResult) transportResult.AsyncState;
 
            // Async completion
            try
            {
                NegoState authState = (NegoState)lazyResult.AsyncObject; 
                authState._Framer.EndWriteMessage(transportResult);
 
                //special case for an error notification 
                if (lazyResult.Result is Exception)
                { 
                    authState._CanRetryAuthentication = true;
                    throw (Exception)lazyResult.Result;
                }
                authState.CheckCompletionBeforeNextReceive(lazyResult); 
            }
            catch (Exception e) 
            { 
                if (lazyResult.InternalPeekCompleted) {
                    // This will throw on a worker thread. 
                    throw;
                }
                lazyResult.InvokeCallback(e);
            } 
            catch {
                if (lazyResult.InternalPeekCompleted) { 
                    // This will throw on a worker thread. 
                    throw;
                } 
                lazyResult.InvokeCallback(new Exception(SR.GetString(SR.net_nonClsCompliantException)));
            }
        }
        // 
        //
        // 
        private static void ReadCallback(IAsyncResult transportResult) 
        {
            GlobalLog.Assert(transportResult.AsyncState is LazyAsyncResult, "ReadCallback|State type is wrong, expected LazyAsyncResult."); 
            if (transportResult.CompletedSynchronously)
            {
                return;
            } 

            LazyAsyncResult lazyResult = (LazyAsyncResult) transportResult.AsyncState; 
 
            // Async completion
            try 
            {
                NegoState authState = (NegoState)lazyResult.AsyncObject;
                byte[] message = authState._Framer.EndReadMessage(transportResult);
                authState.ProcessReceivedBlob(message, lazyResult); 
            }
            catch (Exception e) 
            { 
                if (lazyResult.InternalPeekCompleted) {
                    // This will throw on a worker thread. 
                    throw;
                }

                lazyResult.InvokeCallback(e); 
            }
            catch { 
                if (lazyResult.InternalPeekCompleted) { 
                    // This will throw on a worker thread.
                    throw; 
                }

                lazyResult.InvokeCallback(new Exception(SR.GetString(SR.net_nonClsCompliantException)));
            } 
        }
        // 
        // 
        //
        private unsafe byte[] GetOutgoingBlob(byte[] incomingBlob, ref Win32Exception e) { 

            SecurityStatus statusCode;
            byte[] message = _Context.GetOutgoingBlob(incomingBlob, false, out statusCode);
 
            if (((int) statusCode & unchecked((int) 0x80000000)) != 0)
            { 
                e = new System.ComponentModel.Win32Exception((int) statusCode); 

                message = new byte[8];  //sizeof(long) 
                for (int i = message.Length-1; i >= 0; --i)
                {
                    message[i] = (byte) ((uint) statusCode & 0xFF);
                    statusCode = (SecurityStatus) ((uint) statusCode >> 8); 
                }
            } 
 
            if (message != null && message.Length == 0) {
                message = _EmptyMessage; 
            }
            return message;
        }
        // 
        //
        // 
        internal int EncryptData(byte[] buffer, int offset, int count, ref byte[] outBuffer) 
        {
            CheckThrow(true); 
            //
            // Well, this is to play by the rules but in reality SSPI seems to ignore this sequence number.
            // Means we could simply pass 0
            // 
            ++_WriteSequenceNumber;
            return _Context.Encrypt(buffer, offset, count, ref outBuffer, _WriteSequenceNumber); 
        } 
        //
        // 
        //
        internal int DecryptData(byte[] buffer, int offset, int count, out int newOffset)
        {
            CheckThrow(true); 
            //
            // Well, this is to play by the rules but in reality SSPI seems to ignore this sequence number. 
            // Means we could simply pass 0 
            //
            ++_ReadSequenceNumber; 
            return _Context.Decrypt(buffer, offset, count, out newOffset, _ReadSequenceNumber);
        }
    }
 
}

// File provided for Reference Use Only by Microsoft Corporation (c) 2007.
// Copyright (c) Microsoft Corporation. All rights reserved.
                        

Link Menu

Network programming in C#, Network Programming in VB.NET, Network Programming in .NET
This book is available now!
Buy at Amazon US or
Buy at Amazon UK