From 18d611f9d423028deb5be2e51b99b5cbaef08f19 Mon Sep 17 00:00:00 2001 From: David Shulman Date: Mon, 11 Nov 2019 15:49:41 -0800 Subject: [PATCH] Fix NegotitateStream (server) on Linux for NTLM (dotnet/corefx#42522) This PR is a follow up to PR dotnet/corefx#36827 which added support for Linux server-side GSS-API (AcceptSecContext). This enabled NegotitateStream AuthenticateAsServer* support. It also provided support for ASP.NET Core to allow Kestrel server to have Negotiate authentication on Linux. This PR fixes some problems with Negotiate (SPNEGO) fallback from Kerberos to NTLM. Notably it passes in a correct GSS Acceptor credential so that fallback will work correctly. As part of fixing that, I noticed some other problems with returning the user-identity when NTLM is used. This was tested in a separate enterprise testing environment that I have created. It builds on technologies that we have started using like docker containers and Azure pipelines (e.g. HttpStress). The environment is currently here: https://dev.azure.com/systemnetncl/Enterprise%20Testing. The extra Kerberos tests and container support is here: https://github.com/davidsh/networkingtests When the repo merge is completed, I will work with the infra team to see what things can be merged back into the main repo/CI pipeline and migrate the test sources to an appropriate place in the new repo. Contributes to dotnet/corefx#10041 Contributes to dotnet/corefx#24707 Contributes to dotnet/corefx#30150 Commit migrated from https://github.com/dotnet/corefx/commit/1054f1fba1b7222c138dcd20d8da80cd893fdd01 --- .../Interop.GssBuffer.cs | 6 ++- .../Interop.NetSecurityNative.cs | 9 ++++- .../Microsoft/Win32/SafeHandles/GssSafeHandles.cs | 17 +++++++- .../src/System/Net/ContextFlagsAdapterPal.Unix.cs | 31 +++++++++++++- .../System/Net/Security/NegotiateStreamPal.Unix.cs | 47 +++++++++++++++++----- .../Net/Security/Unix/SafeDeleteNegoContext.cs | 16 ++++++++ .../Unix/System.Net.Security.Native/pal_gssapi.c | 39 ++++++++++++++++-- .../Unix/System.Net.Security.Native/pal_gssapi.h | 9 ++++- 8 files changed, 157 insertions(+), 17 deletions(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Net.Security.Native/Interop.GssBuffer.cs b/src/libraries/Common/src/Interop/Unix/System.Net.Security.Native/Interop.GssBuffer.cs index fc45761..7f2bbbe 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Net.Security.Native/Interop.GssBuffer.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Net.Security.Native/Interop.GssBuffer.cs @@ -12,7 +12,7 @@ internal static partial class Interop internal static partial class NetSecurityNative { [StructLayout(LayoutKind.Sequential)] - internal unsafe struct GssBuffer : IDisposable + internal struct GssBuffer : IDisposable { internal ulong _length; internal IntPtr _data; @@ -52,6 +52,10 @@ internal static partial class Interop return destination; } + internal unsafe ReadOnlySpan Span => (_data != IntPtr.Zero && _length != 0) ? + new ReadOnlySpan(_data.ToPointer(), checked((int)_length)) : + default; + public void Dispose() { if (_data != IntPtr.Zero) diff --git a/src/libraries/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs b/src/libraries/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs index c7f57e2..931787a 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs @@ -48,6 +48,11 @@ internal static partial class Interop out Status minorStatus, ref IntPtr inputName); + [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_AcquireAcceptorCred")] + internal static extern Status AcquireAcceptorCred( + out Status minorStatus, + out SafeGssCredHandle outputCredHandle); + [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_InitiateCredSpNego")] internal static extern Status InitiateCredSpNego( out Status minorStatus, @@ -101,11 +106,13 @@ internal static partial class Interop [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_AcceptSecContext")] internal static extern Status AcceptSecContext( out Status minorStatus, + SafeGssCredHandle acceptorCredHandle, ref SafeGssContextHandle acceptContextHandle, byte[] inputBytes, int inputLength, ref GssBuffer token, - out uint retFlags); + out uint retFlags, + out bool isNtlmUsed); [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_DeleteSecContext")] internal static extern Status DeleteSecContext( diff --git a/src/libraries/Common/src/Microsoft/Win32/SafeHandles/GssSafeHandles.cs b/src/libraries/Common/src/Microsoft/Win32/SafeHandles/GssSafeHandles.cs index aa3727c..ab22e43 100644 --- a/src/libraries/Common/src/Microsoft/Win32/SafeHandles/GssSafeHandles.cs +++ b/src/libraries/Common/src/Microsoft/Win32/SafeHandles/GssSafeHandles.cs @@ -74,6 +74,21 @@ namespace Microsoft.Win32.SafeHandles { private static readonly Lazy s_IsNtlmInstalled = new Lazy(InitIsNtlmInstalled); + public static SafeGssCredHandle CreateAcceptor() + { + SafeGssCredHandle retHandle = null; + Interop.NetSecurityNative.Status status; + Interop.NetSecurityNative.Status minorStatus; + + status = Interop.NetSecurityNative.AcquireAcceptorCred(out minorStatus, out retHandle); + if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) + { + throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); + } + + return retHandle; + } + /// /// returns the handle for the given credentials. /// The method returns an invalid handle if the username is null or empty. @@ -110,7 +125,7 @@ namespace Microsoft.Win32.SafeHandles if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) { retHandle.Dispose(); - throw new Interop.NetSecurityNative.GssApiException(status, minorStatus, null); + throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); } } diff --git a/src/libraries/Common/src/System/Net/ContextFlagsAdapterPal.Unix.cs b/src/libraries/Common/src/System/Net/ContextFlagsAdapterPal.Unix.cs index 599bd4a..77be489 100644 --- a/src/libraries/Common/src/System/Net/ContextFlagsAdapterPal.Unix.cs +++ b/src/libraries/Common/src/System/Net/ContextFlagsAdapterPal.Unix.cs @@ -23,7 +23,6 @@ namespace System.Net 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_IDENTIFY_FLAG, ContextFlagsPal.InitIdentify), 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), @@ -35,6 +34,20 @@ namespace System.Net { 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) { @@ -56,6 +69,22 @@ namespace System.Net { 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) { diff --git a/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.Unix.cs b/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.Unix.cs index 4278936..948ae79 100644 --- a/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.Unix.cs +++ b/src/libraries/Common/src/System/Net/Security/NegotiateStreamPal.Unix.cs @@ -187,10 +187,14 @@ namespace System.Net.Security private static bool GssAcceptSecurityContext( ref SafeGssContextHandle context, + SafeGssCredHandle credential, byte[] buffer, out byte[] outputBuffer, - out uint outFlags) + out uint outFlags, + out bool isNtlmUsed) { + Debug.Assert(credential != null); + bool newContext = false; if (context == null) { @@ -205,11 +209,13 @@ namespace System.Net.Security { Interop.NetSecurityNative.Status minorStatus; status = Interop.NetSecurityNative.AcceptSecContext(out minorStatus, + credential, ref context, buffer, buffer?.Length ?? 0, ref token, - out outFlags); + out outFlags, + out isNtlmUsed); if ((status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) && (status != Interop.NetSecurityNative.Status.GSS_S_CONTINUE_NEEDED)) @@ -249,7 +255,15 @@ namespace System.Net.Security throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); } - return Encoding.UTF8.GetString(token.ToByteArray()); + 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 { @@ -274,7 +288,7 @@ namespace System.Net.Security if (NetEventSource.IsEnabled) { string protocol = isNtlmOnly ? "NTLM" : "SPNEGO"; - NetEventSource.Info(null, $"requested protocol = {protocol}, target = {targetName}"); + NetEventSource.Info(context, $"requested protocol = {protocol}, target = {targetName}"); } context = new SafeDeleteNegoContext(credential, targetName); @@ -305,7 +319,7 @@ namespace System.Net.Security if (NetEventSource.IsEnabled) { string protocol = isNtlmOnly ? "NTLM" : isNtlmUsed ? "SPNEGO-NTLM" : "SPNEGO-Kerberos"; - NetEventSource.Info(null, $"actual protocol = {protocol}"); + NetEventSource.Info(context, $"actual protocol = {protocol}"); } // Populate protocol used for authentication @@ -396,9 +410,11 @@ namespace System.Net.Security SafeGssContextHandle contextHandle = negoContext.GssContext; bool done = GssAcceptSecurityContext( ref contextHandle, + negoContext.AcceptorCredential, incomingBlob, out resultBlob, - out uint outputFlags); + out uint outputFlags, + out bool isNtlmUsed); Debug.Assert(resultBlob != null, "Unexpected null buffer returned by GssApi"); Debug.Assert(negoContext.GssContext == null || contextHandle == negoContext.GssContext); @@ -413,9 +429,22 @@ namespace System.Net.Security contextFlags = ContextFlagsAdapterPal.GetContextFlagsPalFromInterop( (Interop.NetSecurityNative.GssFlags)outputFlags, isServer: true); - SecurityStatusPalErrorCode errorCode = done ? - (negoContext.IsNtlmUsed && resultBlob.Length > 0 ? SecurityStatusPalErrorCode.OK : SecurityStatusPalErrorCode.CompleteNeeded) : - SecurityStatusPalErrorCode.ContinueNeeded; + SecurityStatusPalErrorCode errorCode; + if (done) + { + if (NetEventSource.IsEnabled) + { + string protocol = isNtlmUsed ? "SPNEGO-NTLM" : "SPNEGO-Kerberos"; + NetEventSource.Info(securityContext, $"AcceptSecurityContext: actual protocol = {protocol}"); + } + + negoContext.SetAuthenticationPackage(isNtlmUsed); + errorCode = (isNtlmUsed && resultBlob.Length > 0) ? SecurityStatusPalErrorCode.OK : SecurityStatusPalErrorCode.CompleteNeeded; + } + else + { + errorCode = SecurityStatusPalErrorCode.ContinueNeeded; + } return new SecurityStatusPal(errorCode); } diff --git a/src/libraries/Common/src/System/Net/Security/Unix/SafeDeleteNegoContext.cs b/src/libraries/Common/src/System/Net/Security/Unix/SafeDeleteNegoContext.cs index 3b47a7f..1d55816 100644 --- a/src/libraries/Common/src/System/Net/Security/Unix/SafeDeleteNegoContext.cs +++ b/src/libraries/Common/src/System/Net/Security/Unix/SafeDeleteNegoContext.cs @@ -12,10 +12,20 @@ namespace System.Net.Security { internal sealed class SafeDeleteNegoContext : SafeDeleteContext { + private SafeGssCredHandle _acceptorCredential; private SafeGssNameHandle _targetName; private SafeGssContextHandle _context; private bool _isNtlmUsed; + public SafeGssCredHandle AcceptorCredential + { + get + { + _acceptorCredential ??= SafeGssCredHandle.CreateAcceptor(); + return _acceptorCredential; + } + } + public SafeGssNameHandle TargetName { get { return _targetName; } @@ -78,6 +88,12 @@ namespace System.Net.Security _targetName.Dispose(); _targetName = null; } + + if (_acceptorCredential != null) + { + _acceptorCredential.Dispose(); + _acceptorCredential = null; + } } base.Dispose(disposing); } diff --git a/src/libraries/Native/Unix/System.Net.Security.Native/pal_gssapi.c b/src/libraries/Native/Unix/System.Net.Security.Native/pal_gssapi.c index 041a23b..0fbd518 100644 --- a/src/libraries/Native/Unix/System.Net.Security.Native/pal_gssapi.c +++ b/src/libraries/Native/Unix/System.Net.Security.Native/pal_gssapi.c @@ -299,33 +299,53 @@ uint32_t NetSecurityNative_InitSecContextEx(uint32_t* minorStatus, } uint32_t NetSecurityNative_AcceptSecContext(uint32_t* minorStatus, + GssCredId* acceptorCredHandle, GssCtxId** contextHandle, uint8_t* inputBytes, uint32_t inputLength, PAL_GssBuffer* outBuffer, - uint32_t* retFlags) + uint32_t* retFlags, + int32_t* isNtlmUsed) { assert(minorStatus != NULL); + assert(acceptorCredHandle != NULL); assert(contextHandle != NULL); assert(inputBytes != NULL || inputLength == 0); assert(outBuffer != NULL); + assert(isNtlmUsed != NULL); // Note: *contextHandle is null only in the first call and non-null in the subsequent calls GssBuffer inputToken = {.length = inputLength, .value = inputBytes}; GssBuffer gssBuffer = {.length = 0, .value = NULL}; + gss_OID mechType = GSS_C_NO_OID; uint32_t majorStatus = gss_accept_sec_context(minorStatus, contextHandle, - GSS_C_NO_CREDENTIAL, + acceptorCredHandle, &inputToken, GSS_C_NO_CHANNEL_BINDINGS, NULL, - NULL, + &mechType, &gssBuffer, retFlags, NULL, NULL); +#if HAVE_GSS_SPNEGO_MECHANISM + gss_OID ntlmMech = GSS_NTLM_MECHANISM; +#else + gss_OID ntlmMech = &gss_mech_ntlm_OID_desc; +#endif + + *isNtlmUsed = (gss_oid_equal(mechType, ntlmMech) != 0) ? 1 : 0; + + // The gss_ntlmssp provider doesn't support impersonation or delegation but fails to set the GSS_C_IDENTIFY_FLAG + // flag. So, we'll set it here to keep the behavior consistent with Windows platform. + if (*isNtlmUsed == 1) + { + *retFlags |= GSS_C_IDENTIFY_FLAG; + } + NetSecurityNative_MoveBuffer(&gssBuffer, outBuffer); return majorStatus; } @@ -494,6 +514,19 @@ static uint32_t NetSecurityNative_AcquireCredWithPassword(uint32_t* minorStatus, return majorStatus; } +uint32_t NetSecurityNative_AcquireAcceptorCred(uint32_t* minorStatus, + GssCredId** outputCredHandle) +{ + return gss_acquire_cred(minorStatus, + GSS_C_NO_NAME, + GSS_C_INDEFINITE, + GSS_C_NO_OID_SET, + GSS_C_ACCEPT, + outputCredHandle, + NULL, + NULL); +} + uint32_t NetSecurityNative_InitiateCredWithPassword(uint32_t* minorStatus, int32_t isNtlm, GssName* desiredName, diff --git a/src/libraries/Native/Unix/System.Net.Security.Native/pal_gssapi.h b/src/libraries/Native/Unix/System.Net.Security.Native/pal_gssapi.h index 58a66626..58b9ef9 100644 --- a/src/libraries/Native/Unix/System.Net.Security.Native/pal_gssapi.h +++ b/src/libraries/Native/Unix/System.Net.Security.Native/pal_gssapi.h @@ -90,6 +90,11 @@ Shims the gss_release_name method. DLLEXPORT uint32_t NetSecurityNative_ReleaseName(uint32_t* minorStatus, GssName** inputName); /* +Shims the gss_acquire_cred method with GSS_C_ACCEPT. +*/ +DLLEXPORT uint32_t NetSecurityNative_AcquireAcceptorCred(uint32_t* minorStatus, GssCredId** outputCredHandle); + +/* Shims the gss_acquire_cred method with SPNEGO oids with GSS_C_INITIATE. */ DLLEXPORT uint32_t @@ -133,11 +138,13 @@ DLLEXPORT uint32_t NetSecurityNative_InitSecContextEx(uint32_t* minorStatus, Shims the gss_accept_sec_context method. */ DLLEXPORT uint32_t NetSecurityNative_AcceptSecContext(uint32_t* minorStatus, + GssCredId* acceptorCredHandle, GssCtxId** contextHandle, uint8_t* inputBytes, uint32_t inputLength, PAL_GssBuffer* outBuffer, - uint32_t* retFlags); + uint32_t* retFlags, + int32_t* isNtlmUsed); /* -- 2.7.4