Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 1054f1f

Browse files
authored
Fix NegotitateStream (server) on Linux for NTLM (#42522)
This PR is a follow up to PR #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 #10041 Contributes to #24707 Contributes to #30150
1 parent 7dcbdc0 commit 1054f1f

File tree

8 files changed

+157
-17
lines changed

8 files changed

+157
-17
lines changed

src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.GssBuffer.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ internal static partial class Interop
1212
internal static partial class NetSecurityNative
1313
{
1414
[StructLayout(LayoutKind.Sequential)]
15-
internal unsafe struct GssBuffer : IDisposable
15+
internal struct GssBuffer : IDisposable
1616
{
1717
internal ulong _length;
1818
internal IntPtr _data;
@@ -52,6 +52,10 @@ internal byte[] ToByteArray()
5252
return destination;
5353
}
5454

55+
internal unsafe ReadOnlySpan<byte> Span => (_data != IntPtr.Zero && _length != 0) ?
56+
new ReadOnlySpan<byte>(_data.ToPointer(), checked((int)_length)) :
57+
default;
58+
5559
public void Dispose()
5660
{
5761
if (_data != IntPtr.Zero)

src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ internal static extern Status ReleaseName(
4848
out Status minorStatus,
4949
ref IntPtr inputName);
5050

51+
[DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_AcquireAcceptorCred")]
52+
internal static extern Status AcquireAcceptorCred(
53+
out Status minorStatus,
54+
out SafeGssCredHandle outputCredHandle);
55+
5156
[DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_InitiateCredSpNego")]
5257
internal static extern Status InitiateCredSpNego(
5358
out Status minorStatus,
@@ -101,11 +106,13 @@ internal static extern Status InitSecContext(
101106
[DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_AcceptSecContext")]
102107
internal static extern Status AcceptSecContext(
103108
out Status minorStatus,
109+
SafeGssCredHandle acceptorCredHandle,
104110
ref SafeGssContextHandle acceptContextHandle,
105111
byte[] inputBytes,
106112
int inputLength,
107113
ref GssBuffer token,
108-
out uint retFlags);
114+
out uint retFlags,
115+
out bool isNtlmUsed);
109116

110117
[DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_DeleteSecContext")]
111118
internal static extern Status DeleteSecContext(

src/Common/src/Microsoft/Win32/SafeHandles/GssSafeHandles.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,21 @@ internal class SafeGssCredHandle : SafeHandle
7474
{
7575
private static readonly Lazy<bool> s_IsNtlmInstalled = new Lazy<bool>(InitIsNtlmInstalled);
7676

77+
public static SafeGssCredHandle CreateAcceptor()
78+
{
79+
SafeGssCredHandle retHandle = null;
80+
Interop.NetSecurityNative.Status status;
81+
Interop.NetSecurityNative.Status minorStatus;
82+
83+
status = Interop.NetSecurityNative.AcquireAcceptorCred(out minorStatus, out retHandle);
84+
if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE)
85+
{
86+
throw new Interop.NetSecurityNative.GssApiException(status, minorStatus);
87+
}
88+
89+
return retHandle;
90+
}
91+
7792
/// <summary>
7893
/// returns the handle for the given credentials.
7994
/// The method returns an invalid handle if the username is null or empty.
@@ -110,7 +125,7 @@ public static SafeGssCredHandle Create(string username, string password, bool is
110125
if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE)
111126
{
112127
retHandle.Dispose();
113-
throw new Interop.NetSecurityNative.GssApiException(status, minorStatus, null);
128+
throw new Interop.NetSecurityNative.GssApiException(status, minorStatus);
114129
}
115130
}
116131

src/Common/src/System/Net/ContextFlagsAdapterPal.Unix.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ public ContextFlagMapping(Interop.NetSecurityNative.GssFlags gssFlag, ContextFla
2323
private static readonly ContextFlagMapping[] s_contextFlagMapping = new[]
2424
{
2525
new ContextFlagMapping(Interop.NetSecurityNative.GssFlags.GSS_C_CONF_FLAG, ContextFlagsPal.Confidentiality),
26-
new ContextFlagMapping(Interop.NetSecurityNative.GssFlags.GSS_C_IDENTIFY_FLAG, ContextFlagsPal.InitIdentify),
2726
new ContextFlagMapping(Interop.NetSecurityNative.GssFlags.GSS_C_MUTUAL_FLAG, ContextFlagsPal.MutualAuth),
2827
new ContextFlagMapping(Interop.NetSecurityNative.GssFlags.GSS_C_REPLAY_FLAG, ContextFlagsPal.ReplayDetect),
2928
new ContextFlagMapping(Interop.NetSecurityNative.GssFlags.GSS_C_SEQUENCE_FLAG, ContextFlagsPal.SequenceDetect),
@@ -35,6 +34,20 @@ internal static ContextFlagsPal GetContextFlagsPalFromInterop(Interop.NetSecurit
3534
{
3635
ContextFlagsPal flags = ContextFlagsPal.None;
3736

37+
// GSS_C_IDENTIFY_FLAG is handled separately as its value can either be AcceptIdentify (used by server) or InitIdentify (used by client)
38+
if ((gssFlags & Interop.NetSecurityNative.GssFlags.GSS_C_IDENTIFY_FLAG) != 0)
39+
{
40+
flags |= isServer ? ContextFlagsPal.AcceptIdentify : ContextFlagsPal.InitIdentify;
41+
}
42+
43+
foreach (ContextFlagMapping mapping in s_contextFlagMapping)
44+
{
45+
if ((gssFlags & mapping.GssFlags) == mapping.GssFlags)
46+
{
47+
flags |= mapping.ContextFlag;
48+
}
49+
}
50+
3851
// GSS_C_INTEG_FLAG is handled separately as its value can either be AcceptIntegrity (used by server) or InitIntegrity (used by client)
3952
if ((gssFlags & Interop.NetSecurityNative.GssFlags.GSS_C_INTEG_FLAG) != 0)
4053
{
@@ -56,6 +69,22 @@ internal static Interop.NetSecurityNative.GssFlags GetInteropFromContextFlagsPal
5669
{
5770
Interop.NetSecurityNative.GssFlags gssFlags = 0;
5871

72+
// GSS_C_IDENTIFY_FLAG is set if either AcceptIdentify (used by server) or InitIdentify (used by client) is set
73+
if (isServer)
74+
{
75+
if ((flags & ContextFlagsPal.AcceptIdentify) != 0)
76+
{
77+
gssFlags |= Interop.NetSecurityNative.GssFlags.GSS_C_IDENTIFY_FLAG;
78+
}
79+
}
80+
else
81+
{
82+
if ((flags & ContextFlagsPal.InitIdentify) != 0)
83+
{
84+
gssFlags |= Interop.NetSecurityNative.GssFlags.GSS_C_IDENTIFY_FLAG;
85+
}
86+
}
87+
5988
// GSS_C_INTEG_FLAG is set if either AcceptIntegrity (used by server) or InitIntegrity (used by client) is set
6089
if (isServer)
6190
{

src/Common/src/System/Net/Security/NegotiateStreamPal.Unix.cs

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,14 @@ private static bool GssInitSecurityContext(
187187

188188
private static bool GssAcceptSecurityContext(
189189
ref SafeGssContextHandle context,
190+
SafeGssCredHandle credential,
190191
byte[] buffer,
191192
out byte[] outputBuffer,
192-
out uint outFlags)
193+
out uint outFlags,
194+
out bool isNtlmUsed)
193195
{
196+
Debug.Assert(credential != null);
197+
194198
bool newContext = false;
195199
if (context == null)
196200
{
@@ -205,11 +209,13 @@ private static bool GssAcceptSecurityContext(
205209
{
206210
Interop.NetSecurityNative.Status minorStatus;
207211
status = Interop.NetSecurityNative.AcceptSecContext(out minorStatus,
212+
credential,
208213
ref context,
209214
buffer,
210215
buffer?.Length ?? 0,
211216
ref token,
212-
out outFlags);
217+
out outFlags,
218+
out isNtlmUsed);
213219

214220
if ((status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) &&
215221
(status != Interop.NetSecurityNative.Status.GSS_S_CONTINUE_NEEDED))
@@ -249,7 +255,15 @@ Interop.NetSecurityNative.Status status
249255
throw new Interop.NetSecurityNative.GssApiException(status, minorStatus);
250256
}
251257

252-
return Encoding.UTF8.GetString(token.ToByteArray());
258+
ReadOnlySpan<byte> tokenBytes = token.Span;
259+
int length = tokenBytes.Length;
260+
if (length > 0 && tokenBytes[length - 1] == '\0')
261+
{
262+
// Some GSS-API providers (gss-ntlmssp) include the terminating null with strings, so skip that.
263+
tokenBytes = tokenBytes.Slice(0, length - 1);
264+
}
265+
266+
return Encoding.UTF8.GetString(tokenBytes);
253267
}
254268
finally
255269
{
@@ -274,7 +288,7 @@ private static SecurityStatusPal EstablishSecurityContext(
274288
if (NetEventSource.IsEnabled)
275289
{
276290
string protocol = isNtlmOnly ? "NTLM" : "SPNEGO";
277-
NetEventSource.Info(null, $"requested protocol = {protocol}, target = {targetName}");
291+
NetEventSource.Info(context, $"requested protocol = {protocol}, target = {targetName}");
278292
}
279293

280294
context = new SafeDeleteNegoContext(credential, targetName);
@@ -305,7 +319,7 @@ private static SecurityStatusPal EstablishSecurityContext(
305319
if (NetEventSource.IsEnabled)
306320
{
307321
string protocol = isNtlmOnly ? "NTLM" : isNtlmUsed ? "SPNEGO-NTLM" : "SPNEGO-Kerberos";
308-
NetEventSource.Info(null, $"actual protocol = {protocol}");
322+
NetEventSource.Info(context, $"actual protocol = {protocol}");
309323
}
310324

311325
// Populate protocol used for authentication
@@ -396,9 +410,11 @@ internal static SecurityStatusPal AcceptSecurityContext(
396410
SafeGssContextHandle contextHandle = negoContext.GssContext;
397411
bool done = GssAcceptSecurityContext(
398412
ref contextHandle,
413+
negoContext.AcceptorCredential,
399414
incomingBlob,
400415
out resultBlob,
401-
out uint outputFlags);
416+
out uint outputFlags,
417+
out bool isNtlmUsed);
402418

403419
Debug.Assert(resultBlob != null, "Unexpected null buffer returned by GssApi");
404420
Debug.Assert(negoContext.GssContext == null || contextHandle == negoContext.GssContext);
@@ -413,9 +429,22 @@ internal static SecurityStatusPal AcceptSecurityContext(
413429
contextFlags = ContextFlagsAdapterPal.GetContextFlagsPalFromInterop(
414430
(Interop.NetSecurityNative.GssFlags)outputFlags, isServer: true);
415431

416-
SecurityStatusPalErrorCode errorCode = done ?
417-
(negoContext.IsNtlmUsed && resultBlob.Length > 0 ? SecurityStatusPalErrorCode.OK : SecurityStatusPalErrorCode.CompleteNeeded) :
418-
SecurityStatusPalErrorCode.ContinueNeeded;
432+
SecurityStatusPalErrorCode errorCode;
433+
if (done)
434+
{
435+
if (NetEventSource.IsEnabled)
436+
{
437+
string protocol = isNtlmUsed ? "SPNEGO-NTLM" : "SPNEGO-Kerberos";
438+
NetEventSource.Info(securityContext, $"AcceptSecurityContext: actual protocol = {protocol}");
439+
}
440+
441+
negoContext.SetAuthenticationPackage(isNtlmUsed);
442+
errorCode = (isNtlmUsed && resultBlob.Length > 0) ? SecurityStatusPalErrorCode.OK : SecurityStatusPalErrorCode.CompleteNeeded;
443+
}
444+
else
445+
{
446+
errorCode = SecurityStatusPalErrorCode.ContinueNeeded;
447+
}
419448

420449
return new SecurityStatusPal(errorCode);
421450
}

src/Common/src/System/Net/Security/Unix/SafeDeleteNegoContext.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,20 @@ namespace System.Net.Security
1212
{
1313
internal sealed class SafeDeleteNegoContext : SafeDeleteContext
1414
{
15+
private SafeGssCredHandle _acceptorCredential;
1516
private SafeGssNameHandle _targetName;
1617
private SafeGssContextHandle _context;
1718
private bool _isNtlmUsed;
1819

20+
public SafeGssCredHandle AcceptorCredential
21+
{
22+
get
23+
{
24+
_acceptorCredential ??= SafeGssCredHandle.CreateAcceptor();
25+
return _acceptorCredential;
26+
}
27+
}
28+
1929
public SafeGssNameHandle TargetName
2030
{
2131
get { return _targetName; }
@@ -78,6 +88,12 @@ protected override void Dispose(bool disposing)
7888
_targetName.Dispose();
7989
_targetName = null;
8090
}
91+
92+
if (_acceptorCredential != null)
93+
{
94+
_acceptorCredential.Dispose();
95+
_acceptorCredential = null;
96+
}
8197
}
8298
base.Dispose(disposing);
8399
}

src/Native/Unix/System.Net.Security.Native/pal_gssapi.c

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,33 +299,53 @@ uint32_t NetSecurityNative_InitSecContextEx(uint32_t* minorStatus,
299299
}
300300

301301
uint32_t NetSecurityNative_AcceptSecContext(uint32_t* minorStatus,
302+
GssCredId* acceptorCredHandle,
302303
GssCtxId** contextHandle,
303304
uint8_t* inputBytes,
304305
uint32_t inputLength,
305306
PAL_GssBuffer* outBuffer,
306-
uint32_t* retFlags)
307+
uint32_t* retFlags,
308+
int32_t* isNtlmUsed)
307309
{
308310
assert(minorStatus != NULL);
311+
assert(acceptorCredHandle != NULL);
309312
assert(contextHandle != NULL);
310313
assert(inputBytes != NULL || inputLength == 0);
311314
assert(outBuffer != NULL);
315+
assert(isNtlmUsed != NULL);
312316
// Note: *contextHandle is null only in the first call and non-null in the subsequent calls
313317

314318
GssBuffer inputToken = {.length = inputLength, .value = inputBytes};
315319
GssBuffer gssBuffer = {.length = 0, .value = NULL};
316320

321+
gss_OID mechType = GSS_C_NO_OID;
317322
uint32_t majorStatus = gss_accept_sec_context(minorStatus,
318323
contextHandle,
319-
GSS_C_NO_CREDENTIAL,
324+
acceptorCredHandle,
320325
&inputToken,
321326
GSS_C_NO_CHANNEL_BINDINGS,
322327
NULL,
323-
NULL,
328+
&mechType,
324329
&gssBuffer,
325330
retFlags,
326331
NULL,
327332
NULL);
328333

334+
#if HAVE_GSS_SPNEGO_MECHANISM
335+
gss_OID ntlmMech = GSS_NTLM_MECHANISM;
336+
#else
337+
gss_OID ntlmMech = &gss_mech_ntlm_OID_desc;
338+
#endif
339+
340+
*isNtlmUsed = (gss_oid_equal(mechType, ntlmMech) != 0) ? 1 : 0;
341+
342+
// The gss_ntlmssp provider doesn't support impersonation or delegation but fails to set the GSS_C_IDENTIFY_FLAG
343+
// flag. So, we'll set it here to keep the behavior consistent with Windows platform.
344+
if (*isNtlmUsed == 1)
345+
{
346+
*retFlags |= GSS_C_IDENTIFY_FLAG;
347+
}
348+
329349
NetSecurityNative_MoveBuffer(&gssBuffer, outBuffer);
330350
return majorStatus;
331351
}
@@ -494,6 +514,19 @@ static uint32_t NetSecurityNative_AcquireCredWithPassword(uint32_t* minorStatus,
494514
return majorStatus;
495515
}
496516

517+
uint32_t NetSecurityNative_AcquireAcceptorCred(uint32_t* minorStatus,
518+
GssCredId** outputCredHandle)
519+
{
520+
return gss_acquire_cred(minorStatus,
521+
GSS_C_NO_NAME,
522+
GSS_C_INDEFINITE,
523+
GSS_C_NO_OID_SET,
524+
GSS_C_ACCEPT,
525+
outputCredHandle,
526+
NULL,
527+
NULL);
528+
}
529+
497530
uint32_t NetSecurityNative_InitiateCredWithPassword(uint32_t* minorStatus,
498531
int32_t isNtlm,
499532
GssName* desiredName,

src/Native/Unix/System.Net.Security.Native/pal_gssapi.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ Shims the gss_release_name method.
8989
*/
9090
DLLEXPORT uint32_t NetSecurityNative_ReleaseName(uint32_t* minorStatus, GssName** inputName);
9191

92+
/*
93+
Shims the gss_acquire_cred method with GSS_C_ACCEPT.
94+
*/
95+
DLLEXPORT uint32_t NetSecurityNative_AcquireAcceptorCred(uint32_t* minorStatus, GssCredId** outputCredHandle);
96+
9297
/*
9398
Shims the gss_acquire_cred method with SPNEGO oids with GSS_C_INITIATE.
9499
*/
@@ -133,11 +138,13 @@ DLLEXPORT uint32_t NetSecurityNative_InitSecContextEx(uint32_t* minorStatus,
133138
Shims the gss_accept_sec_context method.
134139
*/
135140
DLLEXPORT uint32_t NetSecurityNative_AcceptSecContext(uint32_t* minorStatus,
141+
GssCredId* acceptorCredHandle,
136142
GssCtxId** contextHandle,
137143
uint8_t* inputBytes,
138144
uint32_t inputLength,
139145
PAL_GssBuffer* outBuffer,
140-
uint32_t* retFlags);
146+
uint32_t* retFlags,
147+
int32_t* isNtlmUsed);
141148

142149
/*
143150

0 commit comments

Comments
 (0)