File: System\ServiceModel\Security\WindowsSspiNegotiation.cs
Project: ndp\cdf\src\WCF\ServiceModel\System.ServiceModel.csproj (System.ServiceModel)
//-----------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
//-----------------------------------------------------------------------------
 
namespace System.ServiceModel.Security
{
    using System.Runtime.InteropServices;
    using System.ServiceModel.Channels;
    using System.ServiceModel;
    using System.ServiceModel.Diagnostics;
    using System.Diagnostics;
    using System.Collections.Generic;
    using System.Text;
    using System.Threading;
    using System.Globalization;
    using System.ComponentModel;
    using System.Security.Principal;
    using System.IdentityModel.Tokens;
    using System.Net;
    using System.IdentityModel;
    using System.IdentityModel.Selectors;
    using System.Security.Authentication.ExtendedProtection;
    using IMD = System.IdentityModel.Diagnostics;
 
    using DiagnosticUtility = System.ServiceModel.DiagnosticUtility;
    using SR = System.ServiceModel.SR;
 
    internal sealed class WindowsSspiNegotiation : ISspiNegotiation
    {
        const int DefaultMaxPromptAttempts = 1;
        SspiContextFlags contextFlags;
        SafeFreeCredentials credentialsHandle;
        bool disposed = false;
        bool doMutualAuth;
        TokenImpersonationLevel impersonationLevel;
        bool isCompleted;
        bool isServer;
        LifeSpan lifespan;
        string protocolName;
        SafeDeleteContext securityContext;
        string servicePrincipalName;
        SecSizes sizes;
        Object syncObject = new Object();
        int tokenSize;
        bool interactiveNegoLogonEnabled = true;
        string clientPackageName;
        bool saveClientCredentialsOnSspiUi = true;
        bool allowNtlm;
        int MaxPromptAttempts = 0;
 
        /// <summary>
        /// Client side ctor
        /// </summary>
        internal WindowsSspiNegotiation(string package, SafeFreeCredentials credentialsHandle, TokenImpersonationLevel impersonationLevel, string servicePrincipalName, bool doMutualAuth, bool interactiveLogonEnabled, bool ntlmEnabled)
            : this(false, package, credentialsHandle, impersonationLevel, servicePrincipalName, doMutualAuth, interactiveLogonEnabled, ntlmEnabled)
        { }
 
        /// <summary>
        /// Server side ctor
        /// </summary>
        internal WindowsSspiNegotiation(string package, SafeFreeCredentials credentialsHandle, string defaultServiceBinding)
            : this(true, package, credentialsHandle, TokenImpersonationLevel.Delegation, defaultServiceBinding, false, false, true)
        { }
 
        WindowsSspiNegotiation(bool isServer, string package, SafeFreeCredentials credentialsHandle, TokenImpersonationLevel impersonationLevel, string servicePrincipalName, bool doMutualAuth, bool interactiveLogonEnabled, bool ntlmEnabled)
        {
            this.tokenSize = SspiWrapper.GetVerifyPackageInfo(package).MaxToken;
            this.isServer = isServer;
            this.servicePrincipalName = servicePrincipalName;
            this.securityContext = null;
            if (isServer)
            {
                this.impersonationLevel = TokenImpersonationLevel.Delegation;
                this.doMutualAuth = false;
            }
            else
            {
                this.impersonationLevel = impersonationLevel;
                this.doMutualAuth = doMutualAuth;
                this.interactiveNegoLogonEnabled = interactiveLogonEnabled;
                this.clientPackageName = package;
                this.allowNtlm = ntlmEnabled;
            }
            this.credentialsHandle = credentialsHandle;
        }
 
        public DateTime ExpirationTimeUtc
        {
            get
            {
                ThrowIfDisposed();
                if (this.LifeSpan == null)
                {
                    return SecurityUtils.MaxUtcDateTime;
                }
                else
                {
                    return this.LifeSpan.ExpiryTimeUtc;
                }
            }
        }
 
        public bool IsCompleted
        {
            get
            {
                ThrowIfDisposed();
                return this.isCompleted;
            }
        }
 
        public bool IsDelegationFlag
        {
            get
            {
                ThrowIfDisposed();
                return (this.contextFlags & SspiContextFlags.Delegate) != 0;
            }
        }
 
        public bool IsIdentifyFlag
        {
            get
            {
                ThrowIfDisposed();
                return (this.contextFlags & (this.isServer ? SspiContextFlags.AcceptIdentify : SspiContextFlags.InitIdentify)) != 0;
            }
        }
 
        public bool IsMutualAuthFlag
        {
            get
            {
                ThrowIfDisposed();
                return (this.contextFlags & SspiContextFlags.MutualAuth) != 0;
            }
        }
 
        public bool IsValidContext
        {
            get
            {
                return (this.securityContext != null && this.securityContext.IsInvalid == false);
            }
        }
 
        public string KeyEncryptionAlgorithm
        {
            get
            {
                return SecurityAlgorithms.WindowsSspiKeyWrap;
            }
        }
 
        public LifeSpan LifeSpan
        {
            get
            {
                ThrowIfDisposed();
                if (this.lifespan == null)
                {
                    LifeSpan tmpLifeSpan = (LifeSpan)SspiWrapper.QueryContextAttributes(this.securityContext, ContextAttribute.Lifespan);
 
                    if (IsCompleted)
                    {
                        // cache it only when it's completed
                        this.lifespan = tmpLifeSpan;
                    }
 
                    return tmpLifeSpan;
                }
 
                return this.lifespan;
            }
        }
 
        public string ProtocolName
        {
            get
            {
                ThrowIfDisposed();
                if (this.protocolName == null)
                {
                    NegotiationInfoClass negotiationInfo = SspiWrapper.QueryContextAttributes(this.securityContext, ContextAttribute.NegotiationInfo) as NegotiationInfoClass;
 
                    if (IsCompleted)
                    {
                        // cache it only when it's completed
                        this.protocolName = negotiationInfo.AuthenticationPackage;
                    }
 
                    return negotiationInfo.AuthenticationPackage;
                }
 
                return this.protocolName;
            }
        }
 
        public string ServicePrincipalName
        {
            get
            {
                ThrowIfDisposed();
                return this.servicePrincipalName;
            }
        }
 
        SecSizes SecuritySizes
        {
            get
            {
                ThrowIfDisposed();
                if (this.sizes == null)
                {
                    SecSizes tmpSizes = (SecSizes)SspiWrapper.QueryContextAttributes(this.securityContext, ContextAttribute.Sizes);
 
                    if (IsCompleted)
                    {
                        // cache it only when it's completed
                        this.sizes = tmpSizes;
                    }
 
                    return tmpSizes;
                }
 
                return this.sizes;
            }
        }
 
        public string GetRemoteIdentityName()
        {
            if (!this.isServer)
            {
                return this.servicePrincipalName;
            }
 
            if (IsValidContext)
            {
                using (SafeCloseHandle contextToken = GetContextToken())
                {
                    using (WindowsIdentity windowsIdentity = new WindowsIdentity(contextToken.DangerousGetHandle(), this.ProtocolName))
                    {
                        return windowsIdentity.Name;
                    }
                }
            }
            return String.Empty;
        }
 
        public byte[] Decrypt(byte[] encryptedContent)
        {
            if (encryptedContent == null)
                throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("encryptedContent");
            ThrowIfDisposed();
 
            SecurityBuffer[] securityBuffer = new SecurityBuffer[2];
            securityBuffer[0] = new SecurityBuffer(encryptedContent, 0, encryptedContent.Length, BufferType.Stream);
            securityBuffer[1] = new SecurityBuffer(0, BufferType.Data);
            int errorCode = SspiWrapper.DecryptMessage(this.securityContext, securityBuffer, 0, true);
            if (errorCode != 0)
            {
                throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new Win32Exception(errorCode));
            }
 
            for (int i = 0; i < securityBuffer.Length; ++i)
            {
                if (securityBuffer[i].type == BufferType.Data)
                {
                    return securityBuffer[i].token;
                }
            }
            OnBadData();
            return null;
        }
 
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
 
        public byte[] Encrypt(byte[] input)
        {
            if (input == null)
                throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("input");
            ThrowIfDisposed();
            SecurityBuffer[] securityBuffer = new SecurityBuffer[3];
 
            byte[] tokenBuffer = DiagnosticUtility.Utility.AllocateByteArray(SecuritySizes.SecurityTrailer);
            securityBuffer[0] = new SecurityBuffer(tokenBuffer, 0, tokenBuffer.Length, BufferType.Token);
            byte[] dataBuffer = DiagnosticUtility.Utility.AllocateByteArray(input.Length);
            Buffer.BlockCopy(input, 0, dataBuffer, 0, input.Length);
            securityBuffer[1] = new SecurityBuffer(dataBuffer, 0, dataBuffer.Length, BufferType.Data);
            byte[] paddingBuffer = DiagnosticUtility.Utility.AllocateByteArray(SecuritySizes.BlockSize);
            securityBuffer[2] = new SecurityBuffer(paddingBuffer, 0, paddingBuffer.Length, BufferType.Padding);
 
            int errorCode = SspiWrapper.EncryptMessage(this.securityContext, securityBuffer, 0);
            if (errorCode != 0)
            {
                throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new Win32Exception(errorCode));
            }
 
            int tokenLen = 0;
            int paddingLen = 0;
            for (int i = 0; i < securityBuffer.Length; ++i)
            {
                if (securityBuffer[i].type == BufferType.Token)
                    tokenLen = securityBuffer[i].size;
                else if (securityBuffer[i].type == BufferType.Padding)
                    paddingLen = securityBuffer[i].size;
            }
            byte[] encryptedData = DiagnosticUtility.Utility.AllocateByteArray(checked(tokenLen + dataBuffer.Length + paddingLen));
 
            Buffer.BlockCopy(tokenBuffer, 0, encryptedData, 0, tokenLen);
            Buffer.BlockCopy(dataBuffer, 0, encryptedData, tokenLen, dataBuffer.Length);
            Buffer.BlockCopy(paddingBuffer, 0, encryptedData, tokenLen + dataBuffer.Length, paddingLen);
 
            return encryptedData;
        }
 
        public byte[] GetOutgoingBlob(byte[] incomingBlob, ChannelBinding channelbinding, ExtendedProtectionPolicy protectionPolicy)
        {
            ThrowIfDisposed();
            int statusCode = 0;
 
            // use the confidentiality option to ensure we can encrypt messages
            SspiContextFlags requestedFlags = SspiContextFlags.Confidentiality
                                            | SspiContextFlags.ReplayDetect
                                            | SspiContextFlags.SequenceDetect;
 
            if (this.doMutualAuth)
            {
                requestedFlags |= SspiContextFlags.MutualAuth;
            }
 
            if (this.impersonationLevel == TokenImpersonationLevel.Delegation)
            {
                requestedFlags |= SspiContextFlags.Delegate;
            }
            else if (this.isServer == false && this.impersonationLevel == TokenImpersonationLevel.Identification)
            {
                requestedFlags |= SspiContextFlags.InitIdentify;
            }
            else if (this.isServer == false && this.impersonationLevel == TokenImpersonationLevel.Anonymous)
            {
                requestedFlags |= SspiContextFlags.InitAnonymous;
            }
 
            ExtendedProtectionPolicyHelper policyHelper = new ExtendedProtectionPolicyHelper(channelbinding, protectionPolicy);
 
            if (isServer)
            {
                if (policyHelper.PolicyEnforcement == PolicyEnforcement.Always && policyHelper.ChannelBinding == null && policyHelper.ProtectionScenario != ProtectionScenario.TrustedProxy)
                {
                    throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new SecurityTokenException(SR.GetString(SR.SecurityChannelBindingMissing)));
                }
 
                if (policyHelper.PolicyEnforcement == PolicyEnforcement.WhenSupported)
                {
                    requestedFlags |= SspiContextFlags.ChannelBindingAllowMissingBindings;
                }
 
                if (policyHelper.ProtectionScenario == ProtectionScenario.TrustedProxy)
                {
                    requestedFlags |= SspiContextFlags.ChannelBindingProxyBindings;
                }
            }
 
            List<SecurityBuffer> list = new List<SecurityBuffer>(2);
 
            if (incomingBlob != null)
            {
                list.Add(new SecurityBuffer(incomingBlob, BufferType.Token));
            }
 
            // when deciding if the channel binding should be added to the security buffer
            // it is necessary to differentiate between  client and server.
            // Server rules were added to policyHelper as they are shared with Kerb and I want them consistent
            // Client adds if not null.
            if (this.isServer)
            {
                if (policyHelper.ShouldAddChannelBindingToASC())
                {
                    list.Add(new SecurityBuffer(policyHelper.ChannelBinding));
                }
            }
            else
            {
                if (policyHelper.ChannelBinding != null)
                {
                    list.Add(new SecurityBuffer(policyHelper.ChannelBinding));
                }
            }
 
            SecurityBuffer[] inSecurityBuffer = null;
            if (list.Count > 0)
            {
                inSecurityBuffer = list.ToArray();
            }
 
            SecurityBuffer outSecurityBuffer = new SecurityBuffer(this.tokenSize, BufferType.Token);
 
            if (!this.isServer)
            {
                //client session
                statusCode = SspiWrapper.InitializeSecurityContext(this.credentialsHandle,
                                                                    ref this.securityContext,
                                                                    this.servicePrincipalName,
                                                                    requestedFlags,
                                                                    Endianness.Network,
                                                                    inSecurityBuffer,
                                                                    outSecurityBuffer,
                                                                    ref this.contextFlags);
            }
            else
            {
                // server session
                //This check is to save an unnecessary ASC call.
                bool isServerSecurityContextNull = this.securityContext == null;
                SspiContextFlags serverContextFlags = this.contextFlags;
 
                statusCode = SspiWrapper.AcceptSecurityContext(this.credentialsHandle,
                                                                ref this.securityContext,
                                                                requestedFlags,
                                                                Endianness.Network,
                                                                inSecurityBuffer,
                                                                outSecurityBuffer,
                                                                ref this.contextFlags);
 
                if (statusCode == (int)SecurityStatus.InvalidToken && !isServerSecurityContextNull)
                {
                    // Call again into ASC after deleting the Securitycontext. If this securitycontext is not deleted 
                    // then when the client sends NTLM blob the service will treat it as Nego2blob and will fail to authenticate the client.
                    this.contextFlags = serverContextFlags;
                    CloseContext();
                    statusCode = SspiWrapper.AcceptSecurityContext(this.credentialsHandle,
                                                                    ref this.securityContext,
                                                                    requestedFlags,
                                                                    Endianness.Network,
                                                                    inSecurityBuffer,
                                                                    outSecurityBuffer,
                                                                    ref this.contextFlags);
                }
            }
 
            if (DiagnosticUtility.ShouldTraceInformation)
            {
                IMD.SecurityTraceRecordHelper.TraceChannelBindingInformation(policyHelper, this.isServer, channelbinding);
            }
 
            if ((statusCode & unchecked((int)0x80000000)) != 0)
            {
                if (!this.isServer
                    && this.interactiveNegoLogonEnabled
                    && SecurityUtils.IsOSGreaterThanOrEqualToWin7()
                    && SspiWrapper.IsSspiPromptingNeeded((uint)statusCode)
                    && SspiWrapper.IsNegotiateExPackagePresent())
                {
                    // If we have prompted enough number of times (DefaultMaxPromptAttempts) with wrong credentials, then we do not prompt again and throw. 
                    if (MaxPromptAttempts >= DefaultMaxPromptAttempts)
                    {
                        throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new Win32Exception(statusCode, SR.GetString(SR.InvalidClientCredentials)));
                    }
 
                    IntPtr ppAuthIdentity = IntPtr.Zero;
                    uint errorCode = SspiWrapper.SspiPromptForCredential(this.servicePrincipalName, this.clientPackageName, out ppAuthIdentity, ref this.saveClientCredentialsOnSspiUi);
                    if (errorCode == (uint)CredentialStatus.Success)
                    {
                        IntPtr ppNewAuthIdentity = IntPtr.Zero;
 
                        if (!this.allowNtlm)
                        {
                            // When Ntlm is  explicitly disabled we don't want the collected 
                            //creds from the Kerb/NTLM tile to be used for NTLM auth.
 
                            uint status = UnsafeNativeMethods.SspiExcludePackage(ppAuthIdentity, "NTLM", out ppNewAuthIdentity);
                        }
                        else
                        {
                            ppNewAuthIdentity = ppAuthIdentity;
                        }
 
                        this.credentialsHandle = SspiWrapper.AcquireCredentialsHandle(this.clientPackageName, CredentialUse.Outbound, ref ppNewAuthIdentity);
 
                        if (IntPtr.Zero != ppNewAuthIdentity)
                        {
                            UnsafeNativeMethods.SspiFreeAuthIdentity(ppNewAuthIdentity);
                        }
 
                        CloseContext();
 
                        MaxPromptAttempts++;
                        return this.GetOutgoingBlob(null, channelbinding, protectionPolicy);
                    }
                    else
                    {
                        // Call into SspiPromptForCredential had an error. Time to throw.
                        if (IntPtr.Zero != ppAuthIdentity)
                        {
                            UnsafeNativeMethods.SspiFreeAuthIdentity(ppAuthIdentity);
                        }
 
                        CloseContext();
                        this.isCompleted = true;
                        throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new Win32Exception((int)errorCode, SR.GetString(SR.SspiErrorOrInvalidClientCredentials)));
                    }
                }
 
                CloseContext();
                this.isCompleted = true;
                if (!this.isServer && (statusCode == (int)SecurityStatus.TargetUnknown
                    || statusCode == (int)SecurityStatus.WrongPrincipal))
                {
                    throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new Win32Exception(statusCode, SR.GetString(SR.IncorrectSpnOrUpnSpecified, this.servicePrincipalName)));
                }
                else
                {
                    throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new Win32Exception(statusCode, SR.GetString(SR.InvalidSspiNegotiation)));
                }
            }
 
            if (DiagnosticUtility.ShouldTraceInformation)
            {
                if (this.isServer)
                {
                    SecurityTraceRecordHelper.TraceServiceOutgoingSpnego(this);
                }
                else
                {
                    SecurityTraceRecordHelper.TraceClientOutgoingSpnego(this);
                }
            }
 
            if (statusCode == (int)SecurityStatus.OK)
            {
                // we're done
                this.isCompleted = true;
 
                // These must all be true to check service binding
                // 1. we are the service (listener)
                // 2. caller is not anonymous
                // 3. protocol is not Kerberos
                // 4. policy is set to check service binding
                // 
                if (isServer && ((this.contextFlags & SspiContextFlags.AcceptAnonymous) == 0) && (string.Compare(this.ProtocolName, NegotiationInfoClass.Kerberos, StringComparison.OrdinalIgnoreCase) != 0) && policyHelper.ShouldCheckServiceBinding)
                {
                    // in the server case the servicePrincipalName is the defaultServiceBinding
                   
                    if (DiagnosticUtility.ShouldTraceInformation)
                    {
                        string serviceBindingNameSentByClient;
                        SspiWrapper.QuerySpecifiedTarget(securityContext, out serviceBindingNameSentByClient);
                        IMD.SecurityTraceRecordHelper.TraceServiceNameBindingOnServer( serviceBindingNameSentByClient, this.servicePrincipalName, policyHelper.ServiceNameCollection);
                    }
                    
                    policyHelper.CheckServiceBinding(this.securityContext, this.servicePrincipalName);
                }
            }
            else
            {
                // we need to continue
            }
 
            return outSecurityBuffer.token;
        }
 
        public void ImpersonateContext()
        {
            ThrowIfDisposed();
            if (!IsValidContext)
            {
                throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new Win32Exception((int)SecurityStatus.InvalidHandle));
            }
 
            SspiWrapper.ImpersonateSecurityContext(this.securityContext);
        }
 
        internal void CloseContext()
        {
            ThrowIfDisposed();
            try
            {
                if (this.securityContext != null)
                {
                    this.securityContext.Close();
                }
            }
            finally
            {
                this.securityContext = null;
            }
        }
 
        private void Dispose(bool disposing)
        {
            lock (this.syncObject)
            {
                if (this.disposed == false)
                {
                    if (disposing)
                    {
                        this.CloseContext();
                    }
 
                    // set to null any references that aren't finalizable
                    this.protocolName = null;
                    this.servicePrincipalName = null;
                    this.sizes = null;
                    this.disposed = true;
                }
            }
        }
 
        internal SafeCloseHandle GetContextToken()
        {
            if (!IsValidContext)
            {
                throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new Win32Exception((int)SecurityStatus.InvalidHandle));
            }
 
            SafeCloseHandle token;
            SecurityStatus status = (SecurityStatus)SspiWrapper.QuerySecurityContextToken(this.securityContext, out token);
            if (status != SecurityStatus.OK)
            {
                Utility.CloseInvalidOutSafeHandle(token);
                throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new Win32Exception((int)status));
            }
            return token;
        }
 
        void OnBadData()
        {
            throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new MessageSecurityException(SR.GetString(SR.BadData)));
        }
 
        void ThrowIfDisposed()
        {
            lock (this.syncObject)
            {
                if (this.disposed)
                {
                    throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new ObjectDisposedException(null));
                }
            }
        }
    }
}