diff --git a/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets b/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets index d417e7b91ec1d7f550e4a9ad4c66d96a5485393e..5e4c002898f643547499def00c5de8826d8c835b 100644 --- a/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets +++ b/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets @@ -279,6 +279,9 @@ The .NET Foundation licenses this file to you under the MIT license. + + + diff --git a/src/libraries/Common/src/System/Net/Security/Unix/SafeDeleteNegoContext.cs b/src/libraries/Common/src/System/Net/Security/Unix/SafeDeleteNegoContext.cs deleted file mode 100644 index 45be4535ec73b23b07e36cae4c7d91937f8c2b85..0000000000000000000000000000000000000000 --- a/src/libraries/Common/src/System/Net/Security/Unix/SafeDeleteNegoContext.cs +++ /dev/null @@ -1,115 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Text; -using Microsoft.Win32.SafeHandles; - -namespace System.Net.Security -{ - internal sealed class SafeDeleteNegoContext : SafeDeleteContext - { - private SafeGssCredHandle? _acceptorCredential; - private SafeGssNameHandle? _targetName; - private SafeGssContextHandle _context; - private bool _isNtlmUsed; - private readonly SafeFreeNegoCredentials? _credential; - - public SafeGssCredHandle AcceptorCredential - { - get - { - _acceptorCredential ??= SafeGssCredHandle.CreateAcceptor(); - return _acceptorCredential; - } - } - - public SafeGssNameHandle? TargetName - { - get { return _targetName; } - } - - // Property represents if final protocol negotiated is Ntlm or not. - public bool IsNtlmUsed - { - get { return _isNtlmUsed; } - } - - public SafeGssContextHandle GssContext - { - get { return _context; } - } - - public SafeDeleteNegoContext(SafeFreeNegoCredentials credential) - : base(IntPtr.Zero) - { - Debug.Assert((null != credential), "Null credential in SafeDeleteNegoContext"); - bool added = false; - credential.DangerousAddRef(ref added); - _credential = credential; - _context = new SafeGssContextHandle(); - } - - public SafeDeleteNegoContext(SafeFreeNegoCredentials credential, string targetName) - : base(IntPtr.Zero) - { - try - { - _targetName = SafeGssNameHandle.CreateTarget(targetName); - _context = new SafeGssContextHandle(); - } - catch - { - Dispose(); - throw; - } - _credential = credential; - bool ignore = false; - _credential.DangerousAddRef(ref ignore); - } - - public void SetGssContext(SafeGssContextHandle context) - { - _context = context; - } - - public void SetAuthenticationPackage(bool isNtlmUsed) - { - _isNtlmUsed = isNtlmUsed; - } - - public override bool IsInvalid - { - get { return (null == _credential); } - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - _context.Dispose(); - - if (_targetName != null) - { - _targetName.Dispose(); - _targetName = null; - } - - if (_acceptorCredential != null) - { - _acceptorCredential.Dispose(); - _acceptorCredential = null; - } - } - base.Dispose(disposing); - } - - protected override bool ReleaseHandle() - { - _credential?.DangerousRelease(); - return true; - } - } -} diff --git a/src/libraries/Common/src/System/Net/Security/Unix/SafeFreeNegoCredentials.cs b/src/libraries/Common/src/System/Net/Security/Unix/SafeFreeNegoCredentials.cs deleted file mode 100644 index e29e9b48cfdefb9618ce9ac6e27fd881087f478d..0000000000000000000000000000000000000000 --- a/src/libraries/Common/src/System/Net/Security/Unix/SafeFreeNegoCredentials.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Text; -using Microsoft.Win32.SafeHandles; - -namespace System.Net.Security -{ - internal sealed class SafeFreeNegoCredentials : SafeFreeCredentials - { - private SafeGssCredHandle _credential; - private readonly Interop.NetSecurityNative.PackageType _packageType; - private readonly string _userName; - private readonly bool _isDefault; - - public SafeGssCredHandle GssCredential - { - get { return _credential; } - } - - // Property represents which protocol is specified (Negotiate, Ntlm or Kerberos). - public Interop.NetSecurityNative.PackageType PackageType - { - get { return _packageType; } - } - - public string UserName - { - get { return _userName; } - } - - public bool IsDefault - { - get { return _isDefault; } - } - - public SafeFreeNegoCredentials(Interop.NetSecurityNative.PackageType packageType, string username, string password, ReadOnlySpan domain) - : base(IntPtr.Zero, true) - { - Debug.Assert(username != null && password != null, "Username and Password can not be null"); - - // any invalid user format will not be manipulated and passed as it is. - int index = username.IndexOf('\\'); - if (index > 0 && username.IndexOf('\\', index + 1) < 0 && domain.IsEmpty) - { - domain = username.AsSpan(0, index); - username = username.Substring(index + 1); - } - - // remove any leading and trailing whitespace - username = username.Trim(); - domain = domain.Trim(); - if (!username.Contains('@') && !domain.IsEmpty) - { - username = string.Concat(username, "@", domain); - } - - bool ignore = false; - _packageType = packageType; - _userName = username; - _isDefault = string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password); - _credential = SafeGssCredHandle.Create(username, password, packageType); - _credential.DangerousAddRef(ref ignore); - } - - public override bool IsInvalid - { - get { return (null == _credential); } - } - - protected override bool ReleaseHandle() - { - _credential.DangerousRelease(); - _credential = null!; - return true; - } - } -} diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs index 5ec6a53015cb27214703bb4b1ccccba511828d01..780db637ba335e289690a81a0b1de75ccc72b6c5 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/NtAuthTests.FakeServer.cs @@ -115,6 +115,7 @@ internal static async Task HandleAuthenticationRequestWithFakeServer(LoopbackSer [ConditionalTheory(nameof(IsNtlmAvailable))] [InlineData(true)] [InlineData(false)] + [SkipOnPlatform(TestPlatforms.Browser, "Credentials and HttpListener is not supported on Browser")] public async Task DefaultHandler_FakeServer_Success(bool useNtlm) { await LoopbackServer.CreateClientAndServerAsync( diff --git a/src/libraries/System.Net.Security/src/ILLink/ILLink.Substitutions.xml b/src/libraries/System.Net.Security/src/ILLink/ILLink.Substitutions.xml new file mode 100644 index 0000000000000000000000000000000000000000..8c56e81a9bb76838b4217dbd3bda29e7430316d6 --- /dev/null +++ b/src/libraries/System.Net.Security/src/ILLink/ILLink.Substitutions.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/libraries/System.Net.Security/src/System.Net.Security.csproj b/src/libraries/System.Net.Security/src/System.Net.Security.csproj index 9cce5ff1e045b0630f2a5926584a9c7fb512912f..a06dbd79b8f678465b6b117bf656c2b8426a34d9 100644 --- a/src/libraries/System.Net.Security/src/System.Net.Security.csproj +++ b/src/libraries/System.Net.Security/src/System.Net.Security.csproj @@ -18,7 +18,7 @@ $(DefineConstants);SYSNETSECURITY_NO_OPENSSL ReferenceAssemblyExclusions.txt - + @@ -29,6 +29,8 @@ + + @@ -57,9 +59,7 @@ - - @@ -107,8 +107,6 @@ Link="Common\System\Net\Security\SSPIHandleCache.cs" /> - + - @@ -171,7 +169,6 @@ Link="Common\System\Net\Security\SecurityBufferType.Windows.cs" /> - @@ -287,13 +284,10 @@ + + - - - - - - + + + + + - - - + diff --git a/src/libraries/System.Net.Security/src/System/Net/ContextFlagsAdapterPal.Unix.cs b/src/libraries/System.Net.Security/src/System/Net/ContextFlagsAdapterPal.Unix.cs deleted file mode 100644 index 6ded2dcc321f484454965a60f487331120b7f407..0000000000000000000000000000000000000000 --- a/src/libraries/System.Net.Security/src/System/Net/ContextFlagsAdapterPal.Unix.cs +++ /dev/null @@ -1,114 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -namespace System.Net -{ - internal static class ContextFlagsAdapterPal - { - private readonly struct ContextFlagMapping - { - public readonly Interop.NetSecurityNative.GssFlags GssFlags; - public readonly ContextFlagsPal ContextFlag; - - public ContextFlagMapping(Interop.NetSecurityNative.GssFlags gssFlag, ContextFlagsPal contextFlag) - { - GssFlags = gssFlag; - ContextFlag = contextFlag; - } - } - - private static readonly ContextFlagMapping[] s_contextFlagMapping = new[] - { - new ContextFlagMapping(Interop.NetSecurityNative.GssFlags.GSS_C_CONF_FLAG, ContextFlagsPal.Confidentiality), - new ContextFlagMapping(Interop.NetSecurityNative.GssFlags.GSS_C_MUTUAL_FLAG, ContextFlagsPal.MutualAuth), - new ContextFlagMapping(Interop.NetSecurityNative.GssFlags.GSS_C_REPLAY_FLAG, ContextFlagsPal.ReplayDetect), - new ContextFlagMapping(Interop.NetSecurityNative.GssFlags.GSS_C_SEQUENCE_FLAG, ContextFlagsPal.SequenceDetect), - new ContextFlagMapping(Interop.NetSecurityNative.GssFlags.GSS_C_DELEG_FLAG, ContextFlagsPal.Delegate) - }; - - - internal static ContextFlagsPal GetContextFlagsPalFromInterop(Interop.NetSecurityNative.GssFlags gssFlags, bool isServer) - { - ContextFlagsPal flags = ContextFlagsPal.None; - - // GSS_C_IDENTIFY_FLAG is handled separately as its value can either be AcceptIdentify (used by server) or InitIdentify (used by client) - if ((gssFlags & Interop.NetSecurityNative.GssFlags.GSS_C_IDENTIFY_FLAG) != 0) - { - flags |= isServer ? ContextFlagsPal.AcceptIdentify : ContextFlagsPal.InitIdentify; - } - - foreach (ContextFlagMapping mapping in s_contextFlagMapping) - { - if ((gssFlags & mapping.GssFlags) == mapping.GssFlags) - { - flags |= mapping.ContextFlag; - } - } - - // GSS_C_INTEG_FLAG is handled separately as its value can either be AcceptIntegrity (used by server) or InitIntegrity (used by client) - if ((gssFlags & Interop.NetSecurityNative.GssFlags.GSS_C_INTEG_FLAG) != 0) - { - flags |= isServer ? ContextFlagsPal.AcceptIntegrity : ContextFlagsPal.InitIntegrity; - } - - foreach (ContextFlagMapping mapping in s_contextFlagMapping) - { - if ((gssFlags & mapping.GssFlags) == mapping.GssFlags) - { - flags |= mapping.ContextFlag; - } - } - - return flags; - } - - internal static Interop.NetSecurityNative.GssFlags GetInteropFromContextFlagsPal(ContextFlagsPal flags, bool isServer) - { - Interop.NetSecurityNative.GssFlags gssFlags = 0; - - // GSS_C_IDENTIFY_FLAG is set if either AcceptIdentify (used by server) or InitIdentify (used by client) is set - if (isServer) - { - if ((flags & ContextFlagsPal.AcceptIdentify) != 0) - { - gssFlags |= Interop.NetSecurityNative.GssFlags.GSS_C_IDENTIFY_FLAG; - } - } - else - { - if ((flags & ContextFlagsPal.InitIdentify) != 0) - { - gssFlags |= Interop.NetSecurityNative.GssFlags.GSS_C_IDENTIFY_FLAG; - } - } - - // GSS_C_INTEG_FLAG is set if either AcceptIntegrity (used by server) or InitIntegrity (used by client) is set - if (isServer) - { - if ((flags & ContextFlagsPal.AcceptIntegrity) != 0) - { - gssFlags |= Interop.NetSecurityNative.GssFlags.GSS_C_INTEG_FLAG; - } - } - else - { - if ((flags & ContextFlagsPal.InitIntegrity) != 0) - { - gssFlags |= Interop.NetSecurityNative.GssFlags.GSS_C_INTEG_FLAG; - } - } - - foreach (ContextFlagMapping mapping in s_contextFlagMapping) - { - if ((flags & mapping.ContextFlag) == mapping.ContextFlag) - { - gssFlags |= mapping.GssFlags; - } - } - - return gssFlags; - } - } -} diff --git a/src/libraries/System.Net.Security/src/System/Net/ContextFlagsAdapterPal.Windows.cs b/src/libraries/System.Net.Security/src/System/Net/ContextFlagsAdapterPal.Windows.cs deleted file mode 100644 index 5ce5bf0410d730c53913787f256a914574900415..0000000000000000000000000000000000000000 --- a/src/libraries/System.Net.Security/src/System/Net/ContextFlagsAdapterPal.Windows.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -namespace System.Net -{ - internal static class ContextFlagsAdapterPal - { - private readonly struct ContextFlagMapping - { - public readonly Interop.SspiCli.ContextFlags Win32Flag; - public readonly ContextFlagsPal ContextFlag; - - public ContextFlagMapping(Interop.SspiCli.ContextFlags win32Flag, ContextFlagsPal contextFlag) - { - Win32Flag = win32Flag; - ContextFlag = contextFlag; - } - } - - private static readonly ContextFlagMapping[] s_contextFlagMapping = new[] - { - new ContextFlagMapping(Interop.SspiCli.ContextFlags.AcceptExtendedError, ContextFlagsPal.AcceptExtendedError), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.AcceptIdentify, ContextFlagsPal.AcceptIdentify), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.AcceptIntegrity, ContextFlagsPal.AcceptIntegrity), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.AcceptStream, ContextFlagsPal.AcceptStream), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.AllocateMemory, ContextFlagsPal.AllocateMemory), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.AllowMissingBindings, ContextFlagsPal.AllowMissingBindings), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.Confidentiality, ContextFlagsPal.Confidentiality), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.Connection, ContextFlagsPal.Connection), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.Delegate, ContextFlagsPal.Delegate), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.InitExtendedError, ContextFlagsPal.InitExtendedError), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.InitIdentify, ContextFlagsPal.InitIdentify), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.InitManualCredValidation, ContextFlagsPal.InitManualCredValidation), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.InitIntegrity, ContextFlagsPal.InitIntegrity), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.InitStream, ContextFlagsPal.InitStream), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.InitUseSuppliedCreds, ContextFlagsPal.InitUseSuppliedCreds), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.MutualAuth, ContextFlagsPal.MutualAuth), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.ProxyBindings, ContextFlagsPal.ProxyBindings), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.ReplayDetect, ContextFlagsPal.ReplayDetect), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.SequenceDetect, ContextFlagsPal.SequenceDetect), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.UnverifiedTargetName, ContextFlagsPal.UnverifiedTargetName), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.UseSessionKey, ContextFlagsPal.UseSessionKey), - new ContextFlagMapping(Interop.SspiCli.ContextFlags.Zero, ContextFlagsPal.None), - }; - - internal static ContextFlagsPal GetContextFlagsPalFromInterop(Interop.SspiCli.ContextFlags win32Flags) - { - ContextFlagsPal flags = ContextFlagsPal.None; - foreach (ContextFlagMapping mapping in s_contextFlagMapping) - { - if ((win32Flags & mapping.Win32Flag) == mapping.Win32Flag) - { - flags |= mapping.ContextFlag; - } - } - - return flags; - } - - internal static Interop.SspiCli.ContextFlags GetInteropFromContextFlagsPal(ContextFlagsPal flags) - { - Interop.SspiCli.ContextFlags win32Flags = Interop.SspiCli.ContextFlags.Zero; - foreach (ContextFlagMapping mapping in s_contextFlagMapping) - { - if ((flags & mapping.ContextFlag) == mapping.ContextFlag) - { - win32Flags |= mapping.Win32Flag; - } - } - - return win32Flags; - } - } -} diff --git a/src/libraries/System.Net.Security/src/System/Net/ContextFlagsPal.cs b/src/libraries/System.Net.Security/src/System/Net/ContextFlagsPal.cs deleted file mode 100644 index 6dad121c15352398ae549b51b87d68f3f0522c0c..0000000000000000000000000000000000000000 --- a/src/libraries/System.Net.Security/src/System/Net/ContextFlagsPal.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -namespace System.Net -{ - [Flags] - internal enum ContextFlagsPal - { - None = 0, - Delegate = 0x00000001, - MutualAuth = 0x00000002, - ReplayDetect = 0x00000004, - SequenceDetect = 0x00000008, - Confidentiality = 0x00000010, - UseSessionKey = 0x00000020, - AllocateMemory = 0x00000100, - Connection = 0x00000800, - InitExtendedError = 0x00004000, - AcceptExtendedError = 0x00008000, - InitStream = 0x00008000, - AcceptStream = 0x00010000, - InitIntegrity = 0x00010000, - AcceptIntegrity = 0x00020000, - InitManualCredValidation = 0x00080000, - InitUseSuppliedCreds = 0x00000080, - InitIdentify = 0x00020000, - AcceptIdentify = 0x00080000, - ProxyBindings = 0x04000000, - AllowMissingBindings = 0x10000000, - UnverifiedTargetName = 0x20000000, - } -} diff --git a/src/libraries/System.Net.Security/src/System/Net/NTAuthentication.Common.cs b/src/libraries/System.Net.Security/src/System/Net/NTAuthentication.Common.cs deleted file mode 100644 index 26351a41f1731b3874af4adb7904f700e3e7969e..0000000000000000000000000000000000000000 --- a/src/libraries/System.Net.Security/src/System/Net/NTAuthentication.Common.cs +++ /dev/null @@ -1,343 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Buffers; -using System.ComponentModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Net.Security; -using System.Runtime.InteropServices; -using System.Runtime.Versioning; -using System.Security.Authentication.ExtendedProtection; - -namespace System.Net -{ - internal sealed partial class NTAuthentication - { - private bool _isServer; - - private SafeFreeCredentials? _credentialsHandle; - private SafeDeleteContext? _securityContext; - private string? _spn; - - private int _tokenSize; - private byte[]? _tokenBuffer; - private ContextFlagsPal _requestedContextFlags; - private ContextFlagsPal _contextFlags; - - private bool _isCompleted; - private string _package; - private string? _lastProtocolName; - private string? _protocolName; - private string? _clientSpecifiedSpn; - - private ChannelBinding? _channelBinding; - - // If set, no more calls should be made. - internal bool IsCompleted => _isCompleted; - internal bool IsValidContext => !(_securityContext == null || _securityContext.IsInvalid); - internal string Package => _package; - - // True indicates this instance is for Server and will use AcceptSecurityContext SSPI API. - internal bool IsServer => _isServer; - - internal string? ClientSpecifiedSpn => _clientSpecifiedSpn ??= GetClientSpecifiedSpn(); - - internal string ProtocolName - { - get - { - // Note: May return string.Empty if the auth is not done yet or failed. - if (_protocolName == null) - { - string? negotiationAuthenticationPackage = null; - - if (IsValidContext) - { - negotiationAuthenticationPackage = NegotiateStreamPal.QueryContextAuthenticationPackage(_securityContext!); - if (IsCompleted) - { - _protocolName = negotiationAuthenticationPackage; - } - } - - return negotiationAuthenticationPackage ?? string.Empty; - } - - return _protocolName; - } - } - - internal bool IsKerberos - { - get - { - _lastProtocolName ??= ProtocolName; - return _lastProtocolName == NegotiationInfoClass.Kerberos; - } - } - - internal bool IsNTLM - { - get - { - _lastProtocolName ??= ProtocolName; - return _lastProtocolName == NegotiationInfoClass.NTLM; - } - } - - // - // This overload does not attempt to impersonate because the caller either did it already or the original thread context is still preserved. - // - internal NTAuthentication(bool isServer, string package, NetworkCredential credential, string? spn, ContextFlagsPal requestedContextFlags, ChannelBinding? channelBinding) - { - Initialize(isServer, package, credential, spn, requestedContextFlags, channelBinding); - } - - [MemberNotNull(nameof(_package))] - private void Initialize(bool isServer, string package, NetworkCredential credential, string? spn, ContextFlagsPal requestedContextFlags, ChannelBinding? channelBinding) - { - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"package={package}, spn={spn}, requestedContextFlags={requestedContextFlags}"); - - _tokenSize = NegotiateStreamPal.QueryMaxTokenSize(package); - _isServer = isServer; - _spn = spn; - _securityContext = null; - _requestedContextFlags = requestedContextFlags; - _package = package; - _channelBinding = channelBinding; - - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Peer SPN-> '{_spn}'"); - - // - // Check if we're using DefaultCredentials. - // - - Debug.Assert(CredentialCache.DefaultCredentials == CredentialCache.DefaultNetworkCredentials); - if (credential == CredentialCache.DefaultCredentials) - { - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "using DefaultCredentials"); - _credentialsHandle = NegotiateStreamPal.AcquireDefaultCredential(package, _isServer); - } - else - { - _credentialsHandle = NegotiateStreamPal.AcquireCredentialsHandle(package, _isServer, credential); - } - } - - internal SafeDeleteContext? GetContext(out SecurityStatusPal status) - { - status = new SecurityStatusPal(SecurityStatusPalErrorCode.OK); - Debug.Assert(IsCompleted && IsValidContext, "Should be called only when completed with success, currently is not!"); - Debug.Assert(IsServer, "The method must not be called by the client side!"); - - if (!IsValidContext) - { - status = new SecurityStatusPal(SecurityStatusPalErrorCode.InvalidHandle); - return null; - } - - return _securityContext; - } - - internal void CloseContext() - { - if (_securityContext != null && !_securityContext.IsClosed) - { - _securityContext.Dispose(); - } - _isCompleted = false; - } - - internal NegotiateAuthenticationStatusCode Wrap(ReadOnlySpan input, IBufferWriter outputWriter, bool requestEncryption, out bool isEncrypted) - { - return NegotiateStreamPal.Wrap(_securityContext!, input, outputWriter, requestEncryption, out isEncrypted); - } - - internal NegotiateAuthenticationStatusCode Unwrap(ReadOnlySpan input, IBufferWriter outputWriter, out bool wasEncrypted) - { - return NegotiateStreamPal.Unwrap(_securityContext!, input, outputWriter, out wasEncrypted); - } - - internal NegotiateAuthenticationStatusCode UnwrapInPlace(Span input, out int unwrappedOffset, out int unwrappedLength, out bool wasEncrypted) - { - return NegotiateStreamPal.UnwrapInPlace(_securityContext!, input, out unwrappedOffset, out unwrappedLength, out wasEncrypted); - } - - internal bool VerifyMIC(ReadOnlySpan message, ReadOnlySpan signature) - { - return NegotiateStreamPal.VerifyMIC(_securityContext!, (_contextFlags & ContextFlagsPal.Confidentiality) != 0, message, signature); - } - - internal void GetMIC(ReadOnlySpan message, IBufferWriter signature) - { - NegotiateStreamPal.GetMIC(_securityContext!, (_contextFlags & ContextFlagsPal.Confidentiality) != 0, message, signature); - } - - internal string? GetOutgoingBlob(string? incomingBlob) - { - return GetOutgoingBlob(incomingBlob, throwOnError: true, out _); - } - - internal string? GetOutgoingBlob(string? incomingBlob, bool throwOnError, out SecurityStatusPal statusCode) - { - byte[]? decodedIncomingBlob = null; - if (incomingBlob != null && incomingBlob.Length > 0) - { - decodedIncomingBlob = Convert.FromBase64String(incomingBlob); - } - byte[]? decodedOutgoingBlob = null; - - if ((IsValidContext || IsCompleted) && decodedIncomingBlob == null) - { - // we tried auth previously, now we got a null blob, we're done. this happens - // with Kerberos & valid credentials on the domain but no ACLs on the resource - _isCompleted = true; - statusCode = new SecurityStatusPal(SecurityStatusPalErrorCode.OK); - } - else - { - decodedOutgoingBlob = GetOutgoingBlob(decodedIncomingBlob, throwOnError, out statusCode); - } - - string? outgoingBlob = null; - if (decodedOutgoingBlob != null && decodedOutgoingBlob.Length > 0) - { - outgoingBlob = Convert.ToBase64String(decodedOutgoingBlob); - } - - if (IsCompleted) - { - CloseContext(); - } - - return outgoingBlob; - } - - internal byte[]? GetOutgoingBlob(byte[]? incomingBlob, bool throwOnError) - { - return GetOutgoingBlob(incomingBlob.AsSpan(), throwOnError, out _); - } - - // Accepts an incoming binary security blob and returns an outgoing binary security blob. - internal byte[]? GetOutgoingBlob(byte[]? incomingBlob, bool throwOnError, out SecurityStatusPal statusCode) - { - return GetOutgoingBlob(incomingBlob.AsSpan(), throwOnError, out statusCode); - } - - internal byte[]? GetOutgoingBlob(ReadOnlySpan incomingBlob, bool throwOnError, out SecurityStatusPal statusCode) - { - _tokenBuffer ??= _tokenSize == 0 ? Array.Empty() : new byte[_tokenSize]; - - bool firstTime = _securityContext == null; - int resultBlobLength; - try - { - if (!_isServer) - { - // client session - statusCode = NegotiateStreamPal.InitializeSecurityContext( - ref _credentialsHandle!, - ref _securityContext, - _spn, - _requestedContextFlags, - incomingBlob, - _channelBinding, - ref _tokenBuffer, - out resultBlobLength, - ref _contextFlags); - - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"SSPIWrapper.InitializeSecurityContext() returns statusCode:0x{((int)statusCode.ErrorCode):x8} ({statusCode})"); - - if (statusCode.ErrorCode == SecurityStatusPalErrorCode.CompleteNeeded) - { - statusCode = NegotiateStreamPal.CompleteAuthToken(ref _securityContext, _tokenBuffer.AsSpan(0, resultBlobLength)); - - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"SSPIWrapper.CompleteAuthToken() returns statusCode:0x{((int)statusCode.ErrorCode):x8} ({statusCode})"); - - resultBlobLength = 0; - } - } - else - { - // Server session. - statusCode = NegotiateStreamPal.AcceptSecurityContext( - _credentialsHandle, - ref _securityContext, - _requestedContextFlags, - incomingBlob, - _channelBinding, - ref _tokenBuffer, - out resultBlobLength, - ref _contextFlags); - - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"SSPIWrapper.AcceptSecurityContext() returns statusCode:0x{((int)statusCode.ErrorCode):x8} ({statusCode})"); - } - } - finally - { - // - // Assuming the ISC or ASC has referenced the credential on the first successful call, - // we want to decrement the effective ref count by "disposing" it. - // The real dispose will happen when the security context is closed. - // Note if the first call was not successful the handle is physically destroyed here. - // - if (firstTime) - { - _credentialsHandle?.Dispose(); - } - } - - - if (((int)statusCode.ErrorCode >= (int)SecurityStatusPalErrorCode.OutOfMemory)) - { - CloseContext(); - _isCompleted = true; - _tokenBuffer = null; - if (throwOnError) - { - throw NegotiateStreamPal.CreateExceptionFromError(statusCode); - } - - return null; - } - else if (firstTime && _credentialsHandle != null) - { - // Cache until it is pushed out by newly incoming handles. - SSPIHandleCache.CacheCredential(_credentialsHandle); - } - - byte[]? result = - resultBlobLength == 0 || _tokenBuffer == null ? null : - _tokenBuffer.Length == resultBlobLength ? _tokenBuffer : - _tokenBuffer[0..resultBlobLength]; - - // The return value will tell us correctly if the handshake is over or not - if (statusCode.ErrorCode == SecurityStatusPalErrorCode.OK - || (_isServer && statusCode.ErrorCode == SecurityStatusPalErrorCode.CompleteNeeded)) - { - // Success. - _isCompleted = true; - _tokenBuffer = null; - } - else - { - // We need to continue. - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"need continue statusCode:0x{((int)statusCode.ErrorCode):x8} ({statusCode}) _securityContext:{_securityContext}"); - } - - return result; - } - - private string? GetClientSpecifiedSpn() - { - Debug.Assert(IsValidContext && IsCompleted, "Trying to get the client SPN before handshaking is done!"); - - string? spn = NegotiateStreamPal.QueryContextClientSpecifiedSpn(_securityContext!); - - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"The client specified SPN is [{spn}]"); - - return spn; - } - } -} diff --git a/src/libraries/System.Net.Security/src/System/Net/NTAuthentication.Managed.cs b/src/libraries/System.Net.Security/src/System/Net/NTAuthentication.Managed.cs deleted file mode 100644 index 50bc6573190979c31d43ca3d606d3cc0aeced6e4..0000000000000000000000000000000000000000 --- a/src/libraries/System.Net.Security/src/System/Net/NTAuthentication.Managed.cs +++ /dev/null @@ -1,1116 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Buffers; -using System.Buffers.Binary; -using System.ComponentModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Formats.Asn1; -using System.Net.Security; -using System.Runtime.InteropServices; -using System.Runtime.Versioning; -using System.Security.Authentication.ExtendedProtection; -using System.Security.Cryptography; -using System.Text; - -// NTLM uses all sorts of broken cryptographic primitives (HMAC-MD5, MD4, RC4) -#pragma warning disable CA5351 - -namespace System.Net -{ - internal sealed partial class NTAuthentication - { - // Input parameters - private readonly bool _isSpNego; - private readonly NetworkCredential _credential; - private readonly string? _spn; - private readonly ChannelBinding? _channelBinding; - private readonly ContextFlagsPal _contextFlags; - - // State parameters - private byte[]? _spnegoMechList; - private byte[]? _negotiateMessage; - private byte[]? _clientSigningKey; - private byte[]? _serverSigningKey; - private byte[]? _clientSealingKey; - private byte[]? _serverSealingKey; - private RC4? _clientSeal; - private RC4? _serverSeal; - private uint _clientSequenceNumber; - private uint _serverSequenceNumber; - public bool IsCompleted { get; set; } - - // value should match the Windows sspicli NTE_FAIL value - // defined in winerror.h - private const int NTE_FAIL = unchecked((int)0x80090020); - - private static ReadOnlySpan NtlmHeader => "NTLMSSP\0"u8; - - private static ReadOnlySpan ClientSigningKeyMagic => "session key to client-to-server signing key magic constant\0"u8; - private static ReadOnlySpan ServerSigningKeyMagic => "session key to server-to-client signing key magic constant\0"u8; - private static ReadOnlySpan ClientSealingKeyMagic => "session key to client-to-server sealing key magic constant\0"u8; - private static ReadOnlySpan ServerSealingKeyMagic => "session key to server-to-client sealing key magic constant\0"u8; - - private static readonly byte[] s_workstation = Encoding.Unicode.GetBytes(Environment.MachineName); - - private static SecurityStatusPal SecurityStatusPalOk = new SecurityStatusPal(SecurityStatusPalErrorCode.OK); - private static SecurityStatusPal SecurityStatusPalContinueNeeded = new SecurityStatusPal(SecurityStatusPalErrorCode.ContinueNeeded); - private static SecurityStatusPal SecurityStatusPalInvalidToken = new SecurityStatusPal(SecurityStatusPalErrorCode.InvalidToken); - private static SecurityStatusPal SecurityStatusPalInternalError = new SecurityStatusPal(SecurityStatusPalErrorCode.InternalError); - private static SecurityStatusPal SecurityStatusPalPackageNotFound = new SecurityStatusPal(SecurityStatusPalErrorCode.PackageNotFound); - private static SecurityStatusPal SecurityStatusPalMessageAltered = new SecurityStatusPal(SecurityStatusPalErrorCode.MessageAltered); - private static SecurityStatusPal SecurityStatusPalLogonDenied = new SecurityStatusPal(SecurityStatusPalErrorCode.LogonDenied); - - private const Flags s_requiredFlags = - Flags.NegotiateNtlm2 | Flags.NegotiateNtlm | Flags.NegotiateUnicode | Flags.TargetName | - Flags.NegotiateVersion | Flags.NegotiateKeyExchange | Flags.Negotiate128 | - Flags.NegotiateTargetInfo | Flags.NegotiateAlwaysSign | Flags.NegotiateSign; - - private static readonly Version s_version = new Version { VersionMajor = 6, VersionMinor = 1, ProductBuild = 7600, CurrentRevision = 15 }; - - private const int ChallengeResponseLength = 24; - - private const int HeaderLength = 8; - - private const int ChallengeLength = 8; - - private const int DigestLength = 16; - - private const int SessionKeyLength = 16; - private const int SignatureLength = 16; - - private const string SpnegoOid = "1.3.6.1.5.5.2"; - - private const string NtlmOid = "1.3.6.1.4.1.311.2.2.10"; - - private enum MessageType : byte - { - Negotiate = 1, - Challenge = 2, - Authenticate = 3, - } - - // 2.2.2.5 NEGOTIATE - [Flags] - private enum Flags : uint - { - NegotiateUnicode = 0x00000001, - NegotiateOEM = 0x00000002, - TargetName = 0x00000004, - NegotiateSign = 0x00000010, - NegotiateSeal = 0x00000020, - NegotiateDatagram = 0x00000040, - NegotiateLMKey = 0x00000080, - NegotiateNtlm = 0x00000200, - NegotiateAnonymous = 0x00000800, - NegotiateDomainSupplied = 0x00001000, - NegotiateWorkstationSupplied = 0x00002000, - NegotiateAlwaysSign = 0x00008000, - TargetTypeDomain = 0x00010000, - TargetTypeServer = 0x00020000, - NegotiateNtlm2 = 0x00080000, - RequestIdenityToken = 0x00100000, - RequestNonNtSessionKey = 0x00400000, - NegotiateTargetInfo = 0x00800000, - NegotiateVersion = 0x02000000, - Negotiate128 = 0x20000000, - NegotiateKeyExchange = 0x40000000, - Negotiate56 = 0x80000000, - } - - private enum AvId - { - EOL, - NbComputerName, - NbDomainName, - DnsComputerName, - DnsDomainName, - DnsTreeName, - Flags, - Timestamp, - SingleHost, - TargetName, - ChannelBindings, - } - - [StructLayout(LayoutKind.Sequential)] - private unsafe struct MessageField - { - public ushort Length; - public ushort MaximumLength; - public int PayloadOffset; - } - - [StructLayout(LayoutKind.Sequential)] - private unsafe struct MessageHeader - { - public fixed byte Header[HeaderLength]; - public MessageType MessageType; - private byte _unused1; - private byte _unused2; - private byte _unused3; - } - - [StructLayout(LayoutKind.Sequential)] - private unsafe struct Version - { - public byte VersionMajor; - public byte VersionMinor; - public ushort ProductBuild; - private byte _unused4; - private byte _unused5; - private byte _unused6; - public byte CurrentRevision; - } - - // Type 1 message - [StructLayout(LayoutKind.Sequential)] - private unsafe struct NegotiateMessage - { - public MessageHeader Header; - public Flags Flags; - public MessageField DomainName; - public MessageField WorkStation; - public Version Version; - } - - // TYPE 2 message - [StructLayout(LayoutKind.Sequential)] - private unsafe struct ChallengeMessage - { - public MessageHeader Header; - public MessageField TargetName; - public Flags Flags; - public fixed byte ServerChallenge[ChallengeLength]; - private ulong _unused; - public MessageField TargetInfo; - public Version Version; - } - - // TYPE 3 message - [StructLayout(LayoutKind.Sequential)] - private unsafe struct AuthenticateMessage - { - public MessageHeader Header; - public MessageField LmChallengeResponse; - public MessageField NtChallengeResponse; - public MessageField DomainName; - public MessageField UserName; - public MessageField Workstation; - public MessageField EncryptedRandomSessionKey; - public Flags Flags; - public Version Version; - public fixed byte Mic[16]; - } - - // Set temp to ConcatenationOf(Responserversion, HiResponserversion, Z(6), Time, ClientChallenge, Z(4), ServerName, Z(4)) - [StructLayout(LayoutKind.Sequential)] - private unsafe struct NtChallengeResponse - { - public fixed byte Hmac[DigestLength]; - public byte Responserversion; - public byte HiResponserversion; - private byte _reserved1; - private byte _reserved2; - private int _reserved3; - public long Time; - public fixed byte ClientChallenge[ChallengeLength]; - private int _reserved4; - public fixed byte ServerInfo[4]; // Has to be non-zero size, so set it to the Z(4) padding - } - - // rfc4178 - private enum NegotiationToken - { - NegTokenInit = 0, - NegTokenResp = 1 - } - - private enum NegTokenInit - { - MechTypes = 0, - ReqFlags = 1, - MechToken = 2, - MechListMIC = 3 - } - - private enum NegTokenResp - { - NegState = 0, - SupportedMech = 1, - ResponseToken = 2, - MechListMIC = 3 - } - - private enum NegState - { - Unknown = -1, // Internal. Not in RFC. - AcceptCompleted = 0, - AcceptIncomplete = 1, - Reject = 2, - RequestMic = 3 - } - - internal NTAuthentication(bool isServer, string package, NetworkCredential credential, string? spn, ContextFlagsPal requestedContextFlags, ChannelBinding? channelBinding) - { - if (isServer) - { - throw new PlatformNotSupportedException(SR.net_nego_server_not_supported); - } - - if (package.Equals("NTLM", StringComparison.OrdinalIgnoreCase)) - { - _isSpNego = false; - } - else if (package.Equals("Negotiate", StringComparison.OrdinalIgnoreCase)) - { - _isSpNego = true; - } - else - { - throw new PlatformNotSupportedException(SR.net_securitypackagesupport); - } - - if (string.IsNullOrWhiteSpace(credential.UserName) || string.IsNullOrWhiteSpace(credential.Password)) - { - // NTLM authentication is not possible with default credentials which are no-op - throw new PlatformNotSupportedException(SR.net_ntlm_not_possible_default_cred); - } - - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"package={package}, spn={spn}, requestedContextFlags={requestedContextFlags}"); - - _credential = credential; - _spn = spn; - _channelBinding = channelBinding; - _contextFlags = requestedContextFlags; - IsServer = isServer; - } - - internal void CloseContext() - { - // Dispose of the state - _negotiateMessage = null; - _clientSigningKey = null; - _serverSigningKey = null; - _clientSealingKey = null; - _serverSealingKey = null; - _clientSeal?.Dispose(); - _serverSeal?.Dispose(); - _clientSeal = null; - _serverSeal = null; - _clientSequenceNumber = 0; - _serverSequenceNumber = 0; - IsCompleted = false; - } - - internal string? GetOutgoingBlob(string? incomingBlob) - { - return GetOutgoingBlob(incomingBlob, throwOnError: true, out _); - } - - internal unsafe string? GetOutgoingBlob(string? incomingBlob, bool throwOnError, out SecurityStatusPal statusCode) - { - Debug.Assert(!IsCompleted); - - byte[]? decodedIncomingBlob = null; - if (incomingBlob != null && incomingBlob.Length > 0) - { - decodedIncomingBlob = Convert.FromBase64String(incomingBlob); - } - byte[]? decodedOutgoingBlob = GetOutgoingBlob(decodedIncomingBlob, throwOnError, out statusCode); - string? outgoingBlob = null; - if (decodedOutgoingBlob != null && decodedOutgoingBlob.Length > 0) - { - outgoingBlob = Convert.ToBase64String(decodedOutgoingBlob); - } - - if (IsCompleted) - { - CloseContext(); - } - - return outgoingBlob; - } - - internal byte[]? GetOutgoingBlob(byte[]? incomingBlob, bool throwOnError) - { - return GetOutgoingBlob(incomingBlob.AsSpan(), throwOnError, out _); - } - - // Accepts an incoming binary security blob and returns an outgoing binary security blob. - internal byte[]? GetOutgoingBlob(byte[]? incomingBlob, bool throwOnError, out SecurityStatusPal statusCode) - { - return GetOutgoingBlob(incomingBlob.AsSpan(), throwOnError, out statusCode); - } - - internal unsafe byte[]? GetOutgoingBlob(ReadOnlySpan incomingBlob, bool throwOnError, out SecurityStatusPal statusCode) - { - byte[]? outgoingBlob; - - // TODO: Logging, validation - if (_negotiateMessage == null) - { - Debug.Assert(incomingBlob.IsEmpty); - - _negotiateMessage = new byte[sizeof(NegotiateMessage)]; - CreateNtlmNegotiateMessage(_negotiateMessage); - - outgoingBlob = _isSpNego ? CreateSpNegoNegotiateMessage(_negotiateMessage) : _negotiateMessage; - statusCode = SecurityStatusPalContinueNeeded; - } - else - { - Debug.Assert(!incomingBlob.IsEmpty); - - if (!_isSpNego) - { - IsCompleted = true; - outgoingBlob = ProcessChallenge(incomingBlob, out statusCode); - } - else - { - outgoingBlob = ProcessSpNegoChallenge(incomingBlob, out statusCode); - } - } - - if (statusCode.ErrorCode >= SecurityStatusPalErrorCode.OutOfMemory && throwOnError) - { - throw new Win32Exception(NTE_FAIL, statusCode.ErrorCode.ToString()); - } - - return outgoingBlob; - } - - private static unsafe void CreateNtlmNegotiateMessage(Span asBytes) - { - Debug.Assert(HeaderLength == NtlmHeader.Length); - Debug.Assert(asBytes.Length == sizeof(NegotiateMessage)); - - ref NegotiateMessage message = ref MemoryMarshal.AsRef(asBytes); - - asBytes.Clear(); - NtlmHeader.CopyTo(asBytes); - message.Header.MessageType = MessageType.Negotiate; - message.Flags = s_requiredFlags; - message.Version = s_version; - } - - private static unsafe int GetFieldLength(MessageField field) - { - ReadOnlySpan span = new ReadOnlySpan(&field, sizeof(MessageField)); - return BinaryPrimitives.ReadInt16LittleEndian(span); - } - - private static unsafe int GetFieldOffset(MessageField field) - { - ReadOnlySpan span = new ReadOnlySpan(&field, sizeof(MessageField)); - return BinaryPrimitives.ReadInt16LittleEndian(span.Slice(4)); - } - - private static ReadOnlySpan GetField(MessageField field, ReadOnlySpan payload) - { - int offset = GetFieldOffset(field); - int length = GetFieldLength(field); - - if (length == 0 || offset + length > payload.Length) - { - return ReadOnlySpan.Empty; - } - - return payload.Slice(GetFieldOffset(field), GetFieldLength(field)); - } - - private static unsafe void SetField(ref MessageField field, int length, int offset) - { - if (length > short.MaxValue) - { - throw new Win32Exception(NTE_FAIL); - } - - Span span = MemoryMarshal.AsBytes(new Span(ref field)); - BinaryPrimitives.WriteInt16LittleEndian(span, (short)length); - BinaryPrimitives.WriteInt16LittleEndian(span.Slice(2), (short)length); - BinaryPrimitives.WriteInt32LittleEndian(span.Slice(4), offset); - } - - private static void AddToPayload(ref MessageField field, ReadOnlySpan data, Span payload, ref int offset) - { - SetField(ref field, data.Length, offset); - data.CopyTo(payload.Slice(offset)); - offset += data.Length; - } - - private static void AddToPayload(ref MessageField field, ReadOnlySpan data, Span payload, ref int offset) - { - int dataLength = Encoding.Unicode.GetBytes(data, payload.Slice(offset)); - SetField(ref field, dataLength, offset); - offset += dataLength; - } - - // Section 3.3.2 - // Define NTOWFv2(Passwd, User, UserDom) as HMAC_MD5(MD4(UNICODE(Passwd)), UNICODE(ConcatenationOf(Uppercase(User), - // UserDom ) ) ) - // EndDefine - private static void makeNtlm2Hash(string domain, string userName, ReadOnlySpan password, Span hash) - { - // Maximum password length for Windows authentication is 128 characters, we enforce - // the limit early to prevent allocating large buffers on stack. - if (password.Length > 128) - { - throw new Win32Exception(NTE_FAIL); - } - - Span pwHash = stackalloc byte[DigestLength]; - Span pwBytes = stackalloc byte[Encoding.Unicode.GetByteCount(password)]; - - try - { - Encoding.Unicode.GetBytes(password, pwBytes); - MD4.HashData(pwBytes, pwHash); - // strangely, user is upper case, domain is not. - byte[] blob = Encoding.Unicode.GetBytes(string.Concat(userName.ToUpperInvariant(), domain)); - int written = HMACMD5.HashData(pwHash, blob, hash); - Debug.Assert(written == HMACMD5.HashSizeInBytes); - } - finally - { - CryptographicOperations.ZeroMemory(pwBytes); - CryptographicOperations.ZeroMemory(pwHash); - } - } - - // Section 3.3.2 - // - // Set temp to ConcatenationOf(Responserversion, HiResponserversion, Z(6), Time, ClientChallenge, Z(4), ServerName, Z(4)) - // Set NTProofStr to HMAC_MD5(ResponseKeyNT, ConcatenationOf(CHALLENGE_MESSAGE.ServerChallenge, temp)) - // Set NtChallengeResponse to ConcatenationOf(NTProofStr, temp) - private unsafe void makeNtlm2ChallengeResponse(DateTime time, ReadOnlySpan ntlm2hash, ReadOnlySpan serverChallenge, Span clientChallenge, ReadOnlySpan serverInfo, ref MessageField field, Span payload, ref int payloadOffset) - { - Debug.Assert(serverChallenge.Length == ChallengeLength); - Debug.Assert(clientChallenge.Length == ChallengeLength); - Debug.Assert(ntlm2hash.Length == DigestLength); - - Span blob = payload.Slice(payloadOffset, sizeof(NtChallengeResponse) + serverInfo.Length); - ref NtChallengeResponse temp = ref MemoryMarshal.AsRef(blob.Slice(0, sizeof(NtChallengeResponse))); - - temp.HiResponserversion = 1; - temp.Responserversion = 1; - temp.Time = time.ToFileTimeUtc(); - - clientChallenge.CopyTo(MemoryMarshal.CreateSpan(ref temp.ClientChallenge[0], ChallengeLength)); - serverInfo.CopyTo(MemoryMarshal.CreateSpan(ref temp.ServerInfo[0], serverInfo.Length)); - - // Calculate NTProofStr - // we created temp part in place where it needs to be. - // now we need to prepend it with calculated hmac. - using (var hmac = IncrementalHash.CreateHMAC(HashAlgorithmName.MD5, ntlm2hash)) - { - hmac.AppendData(serverChallenge); - hmac.AppendData(blob.Slice(DigestLength)); - hmac.GetHashAndReset(blob); - } - - SetField(ref field, blob.Length, payloadOffset); - - payloadOffset += blob.Length; - } - - private unsafe void WriteChannelBindingHash(Span hashBuffer) - { - if (_channelBinding != null) - { - IntPtr cbtData = _channelBinding.DangerousGetHandle(); - int cbtDataSize = _channelBinding.Size; - int written = MD5.HashData(new Span((void*)cbtData, cbtDataSize), hashBuffer); - Debug.Assert(written == MD5.HashSizeInBytes); - } - else - { - hashBuffer.Clear(); - } - } - - private byte[] ProcessTargetInfo(ReadOnlySpan targetInfo, out DateTime time, out bool hasNbNames) - { - int spnSize = _spn != null ? Encoding.Unicode.GetByteCount(_spn) : 0; - - if (spnSize > short.MaxValue) - { - throw new Win32Exception(NTE_FAIL); - } - - bool hasNbComputerName = false, hasNbDomainName = false; - byte[] targetInfoBuffer = new byte[targetInfo.Length + 20 /* channel binding */ + 4 + spnSize /* SPN */ + 8 /* flags */]; - int targetInfoOffset = 0; - - time = DateTime.UtcNow; - - if (targetInfo.Length > 0) - { - ReadOnlySpan info = targetInfo; - while (info.Length >= 4) - { - AvId ID = (AvId)info[0]; - byte l1 = info[2]; - byte l2 = info[3]; - int length = (l2 << 8) + l1; - - if (ID == AvId.EOL) - { - break; - } - - if (ID == AvId.Timestamp) - { - time = DateTime.FromFileTimeUtc(BitConverter.ToInt64(info.Slice(4, 8))); - } - else if (ID == AvId.TargetName || ID == AvId.ChannelBindings) - { - // Skip these, we insert them ourselves - info = info.Slice(length + 4); - continue; - } - else if (ID == AvId.NbComputerName) - { - hasNbComputerName = true; - } - else if (ID == AvId.NbDomainName) - { - hasNbDomainName = true; - } - - // Copy attribute-value pair into destination target info - info.Slice(0, length + 4).CopyTo(targetInfoBuffer.AsSpan(targetInfoOffset, length + 4)); - targetInfoOffset += length + 4; - - info = info.Slice(length + 4); - } - } - - hasNbNames = hasNbComputerName && hasNbDomainName; - - // Add new entries into destination target info - - // Target name (eg. HTTP/example.org) - targetInfoBuffer[targetInfoOffset] = (byte)AvId.TargetName; - BinaryPrimitives.WriteUInt16LittleEndian(targetInfoBuffer.AsSpan(2 + targetInfoOffset), (ushort)spnSize); - if (_spn != null) - { - int bytesWritten = Encoding.Unicode.GetBytes(_spn, targetInfoBuffer.AsSpan(4 + targetInfoOffset)); - Debug.Assert(bytesWritten == spnSize); - } - targetInfoOffset += spnSize + 4; - - // Channel binding - targetInfoBuffer[targetInfoOffset] = (byte)AvId.ChannelBindings; - targetInfoBuffer[targetInfoOffset + 2] = 16; - WriteChannelBindingHash(targetInfoBuffer.AsSpan(targetInfoOffset + 4, 16)); - targetInfoOffset += 16 + 4; - - // Flags - targetInfoBuffer[targetInfoOffset] = (byte)AvId.Flags; - targetInfoBuffer[targetInfoOffset + 2] = 4; // Length of flags - targetInfoBuffer[targetInfoOffset + 4] = 2; // MIC flag - targetInfoOffset += 8; - - // EOL - targetInfoOffset += 4; - - if (targetInfoOffset == targetInfoBuffer.Length) - { - return targetInfoBuffer; - } - - return targetInfoBuffer.AsSpan(targetInfoOffset).ToArray(); - } - - // Section 3.4.5.2 SIGNKEY, 3.4.5.3 SEALKEY - private static byte[] DeriveKey(ReadOnlySpan exportedSessionKey, ReadOnlySpan magic) - { - using (var md5 = IncrementalHash.CreateHash(HashAlgorithmName.MD5)) - { - md5.AppendData(exportedSessionKey); - md5.AppendData(magic); - return md5.GetHashAndReset(); - } - } - - // This gets decoded byte blob and returns response in binary form. - private unsafe byte[]? ProcessChallenge(ReadOnlySpan blob, out SecurityStatusPal statusCode) - { - // TODO: Validate size and offsets - - ref readonly ChallengeMessage challengeMessage = ref MemoryMarshal.AsRef(blob.Slice(0, sizeof(ChallengeMessage))); - - // Verify message type and signature - if (challengeMessage.Header.MessageType != MessageType.Challenge || - !NtlmHeader.SequenceEqual(blob.Slice(0, NtlmHeader.Length))) - { - statusCode = SecurityStatusPalInvalidToken; - return null; - } - - Flags flags = BitConverter.IsLittleEndian ? challengeMessage.Flags : (Flags)BinaryPrimitives.ReverseEndianness((uint)challengeMessage.Flags); - ReadOnlySpan targetName = GetField(challengeMessage.TargetName, blob); - - // Only NTLMv2 with MIC is supported - // - // NegotiateSign and NegotiateKeyExchange are necessary to calculate the key - // that is used for MIC. - if ((flags & s_requiredFlags) != s_requiredFlags) - { - statusCode = SecurityStatusPalInvalidToken; - return null; - } - - ReadOnlySpan targetInfo = GetField(challengeMessage.TargetInfo, blob); - byte[] targetInfoBuffer = ProcessTargetInfo(targetInfo, out DateTime time, out bool hasNbNames); - - // If NTLM v2 authentication is used and the CHALLENGE_MESSAGE does not contain both - // MsvAvNbComputerName and MsvAvNbDomainName AVPairs and either Integrity is TRUE or - // Confidentiality is TRUE, then return STATUS_LOGON_FAILURE ([MS-ERREF] section 2.3.1). - if (!hasNbNames && (flags & (Flags.NegotiateSign | Flags.NegotiateSeal)) != 0) - { - statusCode = SecurityStatusPalInvalidToken; - return null; - } - - int responseLength = - sizeof(AuthenticateMessage) + - ChallengeResponseLength + - sizeof(NtChallengeResponse) + - targetInfoBuffer.Length + - Encoding.Unicode.GetByteCount(_credential.UserName) + - Encoding.Unicode.GetByteCount(_credential.Domain) + - s_workstation.Length + - SessionKeyLength; - - byte[] responseBytes = new byte[responseLength]; - Span responseAsSpan = new Span(responseBytes); - ref AuthenticateMessage response = ref MemoryMarshal.AsRef(responseAsSpan.Slice(0, sizeof(AuthenticateMessage))); - - // variable fields - Span payload = responseAsSpan; - int payloadOffset = sizeof(AuthenticateMessage); - - responseAsSpan.Clear(); - NtlmHeader.CopyTo(responseAsSpan); - - response.Header.MessageType = MessageType.Authenticate; - response.Flags = s_requiredFlags; - response.Version = s_version; - - // Calculate hash for hmac - same for lm2 and ntlm2 - Span ntlm2hash = stackalloc byte[DigestLength]; - makeNtlm2Hash(_credential.Domain, _credential.UserName, _credential.Password, ntlm2hash); - - // Get random bytes for client challenge - Span clientChallenge = stackalloc byte[ChallengeLength]; - RandomNumberGenerator.Fill(clientChallenge); - - // Create empty LM2 response. - SetField(ref response.LmChallengeResponse, ChallengeResponseLength, payloadOffset); - payload.Slice(payloadOffset, ChallengeResponseLength).Clear(); - payloadOffset += ChallengeResponseLength; - - // Create NTLM2 response - ReadOnlySpan serverChallenge = blob.Slice(24, 8); - makeNtlm2ChallengeResponse(time, ntlm2hash, serverChallenge, clientChallenge, targetInfoBuffer, ref response.NtChallengeResponse, payload, ref payloadOffset); - Debug.Assert(payloadOffset == sizeof(AuthenticateMessage) + ChallengeResponseLength + sizeof(NtChallengeResponse) + targetInfoBuffer.Length); - - AddToPayload(ref response.UserName, _credential.UserName, payload, ref payloadOffset); - AddToPayload(ref response.DomainName, _credential.Domain, payload, ref payloadOffset); - AddToPayload(ref response.Workstation, s_workstation, payload, ref payloadOffset); - - // Generate random session key that will be used for signing the messages - Span exportedSessionKey = stackalloc byte[16]; - RandomNumberGenerator.Fill(exportedSessionKey); - - // Both flags are necessary to exchange keys needed for MIC (!) - Debug.Assert(flags.HasFlag(Flags.NegotiateSign) && flags.HasFlag(Flags.NegotiateKeyExchange)); - - // Derive session base key - Span sessionBaseKey = stackalloc byte[HMACMD5.HashSizeInBytes]; - int sessionKeyWritten = HMACMD5.HashData(ntlm2hash, responseAsSpan.Slice(response.NtChallengeResponse.PayloadOffset, 16), sessionBaseKey); - Debug.Assert(sessionKeyWritten == HMACMD5.HashSizeInBytes); - - // Encrypt exportedSessionKey with sessionBaseKey - using (RC4 rc4 = new RC4(sessionBaseKey)) - { - Span encryptedRandomSessionKey = payload.Slice(payloadOffset, 16); - rc4.Transform(exportedSessionKey, encryptedRandomSessionKey); - SetField(ref response.EncryptedRandomSessionKey, 16, payloadOffset); - payloadOffset += 16; - } - - // Calculate MIC - Debug.Assert(_negotiateMessage != null); - using (var hmacMic = IncrementalHash.CreateHMAC(HashAlgorithmName.MD5, exportedSessionKey)) - { - hmacMic.AppendData(_negotiateMessage); - hmacMic.AppendData(blob); - hmacMic.AppendData(responseBytes.AsSpan(0, payloadOffset)); - hmacMic.GetHashAndReset(MemoryMarshal.CreateSpan(ref response.Mic[0], hmacMic.HashLengthInBytes)); - } - - // Derive signing keys - _clientSigningKey = DeriveKey(exportedSessionKey, ClientSigningKeyMagic); - _serverSigningKey = DeriveKey(exportedSessionKey, ServerSigningKeyMagic); - _clientSealingKey = DeriveKey(exportedSessionKey, ClientSealingKeyMagic); - _serverSealingKey = DeriveKey(exportedSessionKey, ServerSealingKeyMagic); - ResetKeys(); - _clientSequenceNumber = 0; - _serverSequenceNumber = 0; - CryptographicOperations.ZeroMemory(exportedSessionKey); - - Debug.Assert(payloadOffset == responseBytes.Length); - - statusCode = SecurityStatusPalOk; - return responseBytes; - } - - private void ResetKeys() - { - // Release buffers to pool - _clientSeal?.Dispose(); - _serverSeal?.Dispose(); - - _clientSeal = new RC4(_clientSealingKey); - _serverSeal = new RC4(_serverSealingKey); - } - - private void CalculateSignature( - ReadOnlySpan message, - uint sequenceNumber, - ReadOnlySpan signingKey, - RC4 seal, - Span signature) - { - BinaryPrimitives.WriteInt32LittleEndian(signature, 1); - BinaryPrimitives.WriteUInt32LittleEndian(signature.Slice(12), sequenceNumber); - using (var hmac = IncrementalHash.CreateHMAC(HashAlgorithmName.MD5, signingKey)) - { - hmac.AppendData(signature.Slice(12, 4)); - hmac.AppendData(message); - Span hmacResult = stackalloc byte[hmac.HashLengthInBytes]; - hmac.GetHashAndReset(hmacResult); - seal.Transform(hmacResult.Slice(0, 8), signature.Slice(4, 8)); - } - } - - internal bool VerifyMIC(ReadOnlySpan message, ReadOnlySpan signature) - { - // Check length and version - if (signature.Length != SignatureLength || - BinaryPrimitives.ReadInt32LittleEndian(signature) != 1 || - _serverSeal == null || - _serverSigningKey == null) - { - return false; - } - - Span expectedSignature = stackalloc byte[SignatureLength]; - CalculateSignature(message, _serverSequenceNumber, _serverSigningKey, _serverSeal, expectedSignature); - - _serverSequenceNumber++; - - return signature.SequenceEqual(expectedSignature); - } - - internal void GetMIC(ReadOnlySpan message, IBufferWriter signature) - { - Debug.Assert(_clientSeal is not null); - Debug.Assert(_clientSigningKey is not null); - - Span signatureBuffer = signature.GetSpan(SignatureLength); - CalculateSignature(message, _clientSequenceNumber, _clientSigningKey, _clientSeal, signatureBuffer); - _clientSequenceNumber++; - signature.Advance(SignatureLength); - } - - private byte[] GetMIC(ReadOnlySpan message) - { - Debug.Assert(_clientSeal is not null); - Debug.Assert(_clientSigningKey is not null); - - byte[] signature = new byte[SignatureLength]; - CalculateSignature(message, _clientSequenceNumber, _clientSigningKey, _clientSeal, signature); - _clientSequenceNumber++; - return signature; - } - - private unsafe byte[] CreateSpNegoNegotiateMessage(ReadOnlySpan ntlmNegotiateMessage) - { - AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); - using (writer.PushSequence(new Asn1Tag(TagClass.Application, 0))) - { - writer.WriteObjectIdentifier(SpnegoOid); - - // NegTokenInit::= SEQUENCE { - // mechTypes[0] MechTypeList, - // reqFlags[1] ContextFlags OPTIONAL, - // --inherited from RFC 2478 for backward compatibility, - // --RECOMMENDED to be left out - // mechToken[2] OCTET STRING OPTIONAL, - // mechListMIC[3] OCTET STRING OPTIONAL, - // ... - // } - using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegotiationToken.NegTokenInit))) - { - using (writer.PushSequence()) - { - // MechType::= OBJECT IDENTIFIER - // -- OID represents each security mechanism as suggested by - // --[RFC2743] - // - // MechTypeList::= SEQUENCE OF MechType - using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenInit.MechTypes))) - { - AsnWriter mechListWriter = new AsnWriter(AsnEncodingRules.DER); - - using (mechListWriter.PushSequence()) - { - mechListWriter.WriteObjectIdentifier(NtlmOid); - } - - _spnegoMechList = mechListWriter.Encode(); - mechListWriter.CopyTo(writer); - } - - using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenInit.MechToken))) - { - writer.WriteOctetString(ntlmNegotiateMessage); - } - } - } - } - - return writer.Encode(); - } - - private unsafe byte[]? ProcessSpNegoChallenge(ReadOnlySpan challenge, out SecurityStatusPal statusCode) - { - NegState state = NegState.Unknown; - string? mech = null; - byte[]? blob = null; - byte[]? mechListMIC = null; - - try - { - AsnValueReader reader = new AsnValueReader(challenge, AsnEncodingRules.DER); - AsnValueReader challengeReader = reader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegotiationToken.NegTokenResp)); - reader.ThrowIfNotEmpty(); - - // NegTokenResp ::= SEQUENCE { - // negState[0] ENUMERATED { - // accept - completed(0), - // accept - incomplete(1), - // reject(2), - // request - mic(3) - // } OPTIONAL, - // --REQUIRED in the first reply from the target - // supportedMech[1] MechType OPTIONAL, - // --present only in the first reply from the target - // responseToken[2] OCTET STRING OPTIONAL, - // mechListMIC[3] OCTET STRING OPTIONAL, - // ... - // } - - challengeReader = challengeReader.ReadSequence(); - - if (challengeReader.HasData && challengeReader.PeekTag().HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.NegState))) - { - AsnValueReader valueReader = challengeReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.NegState)); - state = valueReader.ReadEnumeratedValue(); - valueReader.ThrowIfNotEmpty(); - } - - if (challengeReader.HasData && challengeReader.PeekTag().HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.SupportedMech))) - { - AsnValueReader valueReader = challengeReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.SupportedMech)); - mech = valueReader.ReadObjectIdentifier(); - valueReader.ThrowIfNotEmpty(); - } - - if (challengeReader.HasData && challengeReader.PeekTag().HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.ResponseToken))) - { - AsnValueReader valueReader = challengeReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.ResponseToken)); - blob = valueReader.ReadOctetString(); - valueReader.ThrowIfNotEmpty(); - } - - if (challengeReader.HasData && challengeReader.PeekTag().HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.MechListMIC))) - { - AsnValueReader valueReader = challengeReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.MechListMIC)); - mechListMIC = valueReader.ReadOctetString(); - valueReader.ThrowIfNotEmpty(); - } - - challengeReader.ThrowIfNotEmpty(); - } - catch (AsnContentException) - { - statusCode = SecurityStatusPalInvalidToken; - return null; - } - - if (blob?.Length > 0) - { - // Mechanism should be set on first message. In case of NTLM that's the only - // message with the challenge blob. - if (!NtlmOid.Equals(mech)) - { - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Server requested unknown mechanism {mech}"); - statusCode = SecurityStatusPalPackageNotFound; - return null; - } - - // Process decoded NTLM blob. - byte[]? response = ProcessChallenge(blob, out statusCode); - - if (statusCode.ErrorCode != SecurityStatusPalErrorCode.OK) - { - return null; - } - - if (response?.Length > 0) - { - AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); - - using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegotiationToken.NegTokenResp))) - { - using (writer.PushSequence()) - { - using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.ResponseToken))) - { - writer.WriteOctetString(response); - } - - using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.MechListMIC))) - { - writer.WriteOctetString(GetMIC(_spnegoMechList)); - } - } - } - - statusCode = state == NegState.RequestMic ? SecurityStatusPalContinueNeeded : SecurityStatusPalOk; - return writer.Encode(); - } - } - - if (mechListMIC != null) - { - if (_spnegoMechList == null || state != NegState.AcceptCompleted) - { - statusCode = SecurityStatusPalInternalError; - return null; - } - - if (!VerifyMIC(_spnegoMechList, mechListMIC)) - { - statusCode = SecurityStatusPalMessageAltered; - return null; - } - - ResetKeys(); - } - - IsCompleted = state == NegState.AcceptCompleted || state == NegState.Reject; - statusCode = state switch { - NegState.AcceptCompleted => SecurityStatusPalOk, - NegState.AcceptIncomplete => SecurityStatusPalContinueNeeded, - NegState.Reject => SecurityStatusPalLogonDenied, - _ => SecurityStatusPalInternalError - }; - - return null; - } - - internal NegotiateAuthenticationStatusCode Wrap(ReadOnlySpan input, IBufferWriter outputWriter, bool _/*requestEncryption*/, out bool isEncrypted) - { - if (_clientSeal == null) - { - throw new InvalidOperationException(SR.net_auth_noauth); - } - - Span output = outputWriter.GetSpan(input.Length + SignatureLength); - _clientSeal.Transform(input, output.Slice(SignatureLength, input.Length)); - CalculateSignature(input, _clientSequenceNumber, _clientSigningKey, _clientSeal, output.Slice(0, SignatureLength)); - _clientSequenceNumber++; - - isEncrypted = true; - outputWriter.Advance(input.Length + SignatureLength); - - return NegotiateAuthenticationStatusCode.Completed; - } - - internal NegotiateAuthenticationStatusCode Unwrap(ReadOnlySpan input, IBufferWriter outputWriter, out bool wasEncrypted) - { - wasEncrypted = true; - - if (_serverSeal == null) - { - throw new InvalidOperationException(SR.net_auth_noauth); - } - - if (input.Length < SignatureLength) - { - return NegotiateAuthenticationStatusCode.InvalidToken; - } - - Span output = outputWriter.GetSpan(input.Length - SignatureLength); - _serverSeal.Transform(input.Slice(SignatureLength), output.Slice(0, input.Length - SignatureLength)); - if (!VerifyMIC(output.Slice(0, input.Length - SignatureLength), input.Slice(0, SignatureLength))) - { - CryptographicOperations.ZeroMemory(output); - return NegotiateAuthenticationStatusCode.MessageAltered; - } - - outputWriter.Advance(input.Length - SignatureLength); - - return NegotiateAuthenticationStatusCode.Completed; - } - - internal NegotiateAuthenticationStatusCode UnwrapInPlace(Span input, out int unwrappedOffset, out int unwrappedLength, out bool wasEncrypted) - { - wasEncrypted = true; - unwrappedOffset = SignatureLength; - unwrappedLength = input.Length - SignatureLength; - - if (_serverSeal == null) - { - throw new InvalidOperationException(SR.net_auth_noauth); - } - - if (input.Length < SignatureLength) - { - return NegotiateAuthenticationStatusCode.InvalidToken; - } - - _serverSeal.Transform(input.Slice(SignatureLength), input.Slice(SignatureLength)); - if (!VerifyMIC(input.Slice(SignatureLength), input.Slice(0, SignatureLength))) - { - CryptographicOperations.ZeroMemory(input.Slice(SignatureLength)); - return NegotiateAuthenticationStatusCode.MessageAltered; - } - - return NegotiateAuthenticationStatusCode.Completed; - } - - internal string ProtocolName => _isSpNego ? NegotiationInfoClass.Negotiate : NegotiationInfoClass.NTLM; - -#pragma warning disable CA1822, IDE0060 - internal bool IsNTLM => true; - - internal bool IsKerberos => false; - - internal bool IsServer { get; set; } - - internal bool IsValidContext => true; -#pragma warning restore CA1822, IDE0060 - - internal string? ClientSpecifiedSpn => _spn; - } -} diff --git a/src/libraries/System.Net.Security/src/System/Net/NTAuthentication.cs b/src/libraries/System.Net.Security/src/System/Net/NTAuthentication.cs deleted file mode 100644 index f67274fe139bd783ecdca3e817201b7f5939c5f4..0000000000000000000000000000000000000000 --- a/src/libraries/System.Net.Security/src/System/Net/NTAuthentication.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using System.Net.Security; -using System.Runtime.Versioning; -using System.Security.Authentication.ExtendedProtection; - -namespace System.Net -{ - internal sealed partial class NTAuthentication - { - internal bool IsConfidentialityFlag => (_contextFlags & ContextFlagsPal.Confidentiality) != 0; - - internal bool IsIntegrityFlag => (_contextFlags & (IsServer ? ContextFlagsPal.AcceptIntegrity : ContextFlagsPal.InitIntegrity)) != 0; - - internal bool IsMutualAuthFlag => (_contextFlags & ContextFlagsPal.MutualAuth) != 0; - - internal bool IsDelegationFlag => (_contextFlags & ContextFlagsPal.Delegate) != 0; - - internal bool IsIdentifyFlag => (_contextFlags & (IsServer ? ContextFlagsPal.AcceptIdentify : ContextFlagsPal.InitIdentify)) != 0; - - internal string? Spn => _spn; - } -} diff --git a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Managed.cs b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Managed.cs new file mode 100644 index 0000000000000000000000000000000000000000..4e5e8906b795ccfa1854484bdd67f2f999f22a8d --- /dev/null +++ b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Managed.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Security; + +namespace System.Net +{ + internal abstract partial class NegotiateAuthenticationPal + { + public static NegotiateAuthenticationPal Create(NegotiateAuthenticationClientOptions clientOptions) + { + switch (clientOptions.Package) + { + case NegotiationInfoClass.NTLM: + return new ManagedNtlmNegotiateAuthenticationPal(clientOptions); + + case NegotiationInfoClass.Negotiate: + return new ManagedSpnegoNegotiateAuthenticationPal(clientOptions); + + default: + return new UnsupportedNegotiateAuthenticationPal(clientOptions); + } + } + + public static NegotiateAuthenticationPal Create(NegotiateAuthenticationServerOptions serverOptions) + { + return new UnsupportedNegotiateAuthenticationPal(serverOptions); + } + } +} diff --git a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedNtlm.cs b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedNtlm.cs new file mode 100644 index 0000000000000000000000000000000000000000..866a754af7292247953c3a4cfae70821f546818b --- /dev/null +++ b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedNtlm.cs @@ -0,0 +1,800 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Net.Security; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Security.Authentication.ExtendedProtection; +using System.Security.Cryptography; +using System.Security.Principal; +using System.Text; + +// NTLM uses all sorts of broken cryptographic primitives (HMAC-MD5, MD4, RC4) +#pragma warning disable CA5351 + +namespace System.Net +{ + internal abstract partial class NegotiateAuthenticationPal + { + internal sealed class ManagedNtlmNegotiateAuthenticationPal : NegotiateAuthenticationPal + { + // Input parameters + private readonly NetworkCredential _credential; + private readonly string? _spn; + private readonly ChannelBinding? _channelBinding; + private readonly ProtectionLevel _protectionLevel; + + // State parameters + private byte[]? _negotiateMessage; + private byte[]? _clientSigningKey; + private byte[]? _serverSigningKey; + private byte[]? _clientSealingKey; + private byte[]? _serverSealingKey; + private RC4? _clientSeal; + private RC4? _serverSeal; + private uint _clientSequenceNumber; + private uint _serverSequenceNumber; + private bool _isAuthenticated; + + // value should match the Windows sspicli NTE_FAIL value + // defined in winerror.h + private const int NTE_FAIL = unchecked((int)0x80090020); + + private static ReadOnlySpan NtlmHeader => "NTLMSSP\0"u8; + + private static ReadOnlySpan ClientSigningKeyMagic => "session key to client-to-server signing key magic constant\0"u8; + private static ReadOnlySpan ServerSigningKeyMagic => "session key to server-to-client signing key magic constant\0"u8; + private static ReadOnlySpan ClientSealingKeyMagic => "session key to client-to-server sealing key magic constant\0"u8; + private static ReadOnlySpan ServerSealingKeyMagic => "session key to server-to-client sealing key magic constant\0"u8; + + private static readonly byte[] s_workstation = Encoding.Unicode.GetBytes(Environment.MachineName); + + private const Flags s_requiredFlags = + Flags.NegotiateNtlm2 | Flags.NegotiateNtlm | Flags.NegotiateUnicode | Flags.TargetName | + Flags.NegotiateVersion | Flags.NegotiateKeyExchange | Flags.Negotiate128 | + Flags.NegotiateTargetInfo | Flags.NegotiateAlwaysSign | Flags.NegotiateSign; + + private static readonly Version s_version = new Version { VersionMajor = 6, VersionMinor = 1, ProductBuild = 7600, CurrentRevision = 15 }; + + private const int ChallengeResponseLength = 24; + + private const int HeaderLength = 8; + + private const int ChallengeLength = 8; + + private const int DigestLength = 16; + + private const int SessionKeyLength = 16; + private const int SignatureLength = 16; + + private enum MessageType : byte + { + Negotiate = 1, + Challenge = 2, + Authenticate = 3, + } + + // 2.2.2.5 NEGOTIATE + [Flags] + private enum Flags : uint + { + NegotiateUnicode = 0x00000001, + NegotiateOEM = 0x00000002, + TargetName = 0x00000004, + NegotiateSign = 0x00000010, + NegotiateSeal = 0x00000020, + NegotiateDatagram = 0x00000040, + NegotiateLMKey = 0x00000080, + NegotiateNtlm = 0x00000200, + NegotiateAnonymous = 0x00000800, + NegotiateDomainSupplied = 0x00001000, + NegotiateWorkstationSupplied = 0x00002000, + NegotiateAlwaysSign = 0x00008000, + TargetTypeDomain = 0x00010000, + TargetTypeServer = 0x00020000, + NegotiateNtlm2 = 0x00080000, + RequestIdenityToken = 0x00100000, + RequestNonNtSessionKey = 0x00400000, + NegotiateTargetInfo = 0x00800000, + NegotiateVersion = 0x02000000, + Negotiate128 = 0x20000000, + NegotiateKeyExchange = 0x40000000, + Negotiate56 = 0x80000000, + } + + private enum AvId + { + EOL, + NbComputerName, + NbDomainName, + DnsComputerName, + DnsDomainName, + DnsTreeName, + Flags, + Timestamp, + SingleHost, + TargetName, + ChannelBindings, + } + + [StructLayout(LayoutKind.Sequential)] + private unsafe struct MessageField + { + public ushort Length; + public ushort MaximumLength; + public int PayloadOffset; + } + + [StructLayout(LayoutKind.Sequential)] + private unsafe struct MessageHeader + { + public fixed byte Header[HeaderLength]; + public MessageType MessageType; + private byte _unused1; + private byte _unused2; + private byte _unused3; + } + + [StructLayout(LayoutKind.Sequential)] + private unsafe struct Version + { + public byte VersionMajor; + public byte VersionMinor; + public ushort ProductBuild; + private byte _unused4; + private byte _unused5; + private byte _unused6; + public byte CurrentRevision; + } + + // Type 1 message + [StructLayout(LayoutKind.Sequential)] + private unsafe struct NegotiateMessage + { + public MessageHeader Header; + public Flags Flags; + public MessageField DomainName; + public MessageField WorkStation; + public Version Version; + } + + // TYPE 2 message + [StructLayout(LayoutKind.Sequential)] + private unsafe struct ChallengeMessage + { + public MessageHeader Header; + public MessageField TargetName; + public Flags Flags; + public fixed byte ServerChallenge[ChallengeLength]; + private ulong _unused; + public MessageField TargetInfo; + public Version Version; + } + + // TYPE 3 message + [StructLayout(LayoutKind.Sequential)] + private unsafe struct AuthenticateMessage + { + public MessageHeader Header; + public MessageField LmChallengeResponse; + public MessageField NtChallengeResponse; + public MessageField DomainName; + public MessageField UserName; + public MessageField Workstation; + public MessageField EncryptedRandomSessionKey; + public Flags Flags; + public Version Version; + public fixed byte Mic[16]; + } + + // Set temp to ConcatenationOf(Responserversion, HiResponserversion, Z(6), Time, ClientChallenge, Z(4), ServerName, Z(4)) + [StructLayout(LayoutKind.Sequential)] + private unsafe struct NtChallengeResponse + { + public fixed byte Hmac[DigestLength]; + public byte Responserversion; + public byte HiResponserversion; + private byte _reserved1; + private byte _reserved2; + private int _reserved3; + public long Time; + public fixed byte ClientChallenge[ChallengeLength]; + private int _reserved4; + public fixed byte ServerInfo[4]; // Has to be non-zero size, so set it to the Z(4) padding + } + + public override bool IsAuthenticated => _isAuthenticated; + public override bool IsSigned => _protectionLevel != ProtectionLevel.None; + public override bool IsEncrypted => _protectionLevel == ProtectionLevel.EncryptAndSign; + public override bool IsMutuallyAuthenticated => false; + public override string Package => NegotiationInfoClass.NTLM; + public override string? TargetName => _spn; + public override IIdentity RemoteIdentity => throw new InvalidOperationException(); + public override System.Security.Principal.TokenImpersonationLevel ImpersonationLevel => System.Security.Principal.TokenImpersonationLevel.Impersonation; + + public ManagedNtlmNegotiateAuthenticationPal(NegotiateAuthenticationClientOptions clientOptions) + { + Debug.Assert(clientOptions.Package == NegotiationInfoClass.NTLM); + + _credential = clientOptions.Credential; + if (string.IsNullOrWhiteSpace(_credential.UserName) || string.IsNullOrWhiteSpace(_credential.Password)) + { + // NTLM authentication is not possible with default credentials which are no-op + throw new PlatformNotSupportedException(SR.net_ntlm_not_possible_default_cred); + } + + _spn = clientOptions.TargetName; + _channelBinding = clientOptions.Binding; + _protectionLevel = clientOptions.RequiredProtectionLevel; + + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"package={clientOptions.Package}, spn={_spn}, requiredProtectionLevel={_protectionLevel}"); + } + + public override void Dispose() + { + // Dispose of the state + _negotiateMessage = null; + _clientSigningKey = null; + _serverSigningKey = null; + _clientSealingKey = null; + _serverSealingKey = null; + _clientSeal?.Dispose(); + _serverSeal?.Dispose(); + _clientSeal = null; + _serverSeal = null; + _clientSequenceNumber = 0; + _serverSequenceNumber = 0; + _isAuthenticated = false; + } + + public override unsafe byte[]? GetOutgoingBlob(ReadOnlySpan incomingBlob, out NegotiateAuthenticationStatusCode statusCode) + { + byte[]? outgoingBlob; + + // TODO: Logging, validation + if (_negotiateMessage == null) + { + Debug.Assert(incomingBlob.IsEmpty); + + _negotiateMessage = new byte[sizeof(NegotiateMessage)]; + CreateNtlmNegotiateMessage(_negotiateMessage); + + outgoingBlob = _negotiateMessage; + statusCode = NegotiateAuthenticationStatusCode.ContinueNeeded; + } + else + { + Debug.Assert(!incomingBlob.IsEmpty); + _isAuthenticated = true; + outgoingBlob = ProcessChallenge(incomingBlob, out statusCode); + } + + return outgoingBlob; + } + + private static unsafe void CreateNtlmNegotiateMessage(Span asBytes) + { + Debug.Assert(HeaderLength == NtlmHeader.Length); + Debug.Assert(asBytes.Length == sizeof(NegotiateMessage)); + + ref NegotiateMessage message = ref MemoryMarshal.AsRef(asBytes); + + asBytes.Clear(); + NtlmHeader.CopyTo(asBytes); + message.Header.MessageType = MessageType.Negotiate; + message.Flags = s_requiredFlags; + message.Version = s_version; + } + + private static unsafe int GetFieldLength(MessageField field) + { + ReadOnlySpan span = new ReadOnlySpan(&field, sizeof(MessageField)); + return BinaryPrimitives.ReadInt16LittleEndian(span); + } + + private static unsafe int GetFieldOffset(MessageField field) + { + ReadOnlySpan span = new ReadOnlySpan(&field, sizeof(MessageField)); + return BinaryPrimitives.ReadInt16LittleEndian(span.Slice(4)); + } + + private static ReadOnlySpan GetField(MessageField field, ReadOnlySpan payload) + { + int offset = GetFieldOffset(field); + int length = GetFieldLength(field); + + if (length == 0 || offset + length > payload.Length) + { + return ReadOnlySpan.Empty; + } + + return payload.Slice(GetFieldOffset(field), GetFieldLength(field)); + } + + private static unsafe void SetField(ref MessageField field, int length, int offset) + { + if (length > short.MaxValue) + { + throw new Win32Exception(NTE_FAIL); + } + + Span span = MemoryMarshal.AsBytes(new Span(ref field)); + BinaryPrimitives.WriteInt16LittleEndian(span, (short)length); + BinaryPrimitives.WriteInt16LittleEndian(span.Slice(2), (short)length); + BinaryPrimitives.WriteInt32LittleEndian(span.Slice(4), offset); + } + + private static void AddToPayload(ref MessageField field, ReadOnlySpan data, Span payload, ref int offset) + { + SetField(ref field, data.Length, offset); + data.CopyTo(payload.Slice(offset)); + offset += data.Length; + } + + private static void AddToPayload(ref MessageField field, ReadOnlySpan data, Span payload, ref int offset) + { + int dataLength = Encoding.Unicode.GetBytes(data, payload.Slice(offset)); + SetField(ref field, dataLength, offset); + offset += dataLength; + } + + // Section 3.3.2 + // Define NTOWFv2(Passwd, User, UserDom) as HMAC_MD5(MD4(UNICODE(Passwd)), UNICODE(ConcatenationOf(Uppercase(User), + // UserDom ) ) ) + // EndDefine + private static void makeNtlm2Hash(string domain, string userName, ReadOnlySpan password, Span hash) + { + // Maximum password length for Windows authentication is 128 characters, we enforce + // the limit early to prevent allocating large buffers on stack. + if (password.Length > 128) + { + throw new Win32Exception(NTE_FAIL); + } + + Span pwHash = stackalloc byte[DigestLength]; + Span pwBytes = stackalloc byte[Encoding.Unicode.GetByteCount(password)]; + + try + { + Encoding.Unicode.GetBytes(password, pwBytes); + MD4.HashData(pwBytes, pwHash); + // strangely, user is upper case, domain is not. + byte[] blob = Encoding.Unicode.GetBytes(string.Concat(userName.ToUpperInvariant(), domain)); + int written = HMACMD5.HashData(pwHash, blob, hash); + Debug.Assert(written == HMACMD5.HashSizeInBytes); + } + finally + { + CryptographicOperations.ZeroMemory(pwBytes); + CryptographicOperations.ZeroMemory(pwHash); + } + } + + // Section 3.3.2 + // + // Set temp to ConcatenationOf(Responserversion, HiResponserversion, Z(6), Time, ClientChallenge, Z(4), ServerName, Z(4)) + // Set NTProofStr to HMAC_MD5(ResponseKeyNT, ConcatenationOf(CHALLENGE_MESSAGE.ServerChallenge, temp)) + // Set NtChallengeResponse to ConcatenationOf(NTProofStr, temp) + private unsafe void makeNtlm2ChallengeResponse(DateTime time, ReadOnlySpan ntlm2hash, ReadOnlySpan serverChallenge, Span clientChallenge, ReadOnlySpan serverInfo, ref MessageField field, Span payload, ref int payloadOffset) + { + Debug.Assert(serverChallenge.Length == ChallengeLength); + Debug.Assert(clientChallenge.Length == ChallengeLength); + Debug.Assert(ntlm2hash.Length == DigestLength); + + Span blob = payload.Slice(payloadOffset, sizeof(NtChallengeResponse) + serverInfo.Length); + ref NtChallengeResponse temp = ref MemoryMarshal.AsRef(blob.Slice(0, sizeof(NtChallengeResponse))); + + temp.HiResponserversion = 1; + temp.Responserversion = 1; + temp.Time = time.ToFileTimeUtc(); + + clientChallenge.CopyTo(MemoryMarshal.CreateSpan(ref temp.ClientChallenge[0], ChallengeLength)); + serverInfo.CopyTo(MemoryMarshal.CreateSpan(ref temp.ServerInfo[0], serverInfo.Length)); + + // Calculate NTProofStr + // we created temp part in place where it needs to be. + // now we need to prepend it with calculated hmac. + using (var hmac = IncrementalHash.CreateHMAC(HashAlgorithmName.MD5, ntlm2hash)) + { + hmac.AppendData(serverChallenge); + hmac.AppendData(blob.Slice(DigestLength)); + hmac.GetHashAndReset(blob); + } + + SetField(ref field, blob.Length, payloadOffset); + + payloadOffset += blob.Length; + } + + private unsafe void WriteChannelBindingHash(Span hashBuffer) + { + if (_channelBinding != null) + { + IntPtr cbtData = _channelBinding.DangerousGetHandle(); + int cbtDataSize = _channelBinding.Size; + int written = MD5.HashData(new Span((void*)cbtData, cbtDataSize), hashBuffer); + Debug.Assert(written == MD5.HashSizeInBytes); + } + else + { + hashBuffer.Clear(); + } + } + + private byte[] ProcessTargetInfo(ReadOnlySpan targetInfo, out DateTime time, out bool hasNbNames) + { + int spnSize = _spn != null ? Encoding.Unicode.GetByteCount(_spn) : 0; + + if (spnSize > short.MaxValue) + { + throw new Win32Exception(NTE_FAIL); + } + + bool hasNbComputerName = false, hasNbDomainName = false; + byte[] targetInfoBuffer = new byte[targetInfo.Length + 20 /* channel binding */ + 4 + spnSize /* SPN */ + 8 /* flags */]; + int targetInfoOffset = 0; + + time = DateTime.UtcNow; + + if (targetInfo.Length > 0) + { + ReadOnlySpan info = targetInfo; + while (info.Length >= 4) + { + AvId ID = (AvId)info[0]; + byte l1 = info[2]; + byte l2 = info[3]; + int length = (l2 << 8) + l1; + + if (ID == AvId.EOL) + { + break; + } + + if (ID == AvId.Timestamp) + { + time = DateTime.FromFileTimeUtc(BitConverter.ToInt64(info.Slice(4, 8))); + } + else if (ID == AvId.TargetName || ID == AvId.ChannelBindings) + { + // Skip these, we insert them ourselves + info = info.Slice(length + 4); + continue; + } + else if (ID == AvId.NbComputerName) + { + hasNbComputerName = true; + } + else if (ID == AvId.NbDomainName) + { + hasNbDomainName = true; + } + + // Copy attribute-value pair into destination target info + info.Slice(0, length + 4).CopyTo(targetInfoBuffer.AsSpan(targetInfoOffset, length + 4)); + targetInfoOffset += length + 4; + + info = info.Slice(length + 4); + } + } + + hasNbNames = hasNbComputerName && hasNbDomainName; + + // Add new entries into destination target info + + // Target name (eg. HTTP/example.org) + targetInfoBuffer[targetInfoOffset] = (byte)AvId.TargetName; + BinaryPrimitives.WriteUInt16LittleEndian(targetInfoBuffer.AsSpan(2 + targetInfoOffset), (ushort)spnSize); + if (_spn != null) + { + int bytesWritten = Encoding.Unicode.GetBytes(_spn, targetInfoBuffer.AsSpan(4 + targetInfoOffset)); + Debug.Assert(bytesWritten == spnSize); + } + targetInfoOffset += spnSize + 4; + + // Channel binding + targetInfoBuffer[targetInfoOffset] = (byte)AvId.ChannelBindings; + targetInfoBuffer[targetInfoOffset + 2] = 16; + WriteChannelBindingHash(targetInfoBuffer.AsSpan(targetInfoOffset + 4, 16)); + targetInfoOffset += 16 + 4; + + // Flags + targetInfoBuffer[targetInfoOffset] = (byte)AvId.Flags; + targetInfoBuffer[targetInfoOffset + 2] = 4; // Length of flags + targetInfoBuffer[targetInfoOffset + 4] = 2; // MIC flag + targetInfoOffset += 8; + + // EOL + targetInfoOffset += 4; + + if (targetInfoOffset == targetInfoBuffer.Length) + { + return targetInfoBuffer; + } + + return targetInfoBuffer.AsSpan(targetInfoOffset).ToArray(); + } + + // Section 3.4.5.2 SIGNKEY, 3.4.5.3 SEALKEY + private static byte[] DeriveKey(ReadOnlySpan exportedSessionKey, ReadOnlySpan magic) + { + using (var md5 = IncrementalHash.CreateHash(HashAlgorithmName.MD5)) + { + md5.AppendData(exportedSessionKey); + md5.AppendData(magic); + return md5.GetHashAndReset(); + } + } + + // This gets decoded byte blob and returns response in binary form. + private unsafe byte[]? ProcessChallenge(ReadOnlySpan blob, out NegotiateAuthenticationStatusCode statusCode) + { + // TODO: Validate size and offsets + + ref readonly ChallengeMessage challengeMessage = ref MemoryMarshal.AsRef(blob.Slice(0, sizeof(ChallengeMessage))); + + // Verify message type and signature + if (challengeMessage.Header.MessageType != MessageType.Challenge || + !NtlmHeader.SequenceEqual(blob.Slice(0, NtlmHeader.Length))) + { + statusCode = NegotiateAuthenticationStatusCode.InvalidToken; + return null; + } + + Flags flags = BitConverter.IsLittleEndian ? challengeMessage.Flags : (Flags)BinaryPrimitives.ReverseEndianness((uint)challengeMessage.Flags); + ReadOnlySpan targetName = GetField(challengeMessage.TargetName, blob); + + // Only NTLMv2 with MIC is supported + // + // NegotiateSign and NegotiateKeyExchange are necessary to calculate the key + // that is used for MIC. + if ((flags & s_requiredFlags) != s_requiredFlags) + { + statusCode = NegotiateAuthenticationStatusCode.InvalidToken; + return null; + } + + ReadOnlySpan targetInfo = GetField(challengeMessage.TargetInfo, blob); + byte[] targetInfoBuffer = ProcessTargetInfo(targetInfo, out DateTime time, out bool hasNbNames); + + // If NTLM v2 authentication is used and the CHALLENGE_MESSAGE does not contain both + // MsvAvNbComputerName and MsvAvNbDomainName AVPairs and either Integrity is TRUE or + // Confidentiality is TRUE, then return STATUS_LOGON_FAILURE ([MS-ERREF] section 2.3.1). + if (!hasNbNames && (flags & (Flags.NegotiateSign | Flags.NegotiateSeal)) != 0) + { + statusCode = NegotiateAuthenticationStatusCode.InvalidToken; + return null; + } + + int responseLength = + sizeof(AuthenticateMessage) + + ChallengeResponseLength + + sizeof(NtChallengeResponse) + + targetInfoBuffer.Length + + Encoding.Unicode.GetByteCount(_credential.UserName) + + Encoding.Unicode.GetByteCount(_credential.Domain) + + s_workstation.Length + + SessionKeyLength; + + byte[] responseBytes = new byte[responseLength]; + Span responseAsSpan = new Span(responseBytes); + ref AuthenticateMessage response = ref MemoryMarshal.AsRef(responseAsSpan.Slice(0, sizeof(AuthenticateMessage))); + + // variable fields + Span payload = responseAsSpan; + int payloadOffset = sizeof(AuthenticateMessage); + + responseAsSpan.Clear(); + NtlmHeader.CopyTo(responseAsSpan); + + response.Header.MessageType = MessageType.Authenticate; + response.Flags = s_requiredFlags; + response.Version = s_version; + + // Calculate hash for hmac - same for lm2 and ntlm2 + Span ntlm2hash = stackalloc byte[DigestLength]; + makeNtlm2Hash(_credential.Domain, _credential.UserName, _credential.Password, ntlm2hash); + + // Get random bytes for client challenge + Span clientChallenge = stackalloc byte[ChallengeLength]; + RandomNumberGenerator.Fill(clientChallenge); + + // Create empty LM2 response. + SetField(ref response.LmChallengeResponse, ChallengeResponseLength, payloadOffset); + payload.Slice(payloadOffset, ChallengeResponseLength).Clear(); + payloadOffset += ChallengeResponseLength; + + // Create NTLM2 response + ReadOnlySpan serverChallenge = blob.Slice(24, 8); + makeNtlm2ChallengeResponse(time, ntlm2hash, serverChallenge, clientChallenge, targetInfoBuffer, ref response.NtChallengeResponse, payload, ref payloadOffset); + Debug.Assert(payloadOffset == sizeof(AuthenticateMessage) + ChallengeResponseLength + sizeof(NtChallengeResponse) + targetInfoBuffer.Length); + + AddToPayload(ref response.UserName, _credential.UserName, payload, ref payloadOffset); + AddToPayload(ref response.DomainName, _credential.Domain, payload, ref payloadOffset); + AddToPayload(ref response.Workstation, s_workstation, payload, ref payloadOffset); + + // Generate random session key that will be used for signing the messages + Span exportedSessionKey = stackalloc byte[16]; + RandomNumberGenerator.Fill(exportedSessionKey); + + // Both flags are necessary to exchange keys needed for MIC (!) + Debug.Assert(flags.HasFlag(Flags.NegotiateSign) && flags.HasFlag(Flags.NegotiateKeyExchange)); + + // Derive session base key + Span sessionBaseKey = stackalloc byte[HMACMD5.HashSizeInBytes]; + int sessionKeyWritten = HMACMD5.HashData(ntlm2hash, responseAsSpan.Slice(response.NtChallengeResponse.PayloadOffset, 16), sessionBaseKey); + Debug.Assert(sessionKeyWritten == HMACMD5.HashSizeInBytes); + + // Encrypt exportedSessionKey with sessionBaseKey + using (RC4 rc4 = new RC4(sessionBaseKey)) + { + Span encryptedRandomSessionKey = payload.Slice(payloadOffset, 16); + rc4.Transform(exportedSessionKey, encryptedRandomSessionKey); + SetField(ref response.EncryptedRandomSessionKey, 16, payloadOffset); + payloadOffset += 16; + } + + // Calculate MIC + Debug.Assert(_negotiateMessage != null); + using (var hmacMic = IncrementalHash.CreateHMAC(HashAlgorithmName.MD5, exportedSessionKey)) + { + hmacMic.AppendData(_negotiateMessage); + hmacMic.AppendData(blob); + hmacMic.AppendData(responseBytes.AsSpan(0, payloadOffset)); + hmacMic.GetHashAndReset(MemoryMarshal.CreateSpan(ref response.Mic[0], hmacMic.HashLengthInBytes)); + } + + // Derive signing keys + _clientSigningKey = DeriveKey(exportedSessionKey, ClientSigningKeyMagic); + _serverSigningKey = DeriveKey(exportedSessionKey, ServerSigningKeyMagic); + _clientSealingKey = DeriveKey(exportedSessionKey, ClientSealingKeyMagic); + _serverSealingKey = DeriveKey(exportedSessionKey, ServerSealingKeyMagic); + ResetKeys(); + _clientSequenceNumber = 0; + _serverSequenceNumber = 0; + CryptographicOperations.ZeroMemory(exportedSessionKey); + + Debug.Assert(payloadOffset == responseBytes.Length); + + statusCode = NegotiateAuthenticationStatusCode.Completed; + return responseBytes; + } + + internal void ResetKeys() + { + // Release buffers to pool + _clientSeal?.Dispose(); + _serverSeal?.Dispose(); + + _clientSeal = new RC4(_clientSealingKey); + _serverSeal = new RC4(_serverSealingKey); + } + + private void CalculateSignature( + ReadOnlySpan message, + uint sequenceNumber, + ReadOnlySpan signingKey, + RC4 seal, + Span signature) + { + BinaryPrimitives.WriteInt32LittleEndian(signature, 1); + BinaryPrimitives.WriteUInt32LittleEndian(signature.Slice(12), sequenceNumber); + using (var hmac = IncrementalHash.CreateHMAC(HashAlgorithmName.MD5, signingKey)) + { + hmac.AppendData(signature.Slice(12, 4)); + hmac.AppendData(message); + Span hmacResult = stackalloc byte[hmac.HashLengthInBytes]; + hmac.GetHashAndReset(hmacResult); + seal.Transform(hmacResult.Slice(0, 8), signature.Slice(4, 8)); + } + } + + public override bool VerifyMIC(ReadOnlySpan message, ReadOnlySpan signature) + { + // Check length and version + if (signature.Length != SignatureLength || + BinaryPrimitives.ReadInt32LittleEndian(signature) != 1 || + _serverSeal == null || + _serverSigningKey == null) + { + return false; + } + + Span expectedSignature = stackalloc byte[SignatureLength]; + CalculateSignature(message, _serverSequenceNumber, _serverSigningKey, _serverSeal, expectedSignature); + + _serverSequenceNumber++; + + return signature.SequenceEqual(expectedSignature); + } + + public override void GetMIC(ReadOnlySpan message, IBufferWriter signature) + { + Debug.Assert(_clientSeal is not null); + Debug.Assert(_clientSigningKey is not null); + + Span signatureBuffer = signature.GetSpan(SignatureLength); + CalculateSignature(message, _clientSequenceNumber, _clientSigningKey, _clientSeal, signatureBuffer); + _clientSequenceNumber++; + signature.Advance(SignatureLength); + } + + public override NegotiateAuthenticationStatusCode Wrap(ReadOnlySpan input, IBufferWriter outputWriter, bool _/*requestEncryption*/, out bool isEncrypted) + { + if (_clientSeal == null) + { + throw new InvalidOperationException(SR.net_auth_noauth); + } + + Span output = outputWriter.GetSpan(input.Length + SignatureLength); + _clientSeal.Transform(input, output.Slice(SignatureLength, input.Length)); + CalculateSignature(input, _clientSequenceNumber, _clientSigningKey, _clientSeal, output.Slice(0, SignatureLength)); + _clientSequenceNumber++; + + isEncrypted = true; + outputWriter.Advance(input.Length + SignatureLength); + + return NegotiateAuthenticationStatusCode.Completed; + } + + public override NegotiateAuthenticationStatusCode Unwrap(ReadOnlySpan input, IBufferWriter outputWriter, out bool wasEncrypted) + { + wasEncrypted = true; + + if (_serverSeal == null) + { + throw new InvalidOperationException(SR.net_auth_noauth); + } + + if (input.Length < SignatureLength) + { + return NegotiateAuthenticationStatusCode.InvalidToken; + } + + Span output = outputWriter.GetSpan(input.Length - SignatureLength); + _serverSeal.Transform(input.Slice(SignatureLength), output.Slice(0, input.Length - SignatureLength)); + if (!VerifyMIC(output.Slice(0, input.Length - SignatureLength), input.Slice(0, SignatureLength))) + { + CryptographicOperations.ZeroMemory(output); + return NegotiateAuthenticationStatusCode.MessageAltered; + } + + outputWriter.Advance(input.Length - SignatureLength); + + return NegotiateAuthenticationStatusCode.Completed; + } + + public override NegotiateAuthenticationStatusCode UnwrapInPlace(Span input, out int unwrappedOffset, out int unwrappedLength, out bool wasEncrypted) + { + wasEncrypted = true; + unwrappedOffset = SignatureLength; + unwrappedLength = input.Length - SignatureLength; + + if (_serverSeal == null) + { + throw new InvalidOperationException(SR.net_auth_noauth); + } + + if (input.Length < SignatureLength) + { + return NegotiateAuthenticationStatusCode.InvalidToken; + } + + _serverSeal.Transform(input.Slice(SignatureLength), input.Slice(SignatureLength)); + if (!VerifyMIC(input.Slice(SignatureLength), input.Slice(0, SignatureLength))) + { + CryptographicOperations.ZeroMemory(input.Slice(SignatureLength)); + return NegotiateAuthenticationStatusCode.MessageAltered; + } + + return NegotiateAuthenticationStatusCode.Completed; + } + } + } +} diff --git a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedSpnego.cs b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedSpnego.cs new file mode 100644 index 0000000000000000000000000000000000000000..dbf9fb8aee79914b0f5788a87b322b94b5e01ed9 --- /dev/null +++ b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedSpnego.cs @@ -0,0 +1,451 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.ComponentModel; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Formats.Asn1; +using System.Net.Security; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Security.Authentication.ExtendedProtection; +using System.Security.Cryptography; +using System.Security.Principal; +using System.Text; + +namespace System.Net +{ + internal abstract partial class NegotiateAuthenticationPal + { + internal sealed class ManagedSpnegoNegotiateAuthenticationPal : NegotiateAuthenticationPal + { + // Input parameters + private readonly NegotiateAuthenticationClientOptions _clientOptions; + + // State parameters + private byte[]? _spnegoMechList; + private bool _isAuthenticated; + private bool _supportKerberos; + private NegotiateAuthenticationPal? _optimisticMechanism; + private NegotiateAuthenticationPal? _mechanism; + + private const string SpnegoOid = "1.3.6.1.5.5.2"; + private const string NtlmOid = "1.3.6.1.4.1.311.2.2.10"; + private const string KerberosOid = "1.2.840.113554.1.2.2"; + + // rfc4178 + private enum NegotiationToken + { + NegTokenInit = 0, + NegTokenResp = 1 + } + + private enum NegTokenInit + { + MechTypes = 0, + ReqFlags = 1, + MechToken = 2, + MechListMIC = 3 + } + + private enum NegTokenResp + { + NegState = 0, + SupportedMech = 1, + ResponseToken = 2, + MechListMIC = 3 + } + + private enum NegState + { + Unknown = -1, // Internal. Not in RFC. + AcceptCompleted = 0, + AcceptIncomplete = 1, + Reject = 2, + RequestMic = 3 + } + + public override bool IsAuthenticated => _isAuthenticated && _mechanism?.IsAuthenticated == true; + public override bool IsSigned => _mechanism?.IsSigned ?? false; + public override bool IsEncrypted => _mechanism?.IsEncrypted ?? false; + public override bool IsMutuallyAuthenticated => _mechanism?.IsMutuallyAuthenticated ?? false; + public override string Package => _mechanism?.Package ?? NegotiationInfoClass.Negotiate; + public override string? TargetName => _clientOptions.TargetName; + public override IIdentity RemoteIdentity => _mechanism?.RemoteIdentity ?? throw new InvalidOperationException(); + public override System.Security.Principal.TokenImpersonationLevel ImpersonationLevel => _mechanism?.ImpersonationLevel ?? System.Security.Principal.TokenImpersonationLevel.Impersonation; + + public ManagedSpnegoNegotiateAuthenticationPal(NegotiateAuthenticationClientOptions clientOptions, bool supportKerberos = false) + { + Debug.Assert(clientOptions.Package == NegotiationInfoClass.Negotiate); + _clientOptions = clientOptions; + _supportKerberos = supportKerberos; + } + + public override void Dispose() + { + _optimisticMechanism?.Dispose(); + _optimisticMechanism = null; + _mechanism?.Dispose(); + _mechanism = null; + _isAuthenticated = false; + } + + public override unsafe byte[]? GetOutgoingBlob(ReadOnlySpan incomingBlob, out NegotiateAuthenticationStatusCode statusCode) + { + //Console.WriteLine($"ManagedSpnegoNegotiateAuthenticationPal.GetOutgoingBlob > {Convert.ToBase64String(incomingBlob)}"); + + byte[]? outgoingBlob; + if (_spnegoMechList == null) + { + outgoingBlob = CreateSpNegoNegotiateMessage(incomingBlob, out statusCode); + } + else + { + outgoingBlob = ProcessSpNegoChallenge(incomingBlob, out statusCode); + } + + //Console.WriteLine($"ManagedSpnegoNegotiateAuthenticationPal.GetOutgoingBlob < {(outgoingBlob == null ? "null" : Convert.ToBase64String(outgoingBlob))} {statusCode}"); + + return outgoingBlob; + } + + private NegotiateAuthenticationPal CreateMechanismForPackage(string packageName) + { + return NegotiateAuthenticationPal.Create(new NegotiateAuthenticationClientOptions + { + Package = packageName, + Credential = _clientOptions.Credential, + TargetName = _clientOptions.TargetName, + Binding = _clientOptions.Binding, + RequiredProtectionLevel = _clientOptions.RequiredProtectionLevel, + RequireMutualAuthentication = _clientOptions.RequireMutualAuthentication, + AllowedImpersonationLevel = _clientOptions.AllowedImpersonationLevel, + }); + } + + private IEnumerable> EnumerateMechanisms() + { + if (_supportKerberos) + { + yield return new KeyValuePair(NegotiationInfoClass.Kerberos, KerberosOid); + } + + yield return new KeyValuePair(NegotiationInfoClass.NTLM, NtlmOid); + } + + private byte[]? CreateSpNegoNegotiateMessage(ReadOnlySpan incomingBlob, out NegotiateAuthenticationStatusCode statusCode) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + using (writer.PushSequence(new Asn1Tag(TagClass.Application, 0))) + { + writer.WriteObjectIdentifier(SpnegoOid); + + // NegTokenInit::= SEQUENCE { + // mechTypes[0] MechTypeList, + // reqFlags[1] ContextFlags OPTIONAL, + // --inherited from RFC 2478 for backward compatibility, + // --RECOMMENDED to be left out + // mechToken[2] OCTET STRING OPTIONAL, + // mechListMIC[3] OCTET STRING OPTIONAL, + // ... + // } + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegotiationToken.NegTokenInit))) + { + using (writer.PushSequence()) + { + byte[]? mechBlob = null; + + // MechType::= OBJECT IDENTIFIER + // -- OID represents each security mechanism as suggested by + // --[RFC2743] + // + // MechTypeList::= SEQUENCE OF MechType + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenInit.MechTypes))) + { + AsnWriter mechListWriter = new AsnWriter(AsnEncodingRules.DER); + + using (mechListWriter.PushSequence()) + { + foreach (KeyValuePair packageAndOid in EnumerateMechanisms()) + { + if (_optimisticMechanism == null) + { + _optimisticMechanism = CreateMechanismForPackage(packageAndOid.Key); + mechBlob = _optimisticMechanism.GetOutgoingBlob(incomingBlob, out statusCode); + if (statusCode != NegotiateAuthenticationStatusCode.ContinueNeeded && + statusCode != NegotiateAuthenticationStatusCode.Completed) + { + mechBlob = null; + _optimisticMechanism?.Dispose(); + _optimisticMechanism = null; + if (statusCode != NegotiateAuthenticationStatusCode.Unsupported) + { + return null; + } + continue; + } + } + + mechListWriter.WriteObjectIdentifier(packageAndOid.Value); + } + } + + _spnegoMechList = mechListWriter.Encode(); + mechListWriter.CopyTo(writer); + } + + if (mechBlob != null) + { + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenInit.MechToken))) + { + writer.WriteOctetString(mechBlob); + } + } + } + } + } + + statusCode = NegotiateAuthenticationStatusCode.ContinueNeeded; + return writer.Encode(); + } + + private byte[]? ProcessSpNegoChallenge(ReadOnlySpan challenge, out NegotiateAuthenticationStatusCode statusCode) + { + NegState state = NegState.Unknown; + string? mech = null; + byte[]? blob = null; + byte[]? mechListMIC = null; + + try + { + AsnValueReader reader = new AsnValueReader(challenge, AsnEncodingRules.DER); + AsnValueReader challengeReader = reader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegotiationToken.NegTokenResp)); + reader.ThrowIfNotEmpty(); + + // NegTokenResp ::= SEQUENCE { + // negState[0] ENUMERATED { + // accept - completed(0), + // accept - incomplete(1), + // reject(2), + // request - mic(3) + // } OPTIONAL, + // --REQUIRED in the first reply from the target + // supportedMech[1] MechType OPTIONAL, + // --present only in the first reply from the target + // responseToken[2] OCTET STRING OPTIONAL, + // mechListMIC[3] OCTET STRING OPTIONAL, + // ... + // } + + challengeReader = challengeReader.ReadSequence(); + + if (challengeReader.HasData && challengeReader.PeekTag().HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.NegState))) + { + AsnValueReader valueReader = challengeReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.NegState)); + state = valueReader.ReadEnumeratedValue(); + valueReader.ThrowIfNotEmpty(); + } + + if (challengeReader.HasData && challengeReader.PeekTag().HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.SupportedMech))) + { + AsnValueReader valueReader = challengeReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.SupportedMech)); + mech = valueReader.ReadObjectIdentifier(); + valueReader.ThrowIfNotEmpty(); + } + + if (challengeReader.HasData && challengeReader.PeekTag().HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.ResponseToken))) + { + AsnValueReader valueReader = challengeReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.ResponseToken)); + blob = valueReader.ReadOctetString(); + valueReader.ThrowIfNotEmpty(); + } + + if (challengeReader.HasData && challengeReader.PeekTag().HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.MechListMIC))) + { + AsnValueReader valueReader = challengeReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.MechListMIC)); + mechListMIC = valueReader.ReadOctetString(); + valueReader.ThrowIfNotEmpty(); + } + + challengeReader.ThrowIfNotEmpty(); + } + catch (AsnContentException) + { + statusCode = NegotiateAuthenticationStatusCode.InvalidToken; + return null; + } + + // Validate and choose the mechanism if necessary + string? requestedPackage = mech switch + { + NtlmOid => NegotiationInfoClass.NTLM, + KerberosOid => NegotiationInfoClass.Kerberos, + _ => null + }; + + if (_mechanism is null) + { + if (requestedPackage is null) + { + statusCode = NegotiateAuthenticationStatusCode.Unsupported; + return null; + } + + if (requestedPackage == _optimisticMechanism?.Package) + { + _mechanism = _optimisticMechanism; + } + else + { + // Abandon the optimistic path and restart with a new mechanism + _optimisticMechanism?.Dispose(); + _mechanism = NegotiateAuthenticationPal.Create(new NegotiateAuthenticationClientOptions + { + Package = requestedPackage, + Credential = _clientOptions.Credential, + TargetName = _clientOptions.TargetName, + Binding = _clientOptions.Binding, + RequiredProtectionLevel = _clientOptions.RequiredProtectionLevel, + RequireMutualAuthentication = _clientOptions.RequireMutualAuthentication, + AllowedImpersonationLevel = _clientOptions.AllowedImpersonationLevel, + }); + } + + _optimisticMechanism = null; + } + else + { + if (requestedPackage != null && + _mechanism.Package != requestedPackage) + { + statusCode = NegotiateAuthenticationStatusCode.InvalidToken; + return null; + } + } + + if (blob?.Length > 0) + { + // Process decoded blob. + byte[]? response = _mechanism.GetOutgoingBlob(blob, out statusCode); + + if (statusCode != NegotiateAuthenticationStatusCode.ContinueNeeded && + statusCode != NegotiateAuthenticationStatusCode.Completed) + { + return null; + } + + if (response?.Length > 0) + { + AsnWriter writer = new AsnWriter(AsnEncodingRules.DER); + + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegotiationToken.NegTokenResp))) + { + using (writer.PushSequence()) + { + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.ResponseToken))) + { + writer.WriteOctetString(response); + } + + if (statusCode == NegotiateAuthenticationStatusCode.Completed) + { + using (writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, (int)NegTokenResp.MechListMIC))) + { + ArrayBufferWriter micBuffer = new ArrayBufferWriter(); + _mechanism.GetMIC(_spnegoMechList, micBuffer); + writer.WriteOctetString(micBuffer.WrittenSpan); + } + } + } + } + + statusCode = state == NegState.RequestMic ? NegotiateAuthenticationStatusCode.ContinueNeeded : NegotiateAuthenticationStatusCode.Completed; + _isAuthenticated = statusCode == NegotiateAuthenticationStatusCode.Completed; + return writer.Encode(); + } + } + + if (mechListMIC != null) + { + if (_spnegoMechList == null || state != NegState.AcceptCompleted) + { + statusCode = NegotiateAuthenticationStatusCode.GenericFailure; + return null; + } + + if (!_mechanism.VerifyMIC(_spnegoMechList, mechListMIC)) + { + statusCode = NegotiateAuthenticationStatusCode.MessageAltered; + return null; + } + + (_mechanism as ManagedNtlmNegotiateAuthenticationPal)?.ResetKeys(); + } + + _isAuthenticated = state == NegState.AcceptCompleted || state == NegState.Reject; + statusCode = state switch { + NegState.AcceptCompleted => NegotiateAuthenticationStatusCode.Completed, + NegState.AcceptIncomplete => NegotiateAuthenticationStatusCode.ContinueNeeded, + NegState.Reject => NegotiateAuthenticationStatusCode.UnknownCredentials, + _ => NegotiateAuthenticationStatusCode.GenericFailure + }; + + return null; + } + + public override NegotiateAuthenticationStatusCode Wrap(ReadOnlySpan input, IBufferWriter outputWriter, bool requestEncryption, out bool isEncrypted) + { + if (_mechanism is null || !_isAuthenticated) + { + throw new InvalidOperationException(SR.net_auth_noauth); + } + + return _mechanism.Wrap(input, outputWriter, requestEncryption, out isEncrypted); + } + + public override NegotiateAuthenticationStatusCode Unwrap(ReadOnlySpan input, IBufferWriter outputWriter, out bool wasEncrypted) + { + if (_mechanism is null || !_isAuthenticated) + { + throw new InvalidOperationException(SR.net_auth_noauth); + } + + return _mechanism.Unwrap(input, outputWriter, out wasEncrypted); + } + + public override NegotiateAuthenticationStatusCode UnwrapInPlace(Span input, out int unwrappedOffset, out int unwrappedLength, out bool wasEncrypted) + { + if (_mechanism is null || !_isAuthenticated) + { + throw new InvalidOperationException(SR.net_auth_noauth); + } + + return _mechanism.UnwrapInPlace(input, out unwrappedOffset, out unwrappedLength, out wasEncrypted); + } + + public override bool VerifyMIC(ReadOnlySpan message, ReadOnlySpan signature) + { + if (_mechanism is null || !_isAuthenticated) + { + throw new InvalidOperationException(SR.net_auth_noauth); + } + + return _mechanism.VerifyMIC(message, signature); + } + + public override void GetMIC(ReadOnlySpan message, IBufferWriter signature) + { + if (_mechanism is null || !_isAuthenticated) + { + throw new InvalidOperationException(SR.net_auth_noauth); + } + + _mechanism.GetMIC(message, signature); + } + } + } +} diff --git a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unix.cs b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unix.cs new file mode 100644 index 0000000000000000000000000000000000000000..900d66c05bfc7a85473b8e57ef5fcf10d7e26ada --- /dev/null +++ b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unix.cs @@ -0,0 +1,779 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Buffers.Binary; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Principal; +using System.Security.Authentication.ExtendedProtection; +using System.Text; +using System.Net.Security; +using Microsoft.Win32.SafeHandles; + +namespace System.Net +{ + internal partial class NegotiateAuthenticationPal + { + private static bool UseManagedNtlm { get; } = AppContext.TryGetSwitch("System.Net.Security.UseManagedNtlm", out bool useManagedNtlm) && useManagedNtlm; + + public static NegotiateAuthenticationPal Create(NegotiateAuthenticationClientOptions clientOptions) + { + if (UseManagedNtlm) + { + switch (clientOptions.Package) + { + case NegotiationInfoClass.NTLM: + return new ManagedNtlmNegotiateAuthenticationPal(clientOptions); + + case NegotiationInfoClass.Negotiate: + return new ManagedSpnegoNegotiateAuthenticationPal(clientOptions, supportKerberos: true); + } + } + + try + { + return new UnixNegotiateAuthenticationPal(clientOptions); + } + catch (Win32Exception) + { + return new UnsupportedNegotiateAuthenticationPal(clientOptions); + } + catch (PlatformNotSupportedException) + { + return new UnsupportedNegotiateAuthenticationPal(clientOptions); + } + catch (EntryPointNotFoundException) + { + // GSSAPI shim may not be available on some platforms (Linux Bionic) + return new UnsupportedNegotiateAuthenticationPal(clientOptions); + } + } + + public static NegotiateAuthenticationPal Create(NegotiateAuthenticationServerOptions serverOptions) + { + try + { + return new UnixNegotiateAuthenticationPal(serverOptions); + } + catch (Win32Exception) + { + return new UnsupportedNegotiateAuthenticationPal(serverOptions); + } + catch (PlatformNotSupportedException) + { + return new UnsupportedNegotiateAuthenticationPal(serverOptions); + } + catch (EntryPointNotFoundException) + { + // GSSAPI shim may not be available on some platforms (Linux Bionic) + return new UnsupportedNegotiateAuthenticationPal(serverOptions); + } + } + + internal sealed class UnixNegotiateAuthenticationPal : NegotiateAuthenticationPal + { + private bool _isServer; + private bool _isAuthenticated; + private byte[]? _tokenBuffer; + private SafeGssCredHandle _credentialsHandle; + private SafeGssContextHandle? _securityContext; + private SafeGssNameHandle? _targetNameHandle; + private Interop.NetSecurityNative.GssFlags _requestedContextFlags; + private Interop.NetSecurityNative.GssFlags _contextFlags; + private string _package; + private string? _spn; + private ChannelBinding? _channelBinding; + private readonly Interop.NetSecurityNative.PackageType _packageType; + + public override bool IsAuthenticated => _isAuthenticated; + + public override bool IsSigned => (_contextFlags & Interop.NetSecurityNative.GssFlags.GSS_C_INTEG_FLAG) != 0; + + public override bool IsEncrypted => (_contextFlags & Interop.NetSecurityNative.GssFlags.GSS_C_CONF_FLAG) != 0; + + public override bool IsMutuallyAuthenticated => (_contextFlags & Interop.NetSecurityNative.GssFlags.GSS_C_MUTUAL_FLAG) != 0; + + public override string Package => _package; + + public override string? TargetName + { + get + { + if (_isServer && _spn == null) + { + Debug.Assert(_securityContext is not null && _isAuthenticated, "Trying to get the client SPN before handshaking is done!"); + throw new PlatformNotSupportedException(SR.net_nego_server_not_supported); + } + return _spn; + } + } + + public override IIdentity RemoteIdentity + { + get + { + IIdentity? result; + string? name = _isServer ? null : TargetName; + string protocol = Package; + + Debug.Assert(_securityContext is not null); + + if (_isServer) + { + try + { + name = GssGetUser(_securityContext); + } + catch (Exception ex) + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, ex); + throw; + } + } + + // On the client we don't have access to the remote side identity. + result = new GenericIdentity(name ?? string.Empty, protocol); + return result; + } + } + + public override System.Security.Principal.TokenImpersonationLevel ImpersonationLevel + { + get + { + return + (_contextFlags & Interop.NetSecurityNative.GssFlags.GSS_C_DELEG_FLAG) != 0 && Package != NegotiationInfoClass.NTLM ? TokenImpersonationLevel.Delegation : + (_contextFlags & Interop.NetSecurityNative.GssFlags.GSS_C_IDENTIFY_FLAG) != 0 ? TokenImpersonationLevel.Identification : + TokenImpersonationLevel.Impersonation; + } + } + + public UnixNegotiateAuthenticationPal(NegotiateAuthenticationClientOptions clientOptions) + { + Interop.NetSecurityNative.GssFlags contextFlags = clientOptions.RequiredProtectionLevel switch + { + ProtectionLevel.Sign => Interop.NetSecurityNative.GssFlags.GSS_C_INTEG_FLAG, + ProtectionLevel.EncryptAndSign => Interop.NetSecurityNative.GssFlags.GSS_C_INTEG_FLAG | Interop.NetSecurityNative.GssFlags.GSS_C_CONF_FLAG, + _ => 0 + }; + + contextFlags |= clientOptions.RequireMutualAuthentication ? Interop.NetSecurityNative.GssFlags.GSS_C_MUTUAL_FLAG : 0; + + contextFlags |= clientOptions.AllowedImpersonationLevel switch + { + TokenImpersonationLevel.Identification => Interop.NetSecurityNative.GssFlags.GSS_C_IDENTIFY_FLAG, + TokenImpersonationLevel.Delegation => Interop.NetSecurityNative.GssFlags.GSS_C_DELEG_FLAG, + _ => 0 + }; + + _isServer = false; + _spn = clientOptions.TargetName; + _securityContext = null; + _requestedContextFlags = contextFlags; + _package = clientOptions.Package; + _channelBinding = clientOptions.Binding; + _packageType = GetPackageType(_package); + + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Peer SPN-> '{_spn}'"); + + if (clientOptions.Credential == CredentialCache.DefaultCredentials || + string.IsNullOrWhiteSpace(clientOptions.Credential.UserName) || + string.IsNullOrWhiteSpace(clientOptions.Credential.Password)) + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "using DefaultCredentials"); + _credentialsHandle = AcquireDefaultCredential(); + + if (_packageType == Interop.NetSecurityNative.PackageType.NTLM) + { + // NTLM authentication is not possible with default credentials which are no-op + throw new PlatformNotSupportedException(SR.net_ntlm_not_possible_default_cred); + } + if (string.IsNullOrEmpty(_spn)) + { + throw new PlatformNotSupportedException(SR.net_nego_not_supported_empty_target_with_defaultcreds); + } + } + else + { + _credentialsHandle = AcquireCredentialsHandle(clientOptions.Credential); + } + } + + public UnixNegotiateAuthenticationPal(NegotiateAuthenticationServerOptions serverOptions) + { + Interop.NetSecurityNative.GssFlags contextFlags = serverOptions.RequiredProtectionLevel switch + { + ProtectionLevel.Sign => Interop.NetSecurityNative.GssFlags.GSS_C_INTEG_FLAG, + ProtectionLevel.EncryptAndSign => Interop.NetSecurityNative.GssFlags.GSS_C_INTEG_FLAG | Interop.NetSecurityNative.GssFlags.GSS_C_CONF_FLAG, + _ => 0 + }; + + // NOTE: Historically serverOptions.Policy was ignored on Unix without an exception + // or error message. We continue to do so for compatibility reasons and because there + // are no direct equivalents in GSSAPI. + + _isServer = true; + _securityContext = null; + _requestedContextFlags = contextFlags; + _package = serverOptions.Package; + _channelBinding = serverOptions.Binding; + _packageType = GetPackageType(_package); + + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Peer SPN-> '{_spn}'"); + + if (serverOptions.Credential == CredentialCache.DefaultCredentials || + string.IsNullOrWhiteSpace(serverOptions.Credential.UserName) || + string.IsNullOrWhiteSpace(serverOptions.Credential.Password)) + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "using DefaultCredentials"); + _credentialsHandle = SafeGssCredHandle.CreateAcceptor(); + } + else + { + // NOTE: The input parameter was previously ignored and SafeGssCredHandle.CreateAcceptor + // was always used. We don't know of any uses with non-default credentials so this code + // path is essentially untested. + _credentialsHandle = AcquireCredentialsHandle(serverOptions.Credential); + } + } + + public override void Dispose() + { + _credentialsHandle?.Dispose(); + _targetNameHandle?.Dispose(); + _securityContext?.Dispose(); + } + + public override byte[]? GetOutgoingBlob(ReadOnlySpan incomingBlob, out NegotiateAuthenticationStatusCode statusCode) + { + int resultBlobLength; + if (!_isServer) + { + // client session + statusCode = InitializeSecurityContext( + ref _credentialsHandle!, + ref _securityContext, + ref _targetNameHandle, + _spn, + _requestedContextFlags, + incomingBlob, + _channelBinding, + ref _tokenBuffer, + out resultBlobLength, + ref _contextFlags); + + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"SSPIWrapper.InitializeSecurityContext() returns statusCode:{statusCode}"); + } + else + { + // TODO: We don't currently check channel bindings. + + // Server session. + statusCode = AcceptSecurityContext( + _credentialsHandle, + ref _securityContext, + incomingBlob, + ref _tokenBuffer, + out resultBlobLength, + ref _contextFlags); + + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"SSPIWrapper.AcceptSecurityContext() returns statusCode:{statusCode}"); + } + + if (statusCode >= NegotiateAuthenticationStatusCode.GenericFailure) + { + Dispose(); + _isAuthenticated = true; + _tokenBuffer = null; + return null; + } + + byte[]? result = + resultBlobLength == 0 || _tokenBuffer == null ? null : + _tokenBuffer.Length == resultBlobLength ? _tokenBuffer : + _tokenBuffer[0..resultBlobLength]; + + // The return value will tell us correctly if the handshake is over or not + if (statusCode == NegotiateAuthenticationStatusCode.Completed) + { + // Success. + _isAuthenticated = true; + _tokenBuffer = null; + } + else + { + // We need to continue. + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"need continue _securityContext:{_securityContext}"); + } + + return result; + } + + public override NegotiateAuthenticationStatusCode Wrap(ReadOnlySpan input, IBufferWriter outputWriter, bool requestEncryption, out bool isEncrypted) + { + Debug.Assert(_securityContext is not null); + + Interop.NetSecurityNative.GssBuffer encryptedBuffer = default; + try + { + Interop.NetSecurityNative.Status minorStatus; + bool encrypt = requestEncryption; + Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.WrapBuffer( + out minorStatus, + _securityContext, + ref encrypt, + input, + ref encryptedBuffer); + isEncrypted = encrypt; + if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) + { + return NegotiateAuthenticationStatusCode.GenericFailure; + } + + encryptedBuffer.Span.CopyTo(outputWriter.GetSpan(encryptedBuffer.Span.Length)); + outputWriter.Advance(encryptedBuffer.Span.Length); + return NegotiateAuthenticationStatusCode.Completed; + } + finally + { + encryptedBuffer.Dispose(); + } + } + + public override NegotiateAuthenticationStatusCode Unwrap(ReadOnlySpan input, IBufferWriter outputWriter, out bool wasEncrypted) + { + Debug.Assert(_securityContext is not null); + + Interop.NetSecurityNative.GssBuffer decryptedBuffer = default(Interop.NetSecurityNative.GssBuffer); + try + { + Interop.NetSecurityNative.Status minorStatus; + Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.UnwrapBuffer(out minorStatus, _securityContext, out wasEncrypted, input, ref decryptedBuffer); + if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) + { + return status switch + { + Interop.NetSecurityNative.Status.GSS_S_BAD_SIG => NegotiateAuthenticationStatusCode.MessageAltered, + _ => NegotiateAuthenticationStatusCode.InvalidToken + }; + } + + decryptedBuffer.Span.CopyTo(outputWriter.GetSpan(decryptedBuffer.Span.Length)); + outputWriter.Advance(decryptedBuffer.Span.Length); + return NegotiateAuthenticationStatusCode.Completed; + } + finally + { + decryptedBuffer.Dispose(); + } + } + + public override unsafe NegotiateAuthenticationStatusCode UnwrapInPlace(Span input, out int unwrappedOffset, out int unwrappedLength, out bool wasEncrypted) + { + Debug.Assert(_securityContext is not null); + + Interop.NetSecurityNative.GssBuffer decryptedBuffer = default(Interop.NetSecurityNative.GssBuffer); + try + { + Interop.NetSecurityNative.Status minorStatus; + Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.UnwrapBuffer(out minorStatus, _securityContext, out wasEncrypted, input, ref decryptedBuffer); + if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) + { + unwrappedOffset = 0; + unwrappedLength = 0; + return status switch + { + Interop.NetSecurityNative.Status.GSS_S_BAD_SIG => NegotiateAuthenticationStatusCode.MessageAltered, + _ => NegotiateAuthenticationStatusCode.InvalidToken + }; + } + + decryptedBuffer.Span.CopyTo(input); + unwrappedOffset = 0; + unwrappedLength = decryptedBuffer.Span.Length; + return NegotiateAuthenticationStatusCode.Completed; + } + finally + { + decryptedBuffer.Dispose(); + } + } + + public override unsafe void GetMIC(ReadOnlySpan message, IBufferWriter signature) + { + Debug.Assert(_securityContext is not null); + + Interop.NetSecurityNative.GssBuffer micBuffer = default; + try + { + Interop.NetSecurityNative.Status minorStatus; + Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.GetMic( + out minorStatus, + _securityContext, + message, + ref micBuffer); + if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) + { + throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); + } + + signature.Write(micBuffer.Span); + } + finally + { + micBuffer.Dispose(); + } + } + + public override unsafe bool VerifyMIC(ReadOnlySpan message, ReadOnlySpan signature) + { + Debug.Assert(_securityContext is not null); + + Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.VerifyMic( + out _, + _securityContext, + message, + signature); + return status == Interop.NetSecurityNative.Status.GSS_S_COMPLETE; + } + + private static Interop.NetSecurityNative.PackageType GetPackageType(string package) + { + if (string.Equals(package, NegotiationInfoClass.Negotiate, StringComparison.OrdinalIgnoreCase)) + { + return Interop.NetSecurityNative.PackageType.Negotiate; + } + else if (string.Equals(package, NegotiationInfoClass.NTLM, StringComparison.OrdinalIgnoreCase)) + { + return Interop.NetSecurityNative.PackageType.NTLM; + } + else if (string.Equals(package, NegotiationInfoClass.Kerberos, StringComparison.OrdinalIgnoreCase)) + { + return Interop.NetSecurityNative.PackageType.Kerberos; + } + else + { + // Native shim currently supports only NTLM, Negotiate and Kerberos + throw new PlatformNotSupportedException(SR.net_securitypackagesupport); + } + } + + private SafeGssCredHandle AcquireDefaultCredential() + { + try + { + return SafeGssCredHandle.Create(string.Empty, string.Empty, _packageType); + } + catch (Exception ex) + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, ex); + + // NOTE: We throw PlatformNotSupportedException which is caught in + // NegotiateAuthenticationPal.Create and transformed into instantiation of + // UnsupportedNegotiateAuthenticationPal. + throw new PlatformNotSupportedException(ex.Message, ex); + } + } + + private SafeGssCredHandle AcquireCredentialsHandle(NetworkCredential credential) + { + try + { + string username = credential.UserName; + string password = credential.Password; + ReadOnlySpan domain = credential.Domain; + + Debug.Assert(username != null && password != null, "Username and Password can not be null"); + + // any invalid user format will not be manipulated and passed as it is. + int index = username.IndexOf('\\'); + if (index > 0 && username.IndexOf('\\', index + 1) < 0 && domain.IsEmpty) + { + domain = username.AsSpan(0, index); + username = username.Substring(index + 1); + } + + // remove any leading and trailing whitespace + username = username.Trim(); + domain = domain.Trim(); + if (!username.Contains('@') && !domain.IsEmpty) + { + username = string.Concat(username, "@", domain); + } + + return SafeGssCredHandle.Create(username, password, _packageType); + } + catch (Exception ex) + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, ex); + + // NOTE: We throw PlatformNotSupportedException which is caught in + // NegotiateAuthenticationPal.Create and transformed into instantiation of + // UnsupportedNegotiateAuthenticationPal. + throw new PlatformNotSupportedException(ex.Message, ex); + } + } + + private static string GssGetUser(SafeGssContextHandle context) + { + Interop.NetSecurityNative.GssBuffer token = default(Interop.NetSecurityNative.GssBuffer); + + try + { + Interop.NetSecurityNative.Status status + = Interop.NetSecurityNative.GetUser(out var minorStatus, + context, + ref token); + + if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) + { + throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); + } + + ReadOnlySpan tokenBytes = token.Span; + int length = tokenBytes.Length; + if (length > 0 && tokenBytes[length - 1] == '\0') + { + // Some GSS-API providers (gss-ntlmssp) include the terminating null with strings, so skip that. + tokenBytes = tokenBytes.Slice(0, length - 1); + } + + return Encoding.UTF8.GetString(tokenBytes); + } + finally + { + token.Dispose(); + } + } + + private NegotiateAuthenticationStatusCode InitializeSecurityContext( + ref SafeGssCredHandle credentialsHandle, + ref SafeGssContextHandle? contextHandle, + ref SafeGssNameHandle? targetNameHandle, + string? spn, + Interop.NetSecurityNative.GssFlags requestedContextFlags, + ReadOnlySpan incomingBlob, + ChannelBinding? channelBinding, + ref byte[]? resultBlob, + out int resultBlobLength, + ref Interop.NetSecurityNative.GssFlags contextFlags) + { + resultBlob = null; + resultBlobLength = 0; + + if (contextHandle == null) + { + if (NetEventSource.Log.IsEnabled()) + { + string protocol = _packageType switch { + Interop.NetSecurityNative.PackageType.NTLM => "NTLM", + Interop.NetSecurityNative.PackageType.Kerberos => "Kerberos", + _ => "SPNEGO" + }; + NetEventSource.Info(this, $"requested protocol = {protocol}, target = {spn}"); + } + + targetNameHandle = SafeGssNameHandle.CreateTarget(spn!); + contextHandle = new SafeGssContextHandle(); + } + + Interop.NetSecurityNative.GssBuffer token = default(Interop.NetSecurityNative.GssBuffer); + Interop.NetSecurityNative.Status status; + Interop.NetSecurityNative.Status minorStatus; + try + { + uint outputFlags; + bool isNtlmUsed; + + if (channelBinding != null) + { + // If a TLS channel binding token (cbt) is available then get the pointer + // to the application specific data. + int appDataOffset = Marshal.SizeOf(); + Debug.Assert(appDataOffset < channelBinding.Size); + IntPtr cbtAppData = channelBinding.DangerousGetHandle() + appDataOffset; + int cbtAppDataSize = channelBinding.Size - appDataOffset; + status = Interop.NetSecurityNative.InitSecContext(out minorStatus, + credentialsHandle, + ref contextHandle, + _packageType, + cbtAppData, + cbtAppDataSize, + targetNameHandle, + (uint)requestedContextFlags, + incomingBlob, + ref token, + out outputFlags, + out isNtlmUsed); + } + else + { + status = Interop.NetSecurityNative.InitSecContext(out minorStatus, + credentialsHandle, + ref contextHandle, + _packageType, + targetNameHandle, + (uint)requestedContextFlags, + incomingBlob, + ref token, + out outputFlags, + out isNtlmUsed); + } + + if ((status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) && + (status != Interop.NetSecurityNative.Status.GSS_S_CONTINUE_NEEDED)) + { + if (contextHandle.IsInvalid) + { + targetNameHandle?.Dispose(); + } + + Interop.NetSecurityNative.GssApiException gex = new Interop.NetSecurityNative.GssApiException(status, minorStatus); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, gex); + resultBlob = Array.Empty(); + return GetErrorCode(gex); + } + + resultBlob = token.ToByteArray(); + resultBlobLength = resultBlob?.Length ?? 0; + + if (status == Interop.NetSecurityNative.Status.GSS_S_COMPLETE) + { + if (NetEventSource.Log.IsEnabled()) + { + string protocol = _packageType switch { + Interop.NetSecurityNative.PackageType.NTLM => "NTLM", + Interop.NetSecurityNative.PackageType.Kerberos => "Kerberos", + _ => isNtlmUsed ? "SPNEGO-NTLM" : "SPNEGO-Kerberos" + }; + NetEventSource.Info(this, $"actual protocol = {protocol}"); + } + + // Populate protocol used for authentication + _package = isNtlmUsed ? NegotiationInfoClass.NTLM : NegotiationInfoClass.Kerberos; + } + + Debug.Assert(resultBlob != null, "Unexpected null buffer returned by GssApi"); + contextFlags = (Interop.NetSecurityNative.GssFlags)outputFlags; + + return status == Interop.NetSecurityNative.Status.GSS_S_COMPLETE ? + NegotiateAuthenticationStatusCode.Completed : + NegotiateAuthenticationStatusCode.ContinueNeeded; + } + catch (Exception ex) + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, ex); + return NegotiateAuthenticationStatusCode.GenericFailure; + } + finally + { + token.Dispose(); + } + } + + private NegotiateAuthenticationStatusCode AcceptSecurityContext( + SafeGssCredHandle credentialsHandle, + ref SafeGssContextHandle? contextHandle, + //ContextFlagsPal requestedContextFlags, + ReadOnlySpan incomingBlob, + //ChannelBinding? channelBinding, + ref byte[]? resultBlob, + out int resultBlobLength, + ref Interop.NetSecurityNative.GssFlags contextFlags) + { + contextHandle ??= new SafeGssContextHandle(); + + Interop.NetSecurityNative.GssBuffer token = default(Interop.NetSecurityNative.GssBuffer); + try + { + Interop.NetSecurityNative.Status status; + Interop.NetSecurityNative.Status minorStatus; + status = Interop.NetSecurityNative.AcceptSecContext(out minorStatus, + credentialsHandle, + ref contextHandle, + incomingBlob, + ref token, + out uint outputFlags, + out bool isNtlmUsed); + + if ((status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) && + (status != Interop.NetSecurityNative.Status.GSS_S_CONTINUE_NEEDED)) + { + Interop.NetSecurityNative.GssApiException gex = new Interop.NetSecurityNative.GssApiException(status, minorStatus); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, gex); + resultBlobLength = 0; + return GetErrorCode(gex); + } + + resultBlob = token.ToByteArray(); + + Debug.Assert(resultBlob != null, "Unexpected null buffer returned by GssApi"); + + contextFlags = (Interop.NetSecurityNative.GssFlags)outputFlags; + resultBlobLength = resultBlob.Length; + + NegotiateAuthenticationStatusCode errorCode; + if (status == Interop.NetSecurityNative.Status.GSS_S_COMPLETE) + { + if (NetEventSource.Log.IsEnabled()) + { + string protocol = isNtlmUsed ? "SPNEGO-NTLM" : "SPNEGO-Kerberos"; + NetEventSource.Info(this, $"AcceptSecurityContext: actual protocol = {protocol}"); + } + + // Populate protocol used for authentication + _package = isNtlmUsed ? NegotiationInfoClass.NTLM : NegotiationInfoClass.Kerberos; + errorCode = NegotiateAuthenticationStatusCode.Completed; + } + else + { + errorCode = NegotiateAuthenticationStatusCode.ContinueNeeded; + } + + return errorCode; + } + catch (Exception ex) + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, ex); + resultBlobLength = 0; + return NegotiateAuthenticationStatusCode.GenericFailure; + } + finally + { + token.Dispose(); + } + } + + // https://www.gnu.org/software/gss/reference/gss.pdf (page 25) + private static NegotiateAuthenticationStatusCode GetErrorCode(Interop.NetSecurityNative.GssApiException exception) + { + switch (exception.MajorStatus) + { + case Interop.NetSecurityNative.Status.GSS_S_NO_CRED: + return NegotiateAuthenticationStatusCode.UnknownCredentials; + case Interop.NetSecurityNative.Status.GSS_S_BAD_BINDINGS: + return NegotiateAuthenticationStatusCode.BadBinding; + case Interop.NetSecurityNative.Status.GSS_S_CREDENTIALS_EXPIRED: + return NegotiateAuthenticationStatusCode.CredentialsExpired; + case Interop.NetSecurityNative.Status.GSS_S_DEFECTIVE_TOKEN: + return NegotiateAuthenticationStatusCode.InvalidToken; + case Interop.NetSecurityNative.Status.GSS_S_DEFECTIVE_CREDENTIAL: + return NegotiateAuthenticationStatusCode.InvalidCredentials; + case Interop.NetSecurityNative.Status.GSS_S_BAD_SIG: + return NegotiateAuthenticationStatusCode.MessageAltered; + case Interop.NetSecurityNative.Status.GSS_S_BAD_MECH: + case Interop.NetSecurityNative.Status.GSS_S_UNAVAILABLE: + return NegotiateAuthenticationStatusCode.Unsupported; + case Interop.NetSecurityNative.Status.GSS_S_NO_CONTEXT: + default: + return NegotiateAuthenticationStatusCode.GenericFailure; + } + } + } + } +} diff --git a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unsupported.cs b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unsupported.cs new file mode 100644 index 0000000000000000000000000000000000000000..7e6498832f738c2dcbf29e7373f14b124769ebea --- /dev/null +++ b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Unsupported.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Diagnostics; +using System.Security.Principal; +using System.Net.Security; + +namespace System.Net +{ + internal abstract partial class NegotiateAuthenticationPal + { + internal sealed class UnsupportedNegotiateAuthenticationPal : NegotiateAuthenticationPal + { + private string _package; + private string? _targetName; + + public override bool IsAuthenticated => false; + public override bool IsSigned => false; + public override bool IsEncrypted => false; + public override bool IsMutuallyAuthenticated => false; + public override string Package => _package; + public override string? TargetName => _targetName; + public override IIdentity RemoteIdentity => throw new InvalidOperationException(); + public override System.Security.Principal.TokenImpersonationLevel ImpersonationLevel => System.Security.Principal.TokenImpersonationLevel.Impersonation; + + public UnsupportedNegotiateAuthenticationPal(NegotiateAuthenticationClientOptions clientOptions) + { + _package = clientOptions.Package; + _targetName = clientOptions.TargetName; + } + + public UnsupportedNegotiateAuthenticationPal(NegotiateAuthenticationServerOptions serverOptions) + { + _package = serverOptions.Package; + } + + public override void Dispose() + { + } + + public override byte[]? GetOutgoingBlob(ReadOnlySpan incomingBlob, out NegotiateAuthenticationStatusCode statusCode) + { + statusCode = NegotiateAuthenticationStatusCode.Unsupported; + return null; + } + + public override NegotiateAuthenticationStatusCode Wrap(ReadOnlySpan input, IBufferWriter outputWriter, bool requestEncryption, out bool isEncrypted) => throw new InvalidOperationException(); + public override NegotiateAuthenticationStatusCode Unwrap(ReadOnlySpan input, IBufferWriter outputWriter, out bool wasEncrypted) => throw new InvalidOperationException(); + public override NegotiateAuthenticationStatusCode UnwrapInPlace(Span input, out int unwrappedOffset, out int unwrappedLength, out bool wasEncrypted) => throw new InvalidOperationException(); + public override void GetMIC(ReadOnlySpan message, IBufferWriter signature) => throw new InvalidOperationException(); + public override bool VerifyMIC(ReadOnlySpan message, ReadOnlySpan signature) => throw new InvalidOperationException(); + } + } +} diff --git a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Windows.cs b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Windows.cs new file mode 100644 index 0000000000000000000000000000000000000000..3dcb03bfd08f74446026a6262afdce8b2df3d3d2 --- /dev/null +++ b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.Windows.cs @@ -0,0 +1,795 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Buffers.Binary; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Principal; +using System.Security.Authentication.ExtendedProtection; +using System.Net.Security; + +namespace System.Net +{ + internal partial class NegotiateAuthenticationPal + { + public static NegotiateAuthenticationPal Create(NegotiateAuthenticationClientOptions clientOptions) + { + try + { + return new WindowsNegotiateAuthenticationPal(clientOptions); + } + catch (NotSupportedException) + { + return new UnsupportedNegotiateAuthenticationPal(clientOptions); + } + } + + public static NegotiateAuthenticationPal Create(NegotiateAuthenticationServerOptions serverOptions) + { + try + { + return new WindowsNegotiateAuthenticationPal(serverOptions); + } + catch (NotSupportedException) + { + return new UnsupportedNegotiateAuthenticationPal(serverOptions); + } + } + + internal sealed class WindowsNegotiateAuthenticationPal : NegotiateAuthenticationPal + { + private bool _isServer; + private bool _isAuthenticated; + private int _tokenSize; + private byte[]? _tokenBuffer; + private SafeFreeCredentials? _credentialsHandle; + private SafeDeleteContext? _securityContext; + private Interop.SspiCli.ContextFlags _requestedContextFlags; + private Interop.SspiCli.ContextFlags _contextFlags; + private string _package; + private string? _protocolName; + private string? _spn; + private ChannelBinding? _channelBinding; + + public override bool IsAuthenticated => _isAuthenticated; + + public override bool IsSigned => (_contextFlags & (_isServer ? Interop.SspiCli.ContextFlags.AcceptIntegrity : Interop.SspiCli.ContextFlags.InitIntegrity)) != 0; + + public override bool IsEncrypted => (_contextFlags & Interop.SspiCli.ContextFlags.Confidentiality) != 0; + + public override bool IsMutuallyAuthenticated => (_contextFlags & Interop.SspiCli.ContextFlags.MutualAuth) != 0; + + public override string Package + { + get + { + // Note: May return string.Empty if the auth is not done yet or failed. + if (_protocolName == null) + { + string? negotiationAuthenticationPackage = null; + + if (_securityContext is not null) + { + SecPkgContext_NegotiationInfoW ctx = default; + bool success = SSPIWrapper.QueryBlittableContextAttributes(GlobalSSPI.SSPIAuth, _securityContext, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_NEGOTIATION_INFO, typeof(SafeFreeContextBuffer), out SafeHandle? sspiHandle, ref ctx); + using (sspiHandle) + { + negotiationAuthenticationPackage = success ? NegotiationInfoClass.GetAuthenticationPackageName(sspiHandle!, (int)ctx.NegotiationState) : null; + } + if (_isAuthenticated) + { + _protocolName = negotiationAuthenticationPackage; + } + } + + return negotiationAuthenticationPackage ?? string.Empty; + } + + return _protocolName; + } + } + + public override string? TargetName + { + get + { + if (_isServer && _spn == null) + { + Debug.Assert(_securityContext is not null && _isAuthenticated, "Trying to get the client SPN before handshaking is done!"); + _spn = SSPIWrapper.QueryStringContextAttributes(GlobalSSPI.SSPIAuth, _securityContext, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_CLIENT_SPECIFIED_TARGET); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"The client specified SPN is [{_spn}]"); + } + return _spn; + } + } + + public override IIdentity RemoteIdentity + { + get + { + IIdentity? result; + string? name = _isServer ? null : TargetName; + string protocol = Package; + + Debug.Assert(_securityContext is not null); + + if (_isServer) + { + SecurityContextTokenHandle? token = null; + try + { + name = SSPIWrapper.QueryStringContextAttributes(GlobalSSPI.SSPIAuth, _securityContext, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_NAMES); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"NTAuthentication: The context is associated with [{name}]"); + + // This will return a client token when conducted authentication on server side. + // This token can be used for impersonation. We use it to create a WindowsIdentity and hand it out to the server app. + Interop.SECURITY_STATUS winStatus = (Interop.SECURITY_STATUS)SSPIWrapper.QuerySecurityContextToken( + GlobalSSPI.SSPIAuth, + _securityContext, + out token); + if (winStatus != Interop.SECURITY_STATUS.OK) + { + throw new Win32Exception((int)winStatus); + } + + // The following call was also specifying WindowsAccountType.Normal, true. + // WindowsIdentity.IsAuthenticated is no longer supported in .NET Core + result = new WindowsIdentity(token.DangerousGetHandle(), protocol); + return result; + } + catch (SecurityException) + { + // Ignore and construct generic Identity if failed due to security problem. + } + finally + { + token?.Dispose(); + } + } + + // On the client we don't have access to the remote side identity. + result = new GenericIdentity(name ?? string.Empty, protocol); + return result; + } + } + + public override System.Security.Principal.TokenImpersonationLevel ImpersonationLevel + { + get + { + return + (_contextFlags & Interop.SspiCli.ContextFlags.Delegate) != 0 && Package != NegotiationInfoClass.NTLM ? TokenImpersonationLevel.Delegation : + (_contextFlags & (_isServer ? Interop.SspiCli.ContextFlags.AcceptIdentify : Interop.SspiCli.ContextFlags.InitIdentify)) != 0 ? TokenImpersonationLevel.Identification : + TokenImpersonationLevel.Impersonation; + } + } + + public WindowsNegotiateAuthenticationPal(NegotiateAuthenticationClientOptions clientOptions) + { + Interop.SspiCli.ContextFlags contextFlags = Interop.SspiCli.ContextFlags.Connection; + + contextFlags |= clientOptions.RequiredProtectionLevel switch + { + ProtectionLevel.Sign => Interop.SspiCli.ContextFlags.InitIntegrity, + ProtectionLevel.EncryptAndSign => Interop.SspiCli.ContextFlags.InitIntegrity | Interop.SspiCli.ContextFlags.Confidentiality, + _ => 0 + }; + + contextFlags |= clientOptions.RequireMutualAuthentication ? Interop.SspiCli.ContextFlags.MutualAuth : 0; + + contextFlags |= clientOptions.AllowedImpersonationLevel switch + { + TokenImpersonationLevel.Identification => Interop.SspiCli.ContextFlags.InitIdentify, + TokenImpersonationLevel.Delegation => Interop.SspiCli.ContextFlags.Delegate, + _ => 0 + }; + + _isServer = false; + _tokenSize = SSPIWrapper.GetVerifyPackageInfo(GlobalSSPI.SSPIAuth, clientOptions.Package, true)!.MaxToken; + _spn = clientOptions.TargetName; + _securityContext = null; + _requestedContextFlags = contextFlags; + _package = clientOptions.Package; + _channelBinding = clientOptions.Binding; + + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Peer SPN-> '{_spn}'"); + + // + // Check if we're using DefaultCredentials. + // + + Debug.Assert(CredentialCache.DefaultCredentials == CredentialCache.DefaultNetworkCredentials); + if (clientOptions.Credential == CredentialCache.DefaultCredentials) + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "using DefaultCredentials"); + _credentialsHandle = AcquireDefaultCredential(_package, _isServer); + } + else + { + _credentialsHandle = AcquireCredentialsHandle(_package, _isServer, clientOptions.Credential); + } + } + + public WindowsNegotiateAuthenticationPal(NegotiateAuthenticationServerOptions serverOptions) + { + Interop.SspiCli.ContextFlags contextFlags = serverOptions.RequiredProtectionLevel switch + { + ProtectionLevel.Sign => Interop.SspiCli.ContextFlags.AcceptIntegrity, + ProtectionLevel.EncryptAndSign => Interop.SspiCli.ContextFlags.AcceptIntegrity | Interop.SspiCli.ContextFlags.Confidentiality, + _ => 0 + } | Interop.SspiCli.ContextFlags.Connection; + + if (serverOptions.Policy is not null) + { + if (serverOptions.Policy.PolicyEnforcement == PolicyEnforcement.WhenSupported) + { + contextFlags |= Interop.SspiCli.ContextFlags.AllowMissingBindings; + } + + if (serverOptions.Policy.PolicyEnforcement != PolicyEnforcement.Never && + serverOptions.Policy.ProtectionScenario == ProtectionScenario.TrustedProxy) + { + contextFlags |= Interop.SspiCli.ContextFlags.ProxyBindings; + } + } + + _isServer = true; + _tokenSize = SSPIWrapper.GetVerifyPackageInfo(GlobalSSPI.SSPIAuth, serverOptions.Package, true)!.MaxToken; + _securityContext = null; + _requestedContextFlags = contextFlags; + _package = serverOptions.Package; + _channelBinding = serverOptions.Binding; + + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Peer SPN-> '{_spn}'"); + + // + // Check if we're using DefaultCredentials. + // + + Debug.Assert(CredentialCache.DefaultCredentials == CredentialCache.DefaultNetworkCredentials); + if (serverOptions.Credential == CredentialCache.DefaultCredentials) + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "using DefaultCredentials"); + _credentialsHandle = AcquireDefaultCredential(_package, _isServer); + } + else + { + _credentialsHandle = AcquireCredentialsHandle(_package, _isServer, serverOptions.Credential); + } + } + + public override void Dispose() + { + _securityContext?.Dispose(); + } + + public override byte[]? GetOutgoingBlob(ReadOnlySpan incomingBlob, out NegotiateAuthenticationStatusCode statusCode) + { + _tokenBuffer ??= _tokenSize == 0 ? Array.Empty() : new byte[_tokenSize]; + + bool firstTime = _securityContext == null; + int resultBlobLength; + SecurityStatusPal platformStatusCode; + try + { + if (!_isServer) + { + // client session + platformStatusCode = InitializeSecurityContext( + ref _credentialsHandle!, + ref _securityContext, + _spn, + _requestedContextFlags, + incomingBlob, + _channelBinding, + ref _tokenBuffer, + out resultBlobLength, + ref _contextFlags); + + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"SSPIWrapper.InitializeSecurityContext() returns statusCode:0x{((int)platformStatusCode.ErrorCode):x8} ({platformStatusCode})"); + + if (platformStatusCode.ErrorCode == SecurityStatusPalErrorCode.CompleteNeeded) + { + platformStatusCode = CompleteAuthToken(ref _securityContext, _tokenBuffer.AsSpan(0, resultBlobLength)); + + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"SSPIWrapper.CompleteAuthToken() returns statusCode:0x{((int)platformStatusCode.ErrorCode):x8} ({platformStatusCode})"); + + resultBlobLength = 0; + } + } + else + { + // Server session. + platformStatusCode = AcceptSecurityContext( + _credentialsHandle, + ref _securityContext, + _requestedContextFlags, + incomingBlob, + _channelBinding, + ref _tokenBuffer, + out resultBlobLength, + ref _contextFlags); + + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"SSPIWrapper.AcceptSecurityContext() returns statusCode:0x{((int)platformStatusCode.ErrorCode):x8} ({platformStatusCode})"); + } + } + finally + { + // + // Assuming the ISC or ASC has referenced the credential on the first successful call, + // we want to decrement the effective ref count by "disposing" it. + // The real dispose will happen when the security context is closed. + // Note if the first call was not successful the handle is physically destroyed here. + // + if (firstTime) + { + _credentialsHandle?.Dispose(); + } + } + + // Map error codes + // TODO: Remove double mapping from Win32 codes + statusCode = platformStatusCode.ErrorCode switch + { + SecurityStatusPalErrorCode.OK => NegotiateAuthenticationStatusCode.Completed, + SecurityStatusPalErrorCode.ContinueNeeded => NegotiateAuthenticationStatusCode.ContinueNeeded, + + // These code should never be returned and they should be handled internally + SecurityStatusPalErrorCode.CompleteNeeded => NegotiateAuthenticationStatusCode.Completed, + SecurityStatusPalErrorCode.CompAndContinue => NegotiateAuthenticationStatusCode.ContinueNeeded, + + SecurityStatusPalErrorCode.ContextExpired => NegotiateAuthenticationStatusCode.ContextExpired, + SecurityStatusPalErrorCode.Unsupported => NegotiateAuthenticationStatusCode.Unsupported, + SecurityStatusPalErrorCode.PackageNotFound => NegotiateAuthenticationStatusCode.Unsupported, + SecurityStatusPalErrorCode.CannotInstall => NegotiateAuthenticationStatusCode.Unsupported, + SecurityStatusPalErrorCode.InvalidToken => NegotiateAuthenticationStatusCode.InvalidToken, + SecurityStatusPalErrorCode.QopNotSupported => NegotiateAuthenticationStatusCode.QopNotSupported, + SecurityStatusPalErrorCode.NoImpersonation => NegotiateAuthenticationStatusCode.UnknownCredentials, + SecurityStatusPalErrorCode.LogonDenied => NegotiateAuthenticationStatusCode.UnknownCredentials, + SecurityStatusPalErrorCode.UnknownCredentials => NegotiateAuthenticationStatusCode.UnknownCredentials, + SecurityStatusPalErrorCode.NoCredentials => NegotiateAuthenticationStatusCode.UnknownCredentials, + SecurityStatusPalErrorCode.MessageAltered => NegotiateAuthenticationStatusCode.MessageAltered, + SecurityStatusPalErrorCode.OutOfSequence => NegotiateAuthenticationStatusCode.OutOfSequence, + SecurityStatusPalErrorCode.NoAuthenticatingAuthority => NegotiateAuthenticationStatusCode.InvalidCredentials, + SecurityStatusPalErrorCode.IncompleteCredentials => NegotiateAuthenticationStatusCode.InvalidCredentials, + SecurityStatusPalErrorCode.IllegalMessage => NegotiateAuthenticationStatusCode.InvalidToken, + SecurityStatusPalErrorCode.CertExpired => NegotiateAuthenticationStatusCode.CredentialsExpired, + SecurityStatusPalErrorCode.SecurityQosFailed => NegotiateAuthenticationStatusCode.QopNotSupported, + SecurityStatusPalErrorCode.UnsupportedPreauth => NegotiateAuthenticationStatusCode.InvalidToken, + SecurityStatusPalErrorCode.BadBinding => NegotiateAuthenticationStatusCode.BadBinding, + SecurityStatusPalErrorCode.UntrustedRoot => NegotiateAuthenticationStatusCode.UnknownCredentials, + SecurityStatusPalErrorCode.SmartcardLogonRequired => NegotiateAuthenticationStatusCode.UnknownCredentials, + SecurityStatusPalErrorCode.WrongPrincipal => NegotiateAuthenticationStatusCode.UnknownCredentials, + SecurityStatusPalErrorCode.CannotPack => NegotiateAuthenticationStatusCode.InvalidToken, + SecurityStatusPalErrorCode.TimeSkew => NegotiateAuthenticationStatusCode.InvalidToken, + SecurityStatusPalErrorCode.AlgorithmMismatch => NegotiateAuthenticationStatusCode.InvalidToken, + SecurityStatusPalErrorCode.CertUnknown => NegotiateAuthenticationStatusCode.UnknownCredentials, + + // Processing partial inputs is not supported, so this is result of incorrect input + SecurityStatusPalErrorCode.IncompleteMessage => NegotiateAuthenticationStatusCode.InvalidToken, + + _ => NegotiateAuthenticationStatusCode.GenericFailure, + }; + + if (((int)platformStatusCode.ErrorCode >= (int)SecurityStatusPalErrorCode.OutOfMemory)) + { + //CloseContext(); + _securityContext?.Dispose(); + _isAuthenticated = true; + _tokenBuffer = null; + return null; + } + else if (firstTime && _credentialsHandle != null) + { + // Cache until it is pushed out by newly incoming handles. + SSPIHandleCache.CacheCredential(_credentialsHandle); + } + + byte[]? result = + resultBlobLength == 0 || _tokenBuffer == null ? null : + _tokenBuffer.Length == resultBlobLength ? _tokenBuffer : + _tokenBuffer[0..resultBlobLength]; + + // The return value will tell us correctly if the handshake is over or not + if (platformStatusCode.ErrorCode == SecurityStatusPalErrorCode.OK + || (_isServer && platformStatusCode.ErrorCode == SecurityStatusPalErrorCode.CompleteNeeded)) + { + // Success. + _isAuthenticated = true; + _tokenBuffer = null; + } + else + { + // We need to continue. + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"need continue statusCode:0x{((int)platformStatusCode.ErrorCode):x8} ({platformStatusCode}) _securityContext:{_securityContext}"); + } + + return result; + } + + public override unsafe NegotiateAuthenticationStatusCode Wrap(ReadOnlySpan input, IBufferWriter outputWriter, bool requestEncryption, out bool isEncrypted) + { + Debug.Assert(_securityContext is not null); + + SecPkgContext_Sizes sizes = default; + bool success = SSPIWrapper.QueryBlittableContextAttributes(GlobalSSPI.SSPIAuth, _securityContext, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_SIZES, ref sizes); + Debug.Assert(success); + + // alloc new output buffer if not supplied or too small + int resultSize = input.Length + sizes.cbMaxSignature; + Span outputBuffer = outputWriter.GetSpan(resultSize); + + // make a copy of user data for in-place encryption + input.CopyTo(outputBuffer.Slice(sizes.cbMaxSignature, input.Length)); + + isEncrypted = requestEncryption; + + fixed (byte* outputPtr = outputBuffer) + { + // Prepare buffers TOKEN(signature), DATA and Padding. + Interop.SspiCli.SecBuffer* unmanagedBuffer = stackalloc Interop.SspiCli.SecBuffer[2]; + Interop.SspiCli.SecBuffer* tokenBuffer = &unmanagedBuffer[0]; + Interop.SspiCli.SecBuffer* dataBuffer = &unmanagedBuffer[1]; + tokenBuffer->BufferType = SecurityBufferType.SECBUFFER_TOKEN; + tokenBuffer->pvBuffer = (IntPtr)(outputPtr); + tokenBuffer->cbBuffer = sizes.cbMaxSignature; + dataBuffer->BufferType = SecurityBufferType.SECBUFFER_DATA; + dataBuffer->pvBuffer = (IntPtr)(outputPtr + sizes.cbMaxSignature); + dataBuffer->cbBuffer = input.Length; + + Interop.SspiCli.SecBufferDesc sdcInOut = new Interop.SspiCli.SecBufferDesc(2) + { + pBuffers = unmanagedBuffer + }; + + uint qop = requestEncryption ? 0 : Interop.SspiCli.SECQOP_WRAP_NO_ENCRYPT; + int errorCode = GlobalSSPI.SSPIAuth.EncryptMessage(_securityContext, ref sdcInOut, qop); + + if (errorCode != 0) + { + return errorCode switch + { + (int)Interop.SECURITY_STATUS.ContextExpired => NegotiateAuthenticationStatusCode.ContextExpired, + (int)Interop.SECURITY_STATUS.QopNotSupported => NegotiateAuthenticationStatusCode.QopNotSupported, + _ => NegotiateAuthenticationStatusCode.GenericFailure, + }; + } + + outputWriter.Advance(tokenBuffer->cbBuffer + dataBuffer->cbBuffer); + return NegotiateAuthenticationStatusCode.Completed; + } + } + + public override NegotiateAuthenticationStatusCode Unwrap(ReadOnlySpan input, IBufferWriter outputWriter, out bool wasEncrypted) + { + Span outputBuffer = outputWriter.GetSpan(input.Length).Slice(0, input.Length); + NegotiateAuthenticationStatusCode statusCode; + + input.CopyTo(outputBuffer); + statusCode = UnwrapInPlace(outputBuffer, out int unwrappedOffset, out int unwrappedLength, out wasEncrypted); + + if (statusCode == NegotiateAuthenticationStatusCode.Completed) + { + if (unwrappedOffset > 0) + { + outputBuffer.Slice(unwrappedOffset, unwrappedLength).CopyTo(outputBuffer); + } + outputWriter.Advance(unwrappedLength); + } + + return statusCode; + } + + public override unsafe NegotiateAuthenticationStatusCode UnwrapInPlace(Span input, out int unwrappedOffset, out int unwrappedLength, out bool wasEncrypted) + { + Debug.Assert(_securityContext is not null); + + fixed (byte* inputPtr = input) + { + Interop.SspiCli.SecBuffer* unmanagedBuffer = stackalloc Interop.SspiCli.SecBuffer[2]; + Interop.SspiCli.SecBuffer* streamBuffer = &unmanagedBuffer[0]; + Interop.SspiCli.SecBuffer* dataBuffer = &unmanagedBuffer[1]; + streamBuffer->BufferType = SecurityBufferType.SECBUFFER_STREAM; + streamBuffer->pvBuffer = (IntPtr)inputPtr; + streamBuffer->cbBuffer = input.Length; + dataBuffer->BufferType = SecurityBufferType.SECBUFFER_DATA; + dataBuffer->pvBuffer = IntPtr.Zero; + dataBuffer->cbBuffer = 0; + + Interop.SspiCli.SecBufferDesc sdcInOut = new Interop.SspiCli.SecBufferDesc(2) + { + pBuffers = unmanagedBuffer + }; + + uint qop; + int errorCode = GlobalSSPI.SSPIAuth.DecryptMessage(_securityContext, ref sdcInOut, out qop); + if (errorCode != 0) + { + unwrappedOffset = 0; + unwrappedLength = 0; + wasEncrypted = false; + return errorCode switch + { + (int)Interop.SECURITY_STATUS.MessageAltered => NegotiateAuthenticationStatusCode.MessageAltered, + _ => NegotiateAuthenticationStatusCode.InvalidToken + }; + } + + if (dataBuffer->BufferType != SecurityBufferType.SECBUFFER_DATA) + { + throw new InternalException(dataBuffer->BufferType); + } + + wasEncrypted = qop != Interop.SspiCli.SECQOP_WRAP_NO_ENCRYPT; + + Debug.Assert((nint)dataBuffer->pvBuffer >= (nint)inputPtr); + Debug.Assert((nint)dataBuffer->pvBuffer + dataBuffer->cbBuffer <= (nint)inputPtr + input.Length); + unwrappedOffset = (int)((byte*)dataBuffer->pvBuffer - inputPtr); + unwrappedLength = dataBuffer->cbBuffer; + return NegotiateAuthenticationStatusCode.Completed; + } + } + + public override unsafe void GetMIC(ReadOnlySpan message, IBufferWriter signature) + { + bool refAdded = false; + + Debug.Assert(_securityContext is not null); + + try + { + _securityContext.DangerousAddRef(ref refAdded); + + SecPkgContext_Sizes sizes = default; + bool success = SSPIWrapper.QueryBlittableContextAttributes(GlobalSSPI.SSPIAuth, _securityContext, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_SIZES, ref sizes); + Debug.Assert(success); + + Span signatureBuffer = signature.GetSpan(sizes.cbSecurityTrailer); + + fixed (byte* messagePtr = message) + fixed (byte* signaturePtr = signatureBuffer) + { + // Prepare buffers TOKEN(signature), DATA. + Interop.SspiCli.SecBuffer* unmanagedBuffer = stackalloc Interop.SspiCli.SecBuffer[2]; + Interop.SspiCli.SecBuffer* tokenBuffer = &unmanagedBuffer[0]; + Interop.SspiCli.SecBuffer* dataBuffer = &unmanagedBuffer[1]; + tokenBuffer->BufferType = SecurityBufferType.SECBUFFER_TOKEN; + tokenBuffer->pvBuffer = (IntPtr)signaturePtr; + tokenBuffer->cbBuffer = sizes.cbSecurityTrailer; + dataBuffer->BufferType = SecurityBufferType.SECBUFFER_DATA; + dataBuffer->pvBuffer = (IntPtr)messagePtr; + dataBuffer->cbBuffer = message.Length; + + Interop.SspiCli.SecBufferDesc sdcInOut = new Interop.SspiCli.SecBufferDesc(2) + { + pBuffers = unmanagedBuffer + }; + + uint qop = IsEncrypted ? 0 : Interop.SspiCli.SECQOP_WRAP_NO_ENCRYPT; + int errorCode = Interop.SspiCli.MakeSignature(ref _securityContext._handle, qop, ref sdcInOut, 0); + + if (errorCode != 0) + { + Exception e = new Win32Exception(errorCode); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, e); + throw new Win32Exception(errorCode); + } + + signature.Advance(signatureBuffer.Length); + } + } + finally + { + if (refAdded) + { + _securityContext.DangerousRelease(); + } + } + } + + public override unsafe bool VerifyMIC(ReadOnlySpan message, ReadOnlySpan signature) + { + bool refAdded = false; + + Debug.Assert(_securityContext is not null); + + try + { + _securityContext.DangerousAddRef(ref refAdded); + + fixed (byte* messagePtr = message) + fixed (byte* signaturePtr = signature) + { + Interop.SspiCli.SecBuffer* unmanagedBuffer = stackalloc Interop.SspiCli.SecBuffer[2]; + Interop.SspiCli.SecBuffer* tokenBuffer = &unmanagedBuffer[0]; + Interop.SspiCli.SecBuffer* dataBuffer = &unmanagedBuffer[1]; + tokenBuffer->BufferType = SecurityBufferType.SECBUFFER_TOKEN; + tokenBuffer->pvBuffer = (IntPtr)signaturePtr; + tokenBuffer->cbBuffer = signature.Length; + dataBuffer->BufferType = SecurityBufferType.SECBUFFER_DATA; + dataBuffer->pvBuffer = (IntPtr)messagePtr; + dataBuffer->cbBuffer = message.Length; + + Interop.SspiCli.SecBufferDesc sdcIn = new Interop.SspiCli.SecBufferDesc(2) + { + pBuffers = unmanagedBuffer + }; + + uint qop; + int errorCode = Interop.SspiCli.VerifySignature(ref _securityContext._handle, in sdcIn, 0, &qop); + + if (errorCode != 0) + { + Exception e = new Win32Exception(errorCode); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, e); + throw new Win32Exception(errorCode); + } + + if (IsEncrypted && qop == Interop.SspiCli.SECQOP_WRAP_NO_ENCRYPT) + { + Debug.Fail($"Expected qop = 0, returned value = {qop}"); + throw new InvalidOperationException(SR.net_auth_message_not_encrypted); + } + + return true; + } + } + finally + { + if (refAdded) + { + _securityContext.DangerousRelease(); + } + } + } + + private static SafeFreeCredentials AcquireDefaultCredential(string package, bool isServer) + { + return SSPIWrapper.AcquireDefaultCredential( + GlobalSSPI.SSPIAuth, + package, + (isServer ? Interop.SspiCli.CredentialUse.SECPKG_CRED_INBOUND : Interop.SspiCli.CredentialUse.SECPKG_CRED_OUTBOUND)); + } + + private static SafeFreeCredentials AcquireCredentialsHandle(string package, bool isServer, NetworkCredential credential) + { + SafeSspiAuthDataHandle? authData = null; + try + { + Interop.SECURITY_STATUS result = Interop.SspiCli.SspiEncodeStringsAsAuthIdentity( + credential.UserName, credential.Domain, + credential.Password, out authData); + + if (result != Interop.SECURITY_STATUS.OK) + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, SR.Format(SR.net_log_operation_failed_with_error, nameof(Interop.SspiCli.SspiEncodeStringsAsAuthIdentity), $"0x{(int)result:X}")); + throw new Win32Exception((int)result); + } + + return SSPIWrapper.AcquireCredentialsHandle(GlobalSSPI.SSPIAuth, + package, (isServer ? Interop.SspiCli.CredentialUse.SECPKG_CRED_INBOUND : Interop.SspiCli.CredentialUse.SECPKG_CRED_OUTBOUND), ref authData); + } + finally + { + authData?.Dispose(); + } + } + + private static SecurityStatusPal InitializeSecurityContext( + ref SafeFreeCredentials? credentialsHandle, + ref SafeDeleteContext? securityContext, + string? spn, + Interop.SspiCli.ContextFlags requestedContextFlags, + ReadOnlySpan incomingBlob, + ChannelBinding? channelBinding, + ref byte[]? resultBlob, + out int resultBlobLength, + ref Interop.SspiCli.ContextFlags contextFlags) + { + + InputSecurityBuffers inputBuffers = default; + if (!incomingBlob.IsEmpty) + { + inputBuffers.SetNextBuffer(new InputSecurityBuffer(incomingBlob, SecurityBufferType.SECBUFFER_TOKEN)); + } + + if (channelBinding != null) + { + inputBuffers.SetNextBuffer(new InputSecurityBuffer(channelBinding)); + } + + var outSecurityBuffer = new SecurityBuffer(resultBlob, SecurityBufferType.SECBUFFER_TOKEN); + + contextFlags = Interop.SspiCli.ContextFlags.Zero; + // There is only one SafeDeleteContext type on Windows which is SafeDeleteSslContext so this cast is safe. + SafeDeleteSslContext? sslContext = (SafeDeleteSslContext?)securityContext; + Interop.SECURITY_STATUS winStatus = (Interop.SECURITY_STATUS)SSPIWrapper.InitializeSecurityContext( + GlobalSSPI.SSPIAuth, + ref credentialsHandle, + ref sslContext, + spn, + requestedContextFlags, + Interop.SspiCli.Endianness.SECURITY_NETWORK_DREP, + inputBuffers, + ref outSecurityBuffer, + ref contextFlags); + securityContext = sslContext; + Debug.Assert(outSecurityBuffer.offset == 0); + resultBlob = outSecurityBuffer.token; + resultBlobLength = outSecurityBuffer.size; + return SecurityStatusAdapterPal.GetSecurityStatusPalFromInterop(winStatus); + } + + private static SecurityStatusPal CompleteAuthToken( + ref SafeDeleteContext? securityContext, + ReadOnlySpan incomingBlob) + { + // There is only one SafeDeleteContext type on Windows which is SafeDeleteSslContext so this cast is safe. + SafeDeleteSslContext? sslContext = (SafeDeleteSslContext?)securityContext; + var inSecurityBuffer = new InputSecurityBuffer(incomingBlob, SecurityBufferType.SECBUFFER_TOKEN); + Interop.SECURITY_STATUS winStatus = (Interop.SECURITY_STATUS)SSPIWrapper.CompleteAuthToken( + GlobalSSPI.SSPIAuth, + ref sslContext, + in inSecurityBuffer); + securityContext = sslContext; + return SecurityStatusAdapterPal.GetSecurityStatusPalFromInterop(winStatus); + } + + private static SecurityStatusPal AcceptSecurityContext( + SafeFreeCredentials? credentialsHandle, + ref SafeDeleteContext? securityContext, + Interop.SspiCli.ContextFlags requestedContextFlags, + ReadOnlySpan incomingBlob, + ChannelBinding? channelBinding, + ref byte[]? resultBlob, + out int resultBlobLength, + ref Interop.SspiCli.ContextFlags contextFlags) + { + InputSecurityBuffers inputBuffers = default; + if (!incomingBlob.IsEmpty) + { + inputBuffers.SetNextBuffer(new InputSecurityBuffer(incomingBlob, SecurityBufferType.SECBUFFER_TOKEN)); + } + + if (channelBinding != null) + { + inputBuffers.SetNextBuffer(new InputSecurityBuffer(channelBinding)); + } + + var outSecurityBuffer = new SecurityBuffer(resultBlob, SecurityBufferType.SECBUFFER_TOKEN); + + contextFlags = Interop.SspiCli.ContextFlags.Zero; + // There is only one SafeDeleteContext type on Windows which is SafeDeleteSslContext so this cast is safe. + SafeDeleteSslContext? sslContext = (SafeDeleteSslContext?)securityContext; + Interop.SECURITY_STATUS winStatus = (Interop.SECURITY_STATUS)SSPIWrapper.AcceptSecurityContext( + GlobalSSPI.SSPIAuth, + credentialsHandle, + ref sslContext, + requestedContextFlags, + Interop.SspiCli.Endianness.SECURITY_NETWORK_DREP, + inputBuffers, + ref outSecurityBuffer, + ref contextFlags); + + // SSPI Workaround + // If a client sends up a blob on the initial request, Negotiate returns SEC_E_INVALID_HANDLE + // when it should return SEC_E_INVALID_TOKEN. + if (winStatus == Interop.SECURITY_STATUS.InvalidHandle && securityContext == null && !incomingBlob.IsEmpty) + { + winStatus = Interop.SECURITY_STATUS.InvalidToken; + } + + Debug.Assert(outSecurityBuffer.offset == 0); + resultBlob = outSecurityBuffer.token; + resultBlobLength = outSecurityBuffer.size; + securityContext = sslContext; + return SecurityStatusAdapterPal.GetSecurityStatusPalFromInterop(winStatus); + } + } + } +} diff --git a/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.cs b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.cs new file mode 100644 index 0000000000000000000000000000000000000000..78eed0d81f84c21123c0f1cea1208a01e17179d5 --- /dev/null +++ b/src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Security.Principal; +using System.Net.Security; + +namespace System.Net +{ + internal abstract partial class NegotiateAuthenticationPal : IDisposable + { + public abstract bool IsAuthenticated { get; } + public abstract bool IsSigned { get; } + public abstract bool IsEncrypted { get; } + public abstract bool IsMutuallyAuthenticated { get; } + public abstract string Package { get; } + public abstract string? TargetName { get; } + public abstract IIdentity RemoteIdentity { get; } + public abstract System.Security.Principal.TokenImpersonationLevel ImpersonationLevel { get; } + public abstract void Dispose(); + public abstract byte[]? GetOutgoingBlob(ReadOnlySpan incomingBlob, out NegotiateAuthenticationStatusCode statusCode); + public abstract NegotiateAuthenticationStatusCode Wrap(ReadOnlySpan input, IBufferWriter outputWriter, bool requestEncryption, out bool isEncrypted); + public abstract NegotiateAuthenticationStatusCode Unwrap(ReadOnlySpan input, IBufferWriter outputWriter, out bool wasEncrypted); + public abstract NegotiateAuthenticationStatusCode UnwrapInPlace(Span input, out int unwrappedOffset, out int unwrappedLength, out bool wasEncrypted); + public abstract void GetMIC(ReadOnlySpan message, IBufferWriter signature); + public abstract bool VerifyMIC(ReadOnlySpan message, ReadOnlySpan signature); + } +} diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthentication.cs b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthentication.cs index d3c5d368be6db77802e73f74dc7fe6e2281180cd..6b990651e893962d08fde8463807d8a1dd1f6ff2 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthentication.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthentication.cs @@ -16,13 +16,14 @@ namespace System.Net.Security /// public sealed class NegotiateAuthentication : IDisposable { - private readonly NTAuthentication? _ntAuthentication; + private readonly NegotiateAuthenticationPal _pal; private readonly string _requestedPackage; private readonly bool _isServer; private readonly TokenImpersonationLevel _requiredImpersonationLevel; private readonly ProtectionLevel _requiredProtectionLevel; private readonly ExtendedProtectionPolicy? _extendedProtectionPolicy; private readonly bool _isSecureConnection; + private bool _isDisposed; private IIdentity? _remoteIdentity; /// @@ -34,47 +35,11 @@ public NegotiateAuthentication(NegotiateAuthenticationClientOptions clientOption { ArgumentNullException.ThrowIfNull(clientOptions); - ContextFlagsPal contextFlags = ContextFlagsPal.Connection; - - contextFlags |= clientOptions.RequiredProtectionLevel switch - { - ProtectionLevel.Sign => ContextFlagsPal.InitIntegrity, - ProtectionLevel.EncryptAndSign => ContextFlagsPal.InitIntegrity | ContextFlagsPal.Confidentiality, - _ => 0 - }; - - contextFlags |= clientOptions.RequireMutualAuthentication ? ContextFlagsPal.MutualAuth : 0; - - contextFlags |= clientOptions.AllowedImpersonationLevel switch - { - TokenImpersonationLevel.Identification => ContextFlagsPal.InitIdentify, - TokenImpersonationLevel.Delegation => ContextFlagsPal.Delegate, - _ => 0 - }; - _isServer = false; _requestedPackage = clientOptions.Package; _requiredImpersonationLevel = TokenImpersonationLevel.None; _requiredProtectionLevel = clientOptions.RequiredProtectionLevel; - try - { - _ntAuthentication = new NTAuthentication( - isServer: false, - clientOptions.Package, - clientOptions.Credential, - clientOptions.TargetName, - contextFlags, - clientOptions.Binding); - } - catch (PlatformNotSupportedException) // Managed implementation, Unix - { - } - catch (NotSupportedException) // Windows implementation - { - } - catch (Win32Exception) // Unix implementation in native layer - { - } + _pal = NegotiateAuthenticationPal.Create(clientOptions); } /// @@ -86,52 +51,13 @@ public NegotiateAuthentication(NegotiateAuthenticationServerOptions serverOption { ArgumentNullException.ThrowIfNull(serverOptions); - ContextFlagsPal contextFlags = serverOptions.RequiredProtectionLevel switch - { - ProtectionLevel.Sign => ContextFlagsPal.AcceptIntegrity, - ProtectionLevel.EncryptAndSign => ContextFlagsPal.AcceptIntegrity | ContextFlagsPal.Confidentiality, - _ => 0 - } | ContextFlagsPal.Connection; - - if (serverOptions.Policy is not null) - { - if (serverOptions.Policy.PolicyEnforcement == PolicyEnforcement.WhenSupported) - { - contextFlags |= ContextFlagsPal.AllowMissingBindings; - } - - if (serverOptions.Policy.PolicyEnforcement != PolicyEnforcement.Never && - serverOptions.Policy.ProtectionScenario == ProtectionScenario.TrustedProxy) - { - contextFlags |= ContextFlagsPal.ProxyBindings; - } - } - _isServer = true; _requestedPackage = serverOptions.Package; _requiredImpersonationLevel = serverOptions.RequiredImpersonationLevel; _requiredProtectionLevel = serverOptions.RequiredProtectionLevel; _extendedProtectionPolicy = serverOptions.Policy; _isSecureConnection = serverOptions.Binding != null; - try - { - _ntAuthentication = new NTAuthentication( - isServer: true, - serverOptions.Package, - serverOptions.Credential, - null, - contextFlags, - serverOptions.Binding); - } - catch (PlatformNotSupportedException) // Managed implementation, Unix - { - } - catch (NotSupportedException) // Windows implementation - { - } - catch (Win32Exception) // Unix implementation in native layer - { - } + _pal = NegotiateAuthenticationPal.Create(serverOptions); } /// @@ -140,10 +66,14 @@ public NegotiateAuthentication(NegotiateAuthenticationServerOptions serverOption /// public void Dispose() { - _ntAuthentication?.CloseContext(); - if (_remoteIdentity is IDisposable disposableRemoteIdentity) + if (!_isDisposed) { - disposableRemoteIdentity.Dispose(); + _isDisposed = true; + _pal?.Dispose(); + if (_remoteIdentity is IDisposable disposableRemoteIdentity) + { + disposableRemoteIdentity.Dispose(); + } } } @@ -151,7 +81,7 @@ public void Dispose() /// Indicates whether authentication was successfully completed and the session /// was established. /// - public bool IsAuthenticated => _ntAuthentication?.IsCompleted ?? false; + public bool IsAuthenticated => _isDisposed ? false : _pal.IsAuthenticated; /// /// Indicates the negotiated level of protection. @@ -171,17 +101,17 @@ public void Dispose() /// /// Indicates whether data signing was negotiated. /// - public bool IsSigned => _ntAuthentication?.IsIntegrityFlag ?? false; + public bool IsSigned => _isDisposed ? false : _pal.IsSigned; /// /// Indicates whether data encryption was negotiated. /// - public bool IsEncrypted => _ntAuthentication?.IsConfidentialityFlag ?? false; + public bool IsEncrypted => _isDisposed ? false : _pal.IsEncrypted; /// /// Indicates whether both server and client have been authenticated. /// - public bool IsMutuallyAuthenticated => _ntAuthentication?.IsMutualAuthFlag ?? false; + public bool IsMutuallyAuthenticated => _isDisposed ? false : _pal.IsMutuallyAuthenticated; /// /// Indicates whether the local side of the authentication is representing @@ -204,7 +134,7 @@ public void Dispose() /// property will be Kerberos, NTLM, or any other specific protocol that was /// negotiated between both sides of the authentication. /// - public string Package => _ntAuthentication?.ProtocolName ?? _requestedPackage; + public string Package => _pal.Package ?? _requestedPackage; /// /// Gets target name (service principal name) of the server. @@ -216,7 +146,7 @@ public void Dispose() /// For client-side of the authentication the property returns the target name /// specified in . /// - public string? TargetName => IsServer ? _ntAuthentication?.ClientSpecifiedSpn : _ntAuthentication?.Spn; + public string? TargetName => _pal.TargetName; /// /// Gets information about the identity of the remote party. @@ -233,7 +163,7 @@ public IIdentity RemoteIdentity IIdentity? identity = _remoteIdentity; if (identity is null) { - if (!IsAuthenticated || _ntAuthentication == null) + if (!IsAuthenticated || _isDisposed) { throw new InvalidOperationException(SR.net_auth_noauth); } @@ -241,7 +171,7 @@ public IIdentity RemoteIdentity if (IsServer) { Debug.Assert(!OperatingSystem.IsTvOS(), "Server authentication is not supported on tvOS"); - _remoteIdentity = identity = NegotiateStreamPal.GetIdentity(_ntAuthentication); + _remoteIdentity = identity = _pal.RemoteIdentity; } else { @@ -256,17 +186,7 @@ public IIdentity RemoteIdentity /// One of the values, indicating the negotiated /// level of impresonation. /// - public System.Security.Principal.TokenImpersonationLevel ImpersonationLevel - { - get - { - // We should suppress the delegate flag in NTLM case. - return - _ntAuthentication!.IsDelegationFlag && _ntAuthentication.ProtocolName != NegotiationInfoClass.NTLM ? TokenImpersonationLevel.Delegation : - _ntAuthentication.IsIdentifyFlag ? TokenImpersonationLevel.Identification : - TokenImpersonationLevel.Impersonation; - } - } + public System.Security.Principal.TokenImpersonationLevel ImpersonationLevel => _pal.ImpersonationLevel; /// /// Evaluates an authentication token sent by the other party and returns a token in response. @@ -287,57 +207,12 @@ public System.Security.Principal.TokenImpersonationLevel ImpersonationLevel /// public byte[]? GetOutgoingBlob(ReadOnlySpan incomingBlob, out NegotiateAuthenticationStatusCode statusCode) { - if (_ntAuthentication == null) + if (_isDisposed) { - // Unsupported protocol - statusCode = NegotiateAuthenticationStatusCode.Unsupported; - return null; + throw new InvalidOperationException(SR.net_auth_noauth); } - byte[]? blob = _ntAuthentication.GetOutgoingBlob(incomingBlob, false, out SecurityStatusPal securityStatus); - - // Map error codes - statusCode = securityStatus.ErrorCode switch - { - SecurityStatusPalErrorCode.OK => NegotiateAuthenticationStatusCode.Completed, - SecurityStatusPalErrorCode.ContinueNeeded => NegotiateAuthenticationStatusCode.ContinueNeeded, - - // These code should never be returned and they should be handled internally - SecurityStatusPalErrorCode.CompleteNeeded => NegotiateAuthenticationStatusCode.Completed, - SecurityStatusPalErrorCode.CompAndContinue => NegotiateAuthenticationStatusCode.ContinueNeeded, - - SecurityStatusPalErrorCode.ContextExpired => NegotiateAuthenticationStatusCode.ContextExpired, - SecurityStatusPalErrorCode.Unsupported => NegotiateAuthenticationStatusCode.Unsupported, - SecurityStatusPalErrorCode.PackageNotFound => NegotiateAuthenticationStatusCode.Unsupported, - SecurityStatusPalErrorCode.CannotInstall => NegotiateAuthenticationStatusCode.Unsupported, - SecurityStatusPalErrorCode.InvalidToken => NegotiateAuthenticationStatusCode.InvalidToken, - SecurityStatusPalErrorCode.QopNotSupported => NegotiateAuthenticationStatusCode.QopNotSupported, - SecurityStatusPalErrorCode.NoImpersonation => NegotiateAuthenticationStatusCode.UnknownCredentials, - SecurityStatusPalErrorCode.LogonDenied => NegotiateAuthenticationStatusCode.UnknownCredentials, - SecurityStatusPalErrorCode.UnknownCredentials => NegotiateAuthenticationStatusCode.UnknownCredentials, - SecurityStatusPalErrorCode.NoCredentials => NegotiateAuthenticationStatusCode.UnknownCredentials, - SecurityStatusPalErrorCode.MessageAltered => NegotiateAuthenticationStatusCode.MessageAltered, - SecurityStatusPalErrorCode.OutOfSequence => NegotiateAuthenticationStatusCode.OutOfSequence, - SecurityStatusPalErrorCode.NoAuthenticatingAuthority => NegotiateAuthenticationStatusCode.InvalidCredentials, - SecurityStatusPalErrorCode.IncompleteCredentials => NegotiateAuthenticationStatusCode.InvalidCredentials, - SecurityStatusPalErrorCode.IllegalMessage => NegotiateAuthenticationStatusCode.InvalidToken, - SecurityStatusPalErrorCode.CertExpired => NegotiateAuthenticationStatusCode.CredentialsExpired, - SecurityStatusPalErrorCode.SecurityQosFailed => NegotiateAuthenticationStatusCode.QopNotSupported, - SecurityStatusPalErrorCode.UnsupportedPreauth => NegotiateAuthenticationStatusCode.InvalidToken, - SecurityStatusPalErrorCode.BadBinding => NegotiateAuthenticationStatusCode.BadBinding, - SecurityStatusPalErrorCode.UntrustedRoot => NegotiateAuthenticationStatusCode.UnknownCredentials, - SecurityStatusPalErrorCode.SmartcardLogonRequired => NegotiateAuthenticationStatusCode.UnknownCredentials, - SecurityStatusPalErrorCode.WrongPrincipal => NegotiateAuthenticationStatusCode.UnknownCredentials, - SecurityStatusPalErrorCode.CannotPack => NegotiateAuthenticationStatusCode.InvalidToken, - SecurityStatusPalErrorCode.TimeSkew => NegotiateAuthenticationStatusCode.InvalidToken, - SecurityStatusPalErrorCode.AlgorithmMismatch => NegotiateAuthenticationStatusCode.InvalidToken, - SecurityStatusPalErrorCode.CertUnknown => NegotiateAuthenticationStatusCode.UnknownCredentials, - - // Processing partial inputs is not supported, so this is result of incorrect input - SecurityStatusPalErrorCode.IncompleteMessage => NegotiateAuthenticationStatusCode.InvalidToken, - - _ => NegotiateAuthenticationStatusCode.GenericFailure, - }; + byte[]? blob = _pal.GetOutgoingBlob(incomingBlob, out statusCode); // Additional policy validation if (statusCode == NegotiateAuthenticationStatusCode.Completed) @@ -414,12 +289,12 @@ public System.Security.Principal.TokenImpersonationLevel ImpersonationLevel /// Authentication failed or has not occurred. public NegotiateAuthenticationStatusCode Wrap(ReadOnlySpan input, IBufferWriter outputWriter, bool requestEncryption, out bool isEncrypted) { - if (!IsAuthenticated || _ntAuthentication == null) + if (!IsAuthenticated || _isDisposed) { throw new InvalidOperationException(SR.net_auth_noauth); } - return _ntAuthentication.Wrap(input, outputWriter, requestEncryption, out isEncrypted); + return _pal.Wrap(input, outputWriter, requestEncryption, out isEncrypted); } /// @@ -441,12 +316,12 @@ public NegotiateAuthenticationStatusCode Wrap(ReadOnlySpan input, IBufferW /// Authentication failed or has not occurred. public NegotiateAuthenticationStatusCode Unwrap(ReadOnlySpan input, IBufferWriter outputWriter, out bool wasEncrypted) { - if (!IsAuthenticated || _ntAuthentication == null) + if (!IsAuthenticated || _isDisposed) { throw new InvalidOperationException(SR.net_auth_noauth); } - return _ntAuthentication.Unwrap(input, outputWriter, out wasEncrypted); + return _pal.Unwrap(input, outputWriter, out wasEncrypted); } /// @@ -469,12 +344,12 @@ public NegotiateAuthenticationStatusCode Unwrap(ReadOnlySpan input, IBuffe /// Authentication failed or has not occurred. public NegotiateAuthenticationStatusCode UnwrapInPlace(Span input, out int unwrappedOffset, out int unwrappedLength, out bool wasEncrypted) { - if (!IsAuthenticated || _ntAuthentication == null) + if (!IsAuthenticated || _isDisposed) { throw new InvalidOperationException(SR.net_auth_noauth); } - return _ntAuthentication.UnwrapInPlace(input, out unwrappedOffset, out unwrappedLength, out wasEncrypted); + return _pal.UnwrapInPlace(input, out unwrappedOffset, out unwrappedLength, out wasEncrypted); } /// @@ -490,12 +365,12 @@ public NegotiateAuthenticationStatusCode UnwrapInPlace(Span input, out int /// internal void GetMIC(ReadOnlySpan message, IBufferWriter signature) { - if (!IsAuthenticated || _ntAuthentication == null) + if (!IsAuthenticated || _isDisposed) { throw new InvalidOperationException(SR.net_auth_noauth); } - _ntAuthentication.GetMIC(message, signature); + _pal.GetMIC(message, signature); } /// @@ -512,20 +387,19 @@ internal void GetMIC(ReadOnlySpan message, IBufferWriter signature) /// internal bool VerifyMIC(ReadOnlySpan message, ReadOnlySpan signature) { - if (!IsAuthenticated || _ntAuthentication == null) + if (!IsAuthenticated || _isDisposed) { throw new InvalidOperationException(SR.net_auth_noauth); } - return _ntAuthentication.VerifyMIC(message, signature); + return _pal.VerifyMIC(message, signature); } private bool CheckSpn() { - Debug.Assert(_ntAuthentication != null); Debug.Assert(_extendedProtectionPolicy != null); - if (_ntAuthentication.IsKerberos) + if (_pal.Package == NegotiationInfoClass.Kerberos) { if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, SR.net_log_listener_no_spn_kerberos); return true; @@ -549,7 +423,7 @@ private bool CheckSpn() return true; } - string? clientSpn = _ntAuthentication.ClientSpecifiedSpn; + string? clientSpn = _pal.TargetName; if (string.IsNullOrEmpty(clientSpn)) { diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthenticationStatusCode.cs b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthenticationStatusCode.cs index 0704b390bd21fc47ab521243135f71341b6ae870..b1c5c7dc7903c60354bd663809a2952c1835e084 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthenticationStatusCode.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateAuthenticationStatusCode.cs @@ -25,7 +25,7 @@ public enum NegotiateAuthenticationStatusCode BadBinding, /// Unsupported authentication package was requested. - /// Maps to GSS_S_BAD_MECH status in GSSAPI. + /// Maps to GSS_S_BAD_MECH or GSS_S_UNAVAILABLE status in GSSAPI. Unsupported, /// Message was altered and failed an integrity check validation. diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStream.cs b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStream.cs index 19022bdfa33d929811920f9fd7c3be8cec26d04c..f26a837e0c110f1a2d9034f2b36aad7d5d3df390 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStream.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStream.cs @@ -640,7 +640,13 @@ void ThrowExceptional(ExceptionDispatchInfo e) ArgumentNullException.ThrowIfNull(credential); ArgumentNullException.ThrowIfNull(servicePrincipalName); - NegotiateStreamPal.ValidateImpersonationLevel(impersonationLevel); + if (impersonationLevel != TokenImpersonationLevel.Identification && + impersonationLevel != TokenImpersonationLevel.Impersonation && + impersonationLevel != TokenImpersonationLevel.Delegation) + { + throw new ArgumentOutOfRangeException(nameof(impersonationLevel), impersonationLevel.ToString(), SR.net_auth_supported_impl_levels); + } + if (_context is not null && IsServer != isServer) { throw new InvalidOperationException(SR.net_auth_client_server); diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.Managed.cs b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.Managed.cs deleted file mode 100644 index 9734bdf441ed69bcfd7c66178b31a73f1a034188..0000000000000000000000000000000000000000 --- a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.Managed.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.ComponentModel; -using System.Runtime.Versioning; -using System.Security.Principal; - -namespace System.Net.Security -{ - // - // The class maintains the state of the authentication process and the security context. - // It encapsulates security context and does the real work in authentication and - // user data encryption with NEGO SSPI package. - // - internal static partial class NegotiateStreamPal - { -#pragma warning disable IDE0060 - internal static IIdentity GetIdentity(NTAuthentication context) - { - throw new PlatformNotSupportedException(); - } - - internal static void ValidateImpersonationLevel(TokenImpersonationLevel impersonationLevel) - { - if (impersonationLevel != TokenImpersonationLevel.Identification) - { - throw new ArgumentOutOfRangeException(nameof(impersonationLevel), impersonationLevel.ToString(), - SR.net_auth_supported_impl_levels); - } - } -#pragma warning restore IDE0060 - } -} diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.Unix.cs b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.Unix.cs deleted file mode 100644 index 421e1a41902ebb56246345bf2e7437bfac8a5d3e..0000000000000000000000000000000000000000 --- a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.Unix.cs +++ /dev/null @@ -1,676 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.IO; -using System.Buffers; -using System.Buffers.Binary; -using System.ComponentModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; -using System.Security; -using System.Security.Authentication; -using System.Security.Authentication.ExtendedProtection; -using System.Security.Principal; -using System.Text; -using System.Threading; -using Microsoft.Win32.SafeHandles; - -namespace System.Net.Security -{ - // - // The class maintains the state of the authentication process and the security context. - // It encapsulates security context and does the real work in authentication and - // user data encryption with NEGO SSPI package. - // - internal static partial class NegotiateStreamPal - { - // value should match the Windows sspicli NTE_FAIL value - // defined in winerror.h - private const int NTE_FAIL = unchecked((int)0x80090020); - - internal static string QueryContextClientSpecifiedSpn(SafeDeleteContext _ /*securityContext*/) - { - throw new PlatformNotSupportedException(SR.net_nego_server_not_supported); - } - - internal static string QueryContextAuthenticationPackage(SafeDeleteContext securityContext) - { - SafeDeleteNegoContext negoContext = (SafeDeleteNegoContext)securityContext; - return negoContext.IsNtlmUsed ? NegotiationInfoClass.NTLM : NegotiationInfoClass.Kerberos; - } - - private static byte[] GssWrap( - SafeGssContextHandle context, - ref bool encrypt, - ReadOnlySpan buffer) - { - Interop.NetSecurityNative.GssBuffer encryptedBuffer = default; - try - { - Interop.NetSecurityNative.Status minorStatus; - Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.WrapBuffer(out minorStatus, context, ref encrypt, buffer, ref encryptedBuffer); - if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) - { - throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); - } - - return encryptedBuffer.ToByteArray(); - } - finally - { - encryptedBuffer.Dispose(); - } - } - - private static int GssUnwrap( - SafeGssContextHandle context, - out bool encrypt, - Span buffer) - { - Interop.NetSecurityNative.GssBuffer decryptedBuffer = default(Interop.NetSecurityNative.GssBuffer); - try - { - Interop.NetSecurityNative.Status minorStatus; - Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.UnwrapBuffer(out minorStatus, context, out encrypt, buffer, ref decryptedBuffer); - if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) - { - throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); - } - - decryptedBuffer.Span.CopyTo(buffer); - return decryptedBuffer.Span.Length; - } - finally - { - decryptedBuffer.Dispose(); - } - } - - private static string GssGetUser( - ref SafeGssContextHandle? context) - { - Interop.NetSecurityNative.GssBuffer token = default(Interop.NetSecurityNative.GssBuffer); - - try - { - Interop.NetSecurityNative.Status status - = Interop.NetSecurityNative.GetUser(out var minorStatus, - context, - ref token); - - if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) - { - throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); - } - - ReadOnlySpan tokenBytes = token.Span; - int length = tokenBytes.Length; - if (length > 0 && tokenBytes[length - 1] == '\0') - { - // Some GSS-API providers (gss-ntlmssp) include the terminating null with strings, so skip that. - tokenBytes = tokenBytes.Slice(0, length - 1); - } - -#if NETSTANDARD2_0 - return Encoding.UTF8.GetString(tokenBytes.ToArray(), 0, tokenBytes.Length); -#else - return Encoding.UTF8.GetString(tokenBytes); -#endif - } - finally - { - token.Dispose(); - } - } - - private static SecurityStatusPal EstablishSecurityContext( - SafeFreeNegoCredentials credential, - ref SafeDeleteContext? context, - ChannelBinding? channelBinding, - string? targetName, - ContextFlagsPal inFlags, - ReadOnlySpan incomingBlob, - out byte[]? resultBuffer, - ref ContextFlagsPal outFlags) - { - Interop.NetSecurityNative.PackageType packageType = credential.PackageType; - - resultBuffer = null; - - if (context == null) - { - if (NetEventSource.Log.IsEnabled()) - { - string protocol = packageType switch { - Interop.NetSecurityNative.PackageType.NTLM => "NTLM", - Interop.NetSecurityNative.PackageType.Kerberos => "Kerberos", - _ => "SPNEGO" - }; - NetEventSource.Info(context, $"requested protocol = {protocol}, target = {targetName}"); - } - - context = new SafeDeleteNegoContext(credential, targetName!); - } - - Interop.NetSecurityNative.GssBuffer token = default(Interop.NetSecurityNative.GssBuffer); - Interop.NetSecurityNative.Status status; - Interop.NetSecurityNative.Status minorStatus; - SafeDeleteNegoContext negoContext = (SafeDeleteNegoContext)context; - SafeGssContextHandle contextHandle = negoContext.GssContext; - try - { - Interop.NetSecurityNative.GssFlags inputFlags = - ContextFlagsAdapterPal.GetInteropFromContextFlagsPal(inFlags, isServer: false); - uint outputFlags; - bool isNtlmUsed; - - if (channelBinding != null) - { - // If a TLS channel binding token (cbt) is available then get the pointer - // to the application specific data. - int appDataOffset = Marshal.SizeOf(); - Debug.Assert(appDataOffset < channelBinding.Size); - IntPtr cbtAppData = channelBinding.DangerousGetHandle() + appDataOffset; - int cbtAppDataSize = channelBinding.Size - appDataOffset; - status = Interop.NetSecurityNative.InitSecContext(out minorStatus, - credential.GssCredential, - ref contextHandle, - packageType, - cbtAppData, - cbtAppDataSize, - negoContext.TargetName, - (uint)inputFlags, - incomingBlob, - ref token, - out outputFlags, - out isNtlmUsed); - } - else - { - status = Interop.NetSecurityNative.InitSecContext(out minorStatus, - credential.GssCredential, - ref contextHandle, - packageType, - negoContext.TargetName, - (uint)inputFlags, - incomingBlob, - ref token, - out outputFlags, - out isNtlmUsed); - } - - if ((status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) && - (status != Interop.NetSecurityNative.Status.GSS_S_CONTINUE_NEEDED)) - { - if (negoContext.GssContext.IsInvalid) - { - context.Dispose(); - } - - Interop.NetSecurityNative.GssApiException gex = new Interop.NetSecurityNative.GssApiException(status, minorStatus); - if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, gex); - resultBuffer = Array.Empty(); - return new SecurityStatusPal(GetErrorCode(gex), gex); - } - - resultBuffer = token.ToByteArray(); - - if (status == Interop.NetSecurityNative.Status.GSS_S_COMPLETE) - { - if (NetEventSource.Log.IsEnabled()) - { - string protocol = packageType switch { - Interop.NetSecurityNative.PackageType.NTLM => "NTLM", - Interop.NetSecurityNative.PackageType.Kerberos => "Kerberos", - _ => isNtlmUsed ? "SPNEGO-NTLM" : "SPNEGO-Kerberos" - }; - NetEventSource.Info(context, $"actual protocol = {protocol}"); - } - - // Populate protocol used for authentication - negoContext.SetAuthenticationPackage(isNtlmUsed); - } - - Debug.Assert(resultBuffer != null, "Unexpected null buffer returned by GssApi"); - outFlags = ContextFlagsAdapterPal.GetContextFlagsPalFromInterop( - (Interop.NetSecurityNative.GssFlags)outputFlags, isServer: false); - - SecurityStatusPalErrorCode errorCode = status == Interop.NetSecurityNative.Status.GSS_S_COMPLETE ? - SecurityStatusPalErrorCode.OK : - SecurityStatusPalErrorCode.ContinueNeeded; - return new SecurityStatusPal(errorCode); - } - catch (Exception ex) - { - if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, ex); - return new SecurityStatusPal(SecurityStatusPalErrorCode.InternalError, ex); - } - finally - { - token.Dispose(); - - // Save the inner context handle for further calls to NetSecurity - // - // For the first call `negoContext.GssContext` is invalid and we expect the - // inital handle to be returned from InitSecContext. For any subsequent - // call the handle should stay the same or it can be destroyed by the native - // InitSecContext call. - Debug.Assert( - negoContext.GssContext == contextHandle || - negoContext.GssContext.IsInvalid || - contextHandle.IsInvalid); - negoContext.SetGssContext(contextHandle); - } - } - - internal static SecurityStatusPal InitializeSecurityContext( - ref SafeFreeCredentials credentialsHandle, - ref SafeDeleteContext? securityContext, - string? spn, - ContextFlagsPal requestedContextFlags, - ReadOnlySpan incomingBlob, - ChannelBinding? channelBinding, - ref byte[]? resultBlob, - out int resultBlobLength, - ref ContextFlagsPal contextFlags) - { - SafeFreeNegoCredentials negoCredentialsHandle = (SafeFreeNegoCredentials)credentialsHandle; - - if (negoCredentialsHandle.IsDefault && string.IsNullOrEmpty(spn)) - { - throw new PlatformNotSupportedException(SR.net_nego_not_supported_empty_target_with_defaultcreds); - } - - SecurityStatusPal status = EstablishSecurityContext( - negoCredentialsHandle, - ref securityContext, - channelBinding, - spn, - requestedContextFlags, - incomingBlob, - out resultBlob, - ref contextFlags); - resultBlobLength = resultBlob?.Length ?? 0; - - return status; - } - -#pragma warning disable IDE0060 - internal static SecurityStatusPal AcceptSecurityContext( - SafeFreeCredentials? credentialsHandle, - ref SafeDeleteContext? securityContext, - ContextFlagsPal requestedContextFlags, - ReadOnlySpan incomingBlob, - ChannelBinding? channelBinding, - ref byte[] resultBlob, - out int resultBlobLength, - ref ContextFlagsPal contextFlags) - { - securityContext ??= new SafeDeleteNegoContext((SafeFreeNegoCredentials)credentialsHandle!); - - SafeDeleteNegoContext negoContext = (SafeDeleteNegoContext)securityContext; - SafeGssContextHandle contextHandle = negoContext.GssContext; - Interop.NetSecurityNative.GssBuffer token = default(Interop.NetSecurityNative.GssBuffer); - try - { - Interop.NetSecurityNative.Status status; - Interop.NetSecurityNative.Status minorStatus; - status = Interop.NetSecurityNative.AcceptSecContext(out minorStatus, - negoContext.AcceptorCredential, - ref contextHandle, - incomingBlob, - ref token, - out uint outputFlags, - out bool isNtlmUsed); - - if ((status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) && - (status != Interop.NetSecurityNative.Status.GSS_S_CONTINUE_NEEDED)) - { - if (negoContext.GssContext.IsInvalid) - { - contextHandle.Dispose(); - } - - Interop.NetSecurityNative.GssApiException gex = new Interop.NetSecurityNative.GssApiException(status, minorStatus); - if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, gex); - resultBlobLength = 0; - return new SecurityStatusPal(GetErrorCode(gex), gex); - } - - resultBlob = token.ToByteArray(); - - Debug.Assert(resultBlob != null, "Unexpected null buffer returned by GssApi"); - - contextFlags = ContextFlagsAdapterPal.GetContextFlagsPalFromInterop( - (Interop.NetSecurityNative.GssFlags)outputFlags, isServer: true); - resultBlobLength = resultBlob.Length; - - SecurityStatusPalErrorCode errorCode; - if (status == Interop.NetSecurityNative.Status.GSS_S_COMPLETE) - { - if (NetEventSource.Log.IsEnabled()) - { - string protocol = isNtlmUsed ? "SPNEGO-NTLM" : "SPNEGO-Kerberos"; - NetEventSource.Info(securityContext, $"AcceptSecurityContext: actual protocol = {protocol}"); - } - - negoContext.SetAuthenticationPackage(isNtlmUsed); - errorCode = SecurityStatusPalErrorCode.OK; - } - else - { - errorCode = SecurityStatusPalErrorCode.ContinueNeeded; - } - - return new SecurityStatusPal(errorCode); - } - catch (Exception ex) - { - if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, ex); - resultBlobLength = 0; - return new SecurityStatusPal(SecurityStatusPalErrorCode.InternalError, ex); - } - finally - { - token.Dispose(); - - // Save the inner context handle for further calls to NetSecurity - // - // For the first call `negoContext.GssContext` is invalid and we expect the - // inital handle to be returned from AcceptSecContext. For any subsequent - // call the handle should stay the same or it can be destroyed by the native - // AcceptSecContext call. - Debug.Assert( - negoContext.GssContext == contextHandle || - negoContext.GssContext.IsInvalid || - contextHandle.IsInvalid); - negoContext.SetGssContext(contextHandle); - } - } -#pragma warning restore IDE0060 - - // https://www.gnu.org/software/gss/reference/gss.pdf (page 25) - private static SecurityStatusPalErrorCode GetErrorCode(Interop.NetSecurityNative.GssApiException exception) - { - switch (exception.MajorStatus) - { - case Interop.NetSecurityNative.Status.GSS_S_NO_CRED: - return SecurityStatusPalErrorCode.UnknownCredentials; - case Interop.NetSecurityNative.Status.GSS_S_BAD_BINDINGS: - return SecurityStatusPalErrorCode.BadBinding; - case Interop.NetSecurityNative.Status.GSS_S_CREDENTIALS_EXPIRED: - return SecurityStatusPalErrorCode.CertExpired; - case Interop.NetSecurityNative.Status.GSS_S_DEFECTIVE_TOKEN: - return SecurityStatusPalErrorCode.InvalidToken; - case Interop.NetSecurityNative.Status.GSS_S_DEFECTIVE_CREDENTIAL: - return SecurityStatusPalErrorCode.IncompleteCredentials; - case Interop.NetSecurityNative.Status.GSS_S_BAD_SIG: - return SecurityStatusPalErrorCode.MessageAltered; - case Interop.NetSecurityNative.Status.GSS_S_BAD_MECH: - return SecurityStatusPalErrorCode.Unsupported; - case Interop.NetSecurityNative.Status.GSS_S_NO_CONTEXT: - default: - return SecurityStatusPalErrorCode.InternalError; - } - } - - private static string GetUser( - ref SafeDeleteContext securityContext) - { - SafeDeleteNegoContext negoContext = (SafeDeleteNegoContext)securityContext; - try - { - SafeGssContextHandle? contextHandle = negoContext.GssContext; - return GssGetUser(ref contextHandle); - } - catch (Exception ex) - { - if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, ex); - throw; - } - } - - internal static Win32Exception CreateExceptionFromError(SecurityStatusPal statusCode) - { - return new Win32Exception(NTE_FAIL, (statusCode.Exception != null) ? statusCode.Exception.Message : statusCode.ErrorCode.ToString()); - } - -#pragma warning disable IDE0060 - internal static int QueryMaxTokenSize(string package) - { - // This value is not used on Unix - return 0; - } -#pragma warning restore IDE0060 - - internal static SafeFreeCredentials AcquireDefaultCredential(string package, bool isServer) - { - return AcquireCredentialsHandle(package, isServer, new NetworkCredential(string.Empty, string.Empty, string.Empty)); - } - - internal static SafeFreeCredentials AcquireCredentialsHandle(string package, bool isServer, NetworkCredential credential) - { - bool isEmptyCredential = string.IsNullOrWhiteSpace(credential.UserName) || - string.IsNullOrWhiteSpace(credential.Password); - Interop.NetSecurityNative.PackageType packageType; - - if (string.Equals(package, NegotiationInfoClass.Negotiate, StringComparison.OrdinalIgnoreCase)) - { - packageType = Interop.NetSecurityNative.PackageType.Negotiate; - } - else if (string.Equals(package, NegotiationInfoClass.NTLM, StringComparison.OrdinalIgnoreCase)) - { - packageType = Interop.NetSecurityNative.PackageType.NTLM; - if (isEmptyCredential && !isServer) - { - // NTLM authentication is not possible with default credentials which are no-op - throw new PlatformNotSupportedException(SR.net_ntlm_not_possible_default_cred); - } - } - else if (string.Equals(package, NegotiationInfoClass.Kerberos, StringComparison.OrdinalIgnoreCase)) - { - packageType = Interop.NetSecurityNative.PackageType.Kerberos; - } - else - { - // Native shim currently supports only NTLM, Negotiate and Kerberos - throw new PlatformNotSupportedException(SR.net_securitypackagesupport); - } - - try - { - return isEmptyCredential ? - new SafeFreeNegoCredentials(packageType, string.Empty, string.Empty, string.Empty) : - new SafeFreeNegoCredentials(packageType, credential.UserName, credential.Password, credential.Domain); - } - catch (Exception ex) - { - throw new Win32Exception(NTE_FAIL, ex.Message); - } - } - -#pragma warning disable IDE0060 - internal static SecurityStatusPal CompleteAuthToken( - ref SafeDeleteContext? securityContext, - ReadOnlySpan incomingBlob) - { - return new SecurityStatusPal(SecurityStatusPalErrorCode.OK); - } -#pragma warning restore IDE0060 - - internal static NegotiateAuthenticationStatusCode Unwrap( - SafeDeleteContext securityContext, - ReadOnlySpan input, - IBufferWriter outputWriter, - out bool isEncrypted) - { - SafeGssContextHandle gssContext = ((SafeDeleteNegoContext)securityContext).GssContext!; - Interop.NetSecurityNative.GssBuffer decryptedBuffer = default(Interop.NetSecurityNative.GssBuffer); - try - { - Interop.NetSecurityNative.Status minorStatus; - Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.UnwrapBuffer(out minorStatus, gssContext, out isEncrypted, input, ref decryptedBuffer); - if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) - { - return status switch - { - Interop.NetSecurityNative.Status.GSS_S_BAD_SIG => NegotiateAuthenticationStatusCode.MessageAltered, - _ => NegotiateAuthenticationStatusCode.InvalidToken - }; - } - - decryptedBuffer.Span.CopyTo(outputWriter.GetSpan(decryptedBuffer.Span.Length)); - outputWriter.Advance(decryptedBuffer.Span.Length); - return NegotiateAuthenticationStatusCode.Completed; - } - finally - { - decryptedBuffer.Dispose(); - } - } - - internal static NegotiateAuthenticationStatusCode UnwrapInPlace( - SafeDeleteContext securityContext, - Span input, - out int unwrappedOffset, - out int unwrappedLength, - out bool isEncrypted) - { - SafeGssContextHandle gssContext = ((SafeDeleteNegoContext)securityContext).GssContext!; - Interop.NetSecurityNative.GssBuffer decryptedBuffer = default(Interop.NetSecurityNative.GssBuffer); - try - { - Interop.NetSecurityNative.Status minorStatus; - Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.UnwrapBuffer(out minorStatus, gssContext, out isEncrypted, input, ref decryptedBuffer); - if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) - { - unwrappedOffset = 0; - unwrappedLength = 0; - return status switch - { - Interop.NetSecurityNative.Status.GSS_S_BAD_SIG => NegotiateAuthenticationStatusCode.MessageAltered, - _ => NegotiateAuthenticationStatusCode.InvalidToken - }; - } - - decryptedBuffer.Span.CopyTo(input); - unwrappedOffset = 0; - unwrappedLength = decryptedBuffer.Span.Length; - return NegotiateAuthenticationStatusCode.Completed; - } - finally - { - decryptedBuffer.Dispose(); - } - } - - internal static NegotiateAuthenticationStatusCode Wrap( - SafeDeleteContext securityContext, - ReadOnlySpan input, - IBufferWriter outputWriter, - bool requestEncryption, - out bool isEncrypted) - { - SafeGssContextHandle gssContext = ((SafeDeleteNegoContext)securityContext).GssContext!; - Interop.NetSecurityNative.GssBuffer encryptedBuffer = default; - try - { - Interop.NetSecurityNative.Status minorStatus; - bool encrypt = requestEncryption; - Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.WrapBuffer( - out minorStatus, - gssContext, - ref encrypt, - input, - ref encryptedBuffer); - isEncrypted = encrypt; - if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) - { - return NegotiateAuthenticationStatusCode.GenericFailure; - } - - encryptedBuffer.Span.CopyTo(outputWriter.GetSpan(encryptedBuffer.Span.Length)); - outputWriter.Advance(encryptedBuffer.Span.Length); - return NegotiateAuthenticationStatusCode.Completed; - } - finally - { - encryptedBuffer.Dispose(); - } - } - - internal static bool VerifyMIC( - SafeDeleteContext securityContext, - bool isConfidential, - ReadOnlySpan message, - ReadOnlySpan signature) - { - _ = isConfidential; - SafeGssContextHandle gssContext = ((SafeDeleteNegoContext)securityContext).GssContext!; - Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.VerifyMic( - out _, - gssContext, - message, - signature); - return status == Interop.NetSecurityNative.Status.GSS_S_COMPLETE; - } - - internal static void GetMIC( - SafeDeleteContext securityContext, - bool isConfidential, - ReadOnlySpan message, - IBufferWriter signature) - { - _ = isConfidential; - SafeGssContextHandle gssContext = ((SafeDeleteNegoContext)securityContext).GssContext!; - Interop.NetSecurityNative.GssBuffer micBuffer = default; - try - { - Interop.NetSecurityNative.Status minorStatus; - Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.GetMic( - out minorStatus, - gssContext, - message, - ref micBuffer); - if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) - { - throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); - } - - signature.Write(micBuffer.Span); - } - finally - { - micBuffer.Dispose(); - } - } - - internal static IIdentity GetIdentity(NTAuthentication context) - { - string name = context.Spn!; - string protocol = context.ProtocolName; - - if (context.IsServer) - { - SafeDeleteContext safeContext = context.GetContext(out var status)!; - if (status.ErrorCode != SecurityStatusPalErrorCode.OK) - { - throw new Win32Exception((int)status.ErrorCode); - } - name = GetUser(ref safeContext); - } - - return new GenericIdentity(name, protocol); - } - - internal static void ValidateImpersonationLevel(TokenImpersonationLevel impersonationLevel) - { - if (impersonationLevel != TokenImpersonationLevel.Identification) - { - throw new ArgumentOutOfRangeException(nameof(impersonationLevel), impersonationLevel.ToString(), - SR.net_auth_supported_impl_levels); - } - } - } -} diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.Windows.cs b/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.Windows.cs deleted file mode 100644 index 3d42cacd2557a275e1442dfb5a5eae70878cba92..0000000000000000000000000000000000000000 --- a/src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStreamPal.Windows.cs +++ /dev/null @@ -1,512 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Buffers; -using System.Buffers.Binary; -using System.ComponentModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Runtime.InteropServices; -using System.Security; -using System.Security.Principal; -using System.Security.Authentication.ExtendedProtection; - -namespace System.Net.Security -{ - // - // The class does the real work in authentication and - // user data encryption with NEGO SSPI package. - // - // This is part of the NegotiateStream PAL. - // - internal static partial class NegotiateStreamPal - { - internal static int QueryMaxTokenSize(string package) - { - return SSPIWrapper.GetVerifyPackageInfo(GlobalSSPI.SSPIAuth, package, true)!.MaxToken; - } - - internal static SafeFreeCredentials AcquireDefaultCredential(string package, bool isServer) - { - return SSPIWrapper.AcquireDefaultCredential( - GlobalSSPI.SSPIAuth, - package, - (isServer ? Interop.SspiCli.CredentialUse.SECPKG_CRED_INBOUND : Interop.SspiCli.CredentialUse.SECPKG_CRED_OUTBOUND)); - } - - internal static SafeFreeCredentials AcquireCredentialsHandle(string package, bool isServer, NetworkCredential credential) - { - SafeSspiAuthDataHandle? authData = null; - try - { - Interop.SECURITY_STATUS result = Interop.SspiCli.SspiEncodeStringsAsAuthIdentity( - credential.UserName, credential.Domain, - credential.Password, out authData); - - if (result != Interop.SECURITY_STATUS.OK) - { - if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, SR.Format(SR.net_log_operation_failed_with_error, nameof(Interop.SspiCli.SspiEncodeStringsAsAuthIdentity), $"0x{(int)result:X}")); - throw new Win32Exception((int)result); - } - - return SSPIWrapper.AcquireCredentialsHandle(GlobalSSPI.SSPIAuth, - package, (isServer ? Interop.SspiCli.CredentialUse.SECPKG_CRED_INBOUND : Interop.SspiCli.CredentialUse.SECPKG_CRED_OUTBOUND), ref authData); - } - finally - { - authData?.Dispose(); - } - } - - internal static string? QueryContextAssociatedName(SafeDeleteContext securityContext) - { - return SSPIWrapper.QueryStringContextAttributes(GlobalSSPI.SSPIAuth, securityContext, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_NAMES); - } - - internal static string? QueryContextClientSpecifiedSpn(SafeDeleteContext securityContext) - { - return SSPIWrapper.QueryStringContextAttributes(GlobalSSPI.SSPIAuth, securityContext, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_CLIENT_SPECIFIED_TARGET); - } - - internal static string? QueryContextAuthenticationPackage(SafeDeleteContext securityContext) - { - SecPkgContext_NegotiationInfoW ctx = default; - bool success = SSPIWrapper.QueryBlittableContextAttributes(GlobalSSPI.SSPIAuth, securityContext, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_NEGOTIATION_INFO, typeof(SafeFreeContextBuffer), out SafeHandle? sspiHandle, ref ctx); - using (sspiHandle) - { - return success ? NegotiationInfoClass.GetAuthenticationPackageName(sspiHandle!, (int)ctx.NegotiationState) : null; - } - } - - internal static SecurityStatusPal InitializeSecurityContext( - ref SafeFreeCredentials? credentialsHandle, - ref SafeDeleteContext? securityContext, - string? spn, - ContextFlagsPal requestedContextFlags, - ReadOnlySpan incomingBlob, - ChannelBinding? channelBinding, - ref byte[]? resultBlob, - out int resultBlobLength, - ref ContextFlagsPal contextFlags) - { - - InputSecurityBuffers inputBuffers = default; - if (!incomingBlob.IsEmpty) - { - inputBuffers.SetNextBuffer(new InputSecurityBuffer(incomingBlob, SecurityBufferType.SECBUFFER_TOKEN)); - } - - if (channelBinding != null) - { - inputBuffers.SetNextBuffer(new InputSecurityBuffer(channelBinding)); - } - - var outSecurityBuffer = new SecurityBuffer(resultBlob, SecurityBufferType.SECBUFFER_TOKEN); - - Interop.SspiCli.ContextFlags outContextFlags = Interop.SspiCli.ContextFlags.Zero; - // There is only one SafeDeleteContext type on Windows which is SafeDeleteSslContext so this cast is safe. - SafeDeleteSslContext? sslContext = (SafeDeleteSslContext?)securityContext; - Interop.SECURITY_STATUS winStatus = (Interop.SECURITY_STATUS)SSPIWrapper.InitializeSecurityContext( - GlobalSSPI.SSPIAuth, - ref credentialsHandle, - ref sslContext, - spn, - ContextFlagsAdapterPal.GetInteropFromContextFlagsPal(requestedContextFlags), - Interop.SspiCli.Endianness.SECURITY_NETWORK_DREP, - inputBuffers, - ref outSecurityBuffer, - ref outContextFlags); - securityContext = sslContext; - Debug.Assert(outSecurityBuffer.offset == 0); - resultBlob = outSecurityBuffer.token; - resultBlobLength = outSecurityBuffer.size; - contextFlags = ContextFlagsAdapterPal.GetContextFlagsPalFromInterop(outContextFlags); - return SecurityStatusAdapterPal.GetSecurityStatusPalFromInterop(winStatus); - } - - internal static SecurityStatusPal CompleteAuthToken( - ref SafeDeleteContext? securityContext, - ReadOnlySpan incomingBlob) - { - // There is only one SafeDeleteContext type on Windows which is SafeDeleteSslContext so this cast is safe. - SafeDeleteSslContext? sslContext = (SafeDeleteSslContext?)securityContext; - var inSecurityBuffer = new InputSecurityBuffer(incomingBlob, SecurityBufferType.SECBUFFER_TOKEN); - Interop.SECURITY_STATUS winStatus = (Interop.SECURITY_STATUS)SSPIWrapper.CompleteAuthToken( - GlobalSSPI.SSPIAuth, - ref sslContext, - in inSecurityBuffer); - securityContext = sslContext; - return SecurityStatusAdapterPal.GetSecurityStatusPalFromInterop(winStatus); - } - - internal static SecurityStatusPal AcceptSecurityContext( - SafeFreeCredentials? credentialsHandle, - ref SafeDeleteContext? securityContext, - ContextFlagsPal requestedContextFlags, - ReadOnlySpan incomingBlob, - ChannelBinding? channelBinding, - ref byte[]? resultBlob, - out int resultBlobLength, - ref ContextFlagsPal contextFlags) - { - InputSecurityBuffers inputBuffers = default; - if (!incomingBlob.IsEmpty) - { - inputBuffers.SetNextBuffer(new InputSecurityBuffer(incomingBlob, SecurityBufferType.SECBUFFER_TOKEN)); - } - - if (channelBinding != null) - { - inputBuffers.SetNextBuffer(new InputSecurityBuffer(channelBinding)); - } - - var outSecurityBuffer = new SecurityBuffer(resultBlob, SecurityBufferType.SECBUFFER_TOKEN); - - Interop.SspiCli.ContextFlags outContextFlags = Interop.SspiCli.ContextFlags.Zero; - // There is only one SafeDeleteContext type on Windows which is SafeDeleteSslContext so this cast is safe. - SafeDeleteSslContext? sslContext = (SafeDeleteSslContext?)securityContext; - Interop.SECURITY_STATUS winStatus = (Interop.SECURITY_STATUS)SSPIWrapper.AcceptSecurityContext( - GlobalSSPI.SSPIAuth, - credentialsHandle, - ref sslContext, - ContextFlagsAdapterPal.GetInteropFromContextFlagsPal(requestedContextFlags), - Interop.SspiCli.Endianness.SECURITY_NETWORK_DREP, - inputBuffers, - ref outSecurityBuffer, - ref outContextFlags); - - // SSPI Workaround - // If a client sends up a blob on the initial request, Negotiate returns SEC_E_INVALID_HANDLE - // when it should return SEC_E_INVALID_TOKEN. - if (winStatus == Interop.SECURITY_STATUS.InvalidHandle && securityContext == null && !incomingBlob.IsEmpty) - { - winStatus = Interop.SECURITY_STATUS.InvalidToken; - } - - Debug.Assert(outSecurityBuffer.offset == 0); - resultBlob = outSecurityBuffer.token; - resultBlobLength = outSecurityBuffer.size; - securityContext = sslContext; - contextFlags = ContextFlagsAdapterPal.GetContextFlagsPalFromInterop(outContextFlags); - return SecurityStatusAdapterPal.GetSecurityStatusPalFromInterop(winStatus); - } - - internal static Win32Exception CreateExceptionFromError(SecurityStatusPal statusCode) - { - return new Win32Exception((int)SecurityStatusAdapterPal.GetInteropFromSecurityStatusPal(statusCode)); - } - - internal static NegotiateAuthenticationStatusCode Unwrap( - SafeDeleteContext securityContext, - ReadOnlySpan input, - IBufferWriter outputWriter, - out bool wasEncrypted) - { - Span outputBuffer = outputWriter.GetSpan(input.Length).Slice(0, input.Length); - NegotiateAuthenticationStatusCode statusCode; - - input.CopyTo(outputBuffer); - statusCode = UnwrapInPlace(securityContext, outputBuffer, out int unwrappedOffset, out int unwrappedLength, out wasEncrypted); - - if (statusCode == NegotiateAuthenticationStatusCode.Completed) - { - if (unwrappedOffset > 0) - { - outputBuffer.Slice(unwrappedOffset, unwrappedLength).CopyTo(outputBuffer); - } - outputWriter.Advance(unwrappedLength); - } - - return statusCode; - } - - internal static unsafe NegotiateAuthenticationStatusCode UnwrapInPlace( - SafeDeleteContext securityContext, - Span input, - out int unwrappedOffset, - out int unwrappedLength, - out bool wasEncrypted) - { - fixed (byte* inputPtr = input) - { - Interop.SspiCli.SecBuffer* unmanagedBuffer = stackalloc Interop.SspiCli.SecBuffer[2]; - Interop.SspiCli.SecBuffer* streamBuffer = &unmanagedBuffer[0]; - Interop.SspiCli.SecBuffer* dataBuffer = &unmanagedBuffer[1]; - streamBuffer->BufferType = SecurityBufferType.SECBUFFER_STREAM; - streamBuffer->pvBuffer = (IntPtr)inputPtr; - streamBuffer->cbBuffer = input.Length; - dataBuffer->BufferType = SecurityBufferType.SECBUFFER_DATA; - dataBuffer->pvBuffer = IntPtr.Zero; - dataBuffer->cbBuffer = 0; - - Interop.SspiCli.SecBufferDesc sdcInOut = new Interop.SspiCli.SecBufferDesc(2) - { - pBuffers = unmanagedBuffer - }; - - uint qop; - int errorCode = GlobalSSPI.SSPIAuth.DecryptMessage(securityContext, ref sdcInOut, out qop); - if (errorCode != 0) - { - unwrappedOffset = 0; - unwrappedLength = 0; - wasEncrypted = false; - return errorCode switch - { - (int)Interop.SECURITY_STATUS.MessageAltered => NegotiateAuthenticationStatusCode.MessageAltered, - _ => NegotiateAuthenticationStatusCode.InvalidToken - }; - } - - if (dataBuffer->BufferType != SecurityBufferType.SECBUFFER_DATA) - { - throw new InternalException(dataBuffer->BufferType); - } - - wasEncrypted = qop != Interop.SspiCli.SECQOP_WRAP_NO_ENCRYPT; - - Debug.Assert((nint)dataBuffer->pvBuffer >= (nint)inputPtr); - Debug.Assert((nint)dataBuffer->pvBuffer + dataBuffer->cbBuffer <= (nint)inputPtr + input.Length); - unwrappedOffset = (int)((byte*)dataBuffer->pvBuffer - inputPtr); - unwrappedLength = dataBuffer->cbBuffer; - return NegotiateAuthenticationStatusCode.Completed; - } - } - - internal static unsafe NegotiateAuthenticationStatusCode Wrap( - SafeDeleteContext securityContext, - ReadOnlySpan input, - IBufferWriter outputWriter, - bool requestEncryption, - out bool isEncrypted) - { - SecPkgContext_Sizes sizes = default; - bool success = SSPIWrapper.QueryBlittableContextAttributes(GlobalSSPI.SSPIAuth, securityContext, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_SIZES, ref sizes); - Debug.Assert(success); - - // alloc new output buffer if not supplied or too small - int resultSize = input.Length + sizes.cbMaxSignature; - Span outputBuffer = outputWriter.GetSpan(resultSize); - - // make a copy of user data for in-place encryption - input.CopyTo(outputBuffer.Slice(sizes.cbMaxSignature, input.Length)); - - isEncrypted = requestEncryption; - - fixed (byte* outputPtr = outputBuffer) - { - // Prepare buffers TOKEN(signature), DATA and Padding. - Interop.SspiCli.SecBuffer* unmanagedBuffer = stackalloc Interop.SspiCli.SecBuffer[2]; - Interop.SspiCli.SecBuffer* tokenBuffer = &unmanagedBuffer[0]; - Interop.SspiCli.SecBuffer* dataBuffer = &unmanagedBuffer[1]; - tokenBuffer->BufferType = SecurityBufferType.SECBUFFER_TOKEN; - tokenBuffer->pvBuffer = (IntPtr)(outputPtr); - tokenBuffer->cbBuffer = sizes.cbMaxSignature; - dataBuffer->BufferType = SecurityBufferType.SECBUFFER_DATA; - dataBuffer->pvBuffer = (IntPtr)(outputPtr + sizes.cbMaxSignature); - dataBuffer->cbBuffer = input.Length; - - Interop.SspiCli.SecBufferDesc sdcInOut = new Interop.SspiCli.SecBufferDesc(2) - { - pBuffers = unmanagedBuffer - }; - - uint qop = requestEncryption ? 0 : Interop.SspiCli.SECQOP_WRAP_NO_ENCRYPT; - int errorCode = GlobalSSPI.SSPIAuth.EncryptMessage(securityContext, ref sdcInOut, qop); - - if (errorCode != 0) - { - return errorCode switch - { - (int)Interop.SECURITY_STATUS.ContextExpired => NegotiateAuthenticationStatusCode.ContextExpired, - (int)Interop.SECURITY_STATUS.QopNotSupported => NegotiateAuthenticationStatusCode.QopNotSupported, - _ => NegotiateAuthenticationStatusCode.GenericFailure, - }; - } - - outputWriter.Advance(tokenBuffer->cbBuffer + dataBuffer->cbBuffer); - return NegotiateAuthenticationStatusCode.Completed; - } - } - - internal static unsafe bool VerifyMIC( - SafeDeleteContext securityContext, - bool isConfidential, - ReadOnlySpan message, - ReadOnlySpan signature) - { - bool refAdded = false; - - try - { - securityContext.DangerousAddRef(ref refAdded); - - fixed (byte* messagePtr = message) - fixed (byte* signaturePtr = signature) - { - Interop.SspiCli.SecBuffer* unmanagedBuffer = stackalloc Interop.SspiCli.SecBuffer[2]; - Interop.SspiCli.SecBuffer* tokenBuffer = &unmanagedBuffer[0]; - Interop.SspiCli.SecBuffer* dataBuffer = &unmanagedBuffer[1]; - tokenBuffer->BufferType = SecurityBufferType.SECBUFFER_TOKEN; - tokenBuffer->pvBuffer = (IntPtr)signaturePtr; - tokenBuffer->cbBuffer = signature.Length; - dataBuffer->BufferType = SecurityBufferType.SECBUFFER_DATA; - dataBuffer->pvBuffer = (IntPtr)messagePtr; - dataBuffer->cbBuffer = message.Length; - - Interop.SspiCli.SecBufferDesc sdcIn = new Interop.SspiCli.SecBufferDesc(2) - { - pBuffers = unmanagedBuffer - }; - - uint qop; - int errorCode = Interop.SspiCli.VerifySignature(ref securityContext._handle, in sdcIn, 0, &qop); - - if (errorCode != 0) - { - Exception e = new Win32Exception(errorCode); - if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, e); - throw new Win32Exception(errorCode); - } - - if (isConfidential && qop == Interop.SspiCli.SECQOP_WRAP_NO_ENCRYPT) - { - Debug.Fail($"Expected qop = 0, returned value = {qop}"); - throw new InvalidOperationException(SR.net_auth_message_not_encrypted); - } - - return true; - } - } - finally - { - if (refAdded) - { - securityContext.DangerousRelease(); - } - } - } - - internal static unsafe void GetMIC( - SafeDeleteContext securityContext, - bool isConfidential, - ReadOnlySpan message, - IBufferWriter signature) - { - bool refAdded = false; - - try - { - securityContext.DangerousAddRef(ref refAdded); - - SecPkgContext_Sizes sizes = default; - bool success = SSPIWrapper.QueryBlittableContextAttributes(GlobalSSPI.SSPIAuth, securityContext, Interop.SspiCli.ContextAttribute.SECPKG_ATTR_SIZES, ref sizes); - Debug.Assert(success); - - Span signatureBuffer = signature.GetSpan(sizes.cbSecurityTrailer); - - fixed (byte* messagePtr = message) - fixed (byte* signaturePtr = signatureBuffer) - { - // Prepare buffers TOKEN(signature), DATA. - Interop.SspiCli.SecBuffer* unmanagedBuffer = stackalloc Interop.SspiCli.SecBuffer[2]; - Interop.SspiCli.SecBuffer* tokenBuffer = &unmanagedBuffer[0]; - Interop.SspiCli.SecBuffer* dataBuffer = &unmanagedBuffer[1]; - tokenBuffer->BufferType = SecurityBufferType.SECBUFFER_TOKEN; - tokenBuffer->pvBuffer = (IntPtr)signaturePtr; - tokenBuffer->cbBuffer = sizes.cbSecurityTrailer; - dataBuffer->BufferType = SecurityBufferType.SECBUFFER_DATA; - dataBuffer->pvBuffer = (IntPtr)messagePtr; - dataBuffer->cbBuffer = message.Length; - - Interop.SspiCli.SecBufferDesc sdcInOut = new Interop.SspiCli.SecBufferDesc(2) - { - pBuffers = unmanagedBuffer - }; - - uint qop = isConfidential ? 0 : Interop.SspiCli.SECQOP_WRAP_NO_ENCRYPT; - int errorCode = Interop.SspiCli.MakeSignature(ref securityContext._handle, qop, ref sdcInOut, 0); - - if (errorCode != 0) - { - Exception e = new Win32Exception(errorCode); - if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, e); - throw new Win32Exception(errorCode); - } - - signature.Advance(signatureBuffer.Length); - } - } - finally - { - if (refAdded) - { - securityContext.DangerousRelease(); - } - } - } - - internal static IIdentity GetIdentity(NTAuthentication context) - { - IIdentity? result; - string? name = context.IsServer ? null : context.Spn; - string protocol = context.ProtocolName; - - if (context.IsServer) - { - SecurityContextTokenHandle? token = null; - try - { - SafeDeleteContext? securityContext = context.GetContext(out SecurityStatusPal status); - if (status.ErrorCode != SecurityStatusPalErrorCode.OK) - { - throw new Win32Exception((int)SecurityStatusAdapterPal.GetInteropFromSecurityStatusPal(status)); - } - - name = QueryContextAssociatedName(securityContext!); - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(context, $"NTAuthentication: The context is associated with [{name}]"); - - // This will return a client token when conducted authentication on server side. - // This token can be used for impersonation. We use it to create a WindowsIdentity and hand it out to the server app. - Interop.SECURITY_STATUS winStatus = (Interop.SECURITY_STATUS)SSPIWrapper.QuerySecurityContextToken( - GlobalSSPI.SSPIAuth, - securityContext!, - out token); - if (winStatus != Interop.SECURITY_STATUS.OK) - { - throw new Win32Exception((int)winStatus); - } - string authtype = context.ProtocolName; - - // The following call was also specifying WindowsAccountType.Normal, true. - // WindowsIdentity.IsAuthenticated is no longer supported in .NET Core - result = new WindowsIdentity(token.DangerousGetHandle(), authtype); - return result; - } - catch (SecurityException) - { - // Ignore and construct generic Identity if failed due to security problem. - } - finally - { - token?.Dispose(); - } - } - - // On the client we don't have access to the remote side identity. - result = new GenericIdentity(name ?? string.Empty, protocol); - return result; - } - - internal static void ValidateImpersonationLevel(TokenImpersonationLevel impersonationLevel) - { - if (impersonationLevel != TokenImpersonationLevel.Identification && - impersonationLevel != TokenImpersonationLevel.Impersonation && - impersonationLevel != TokenImpersonationLevel.Delegation) - { - throw new ArgumentOutOfRangeException(nameof(impersonationLevel), impersonationLevel.ToString(), SR.net_auth_supported_impl_levels); - } - } - } -}