Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Implement server-side of NegotiateStream on Unix #36827

Merged
merged 17 commits into from
Apr 19, 2019
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,20 @@ internal static extern Status AcceptSecContext(
ref SafeGssContextHandle acceptContextHandle,
byte[] inputBytes,
int inputLength,
ref GssBuffer token);
ref GssBuffer token,
out uint retFlags);

[DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_DeleteSecContext")]
internal static extern Status DeleteSecContext(
out Status minorStatus,
ref IntPtr contextHandle);

[DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_GetUser")]
internal static extern Status GetUser(
out Status minorStatus,
SafeGssContextHandle acceptContextHandle,
ref GssBuffer token);

[DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_Wrap")]
private static extern Status Wrap(
out Status minorStatus,
Expand Down
3 changes: 2 additions & 1 deletion src/Common/src/System/Net/NTAuthentication.Common.cs
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,8 @@ internal byte[] GetOutgoingBlob(byte[] incomingBlob, bool throwOnError, out Secu
}

// The return value will tell us correctly if the handshake is over or not
if (statusCode.ErrorCode == SecurityStatusPalErrorCode.OK)
if (statusCode.ErrorCode == SecurityStatusPalErrorCode.OK
|| (_isServer && statusCode.ErrorCode == SecurityStatusPalErrorCode.CompleteNeeded))
{
// Success.
_isCompleted = true;
Expand Down
147 changes: 138 additions & 9 deletions src/Common/src/System/Net/Security/NegotiateStreamPal.Unix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
// See the LICENSE file in the project root for more information.

using System.IO;
using System.Security;
using System.Security.Principal;
using System.Threading;
using System.ComponentModel;
using System.Diagnostics;
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
Expand Down Expand Up @@ -124,8 +125,10 @@ private static bool GssInitSecurityContext(
// EstablishSecurityContext is called multiple times in a session.
// In each call, we need to pass the context handle from the previous call.
// For the first call, the context handle will be null.
bool newContext = false;
if (context == null)
{
newContext = true;
context = new SafeGssContextHandle();
}

Expand All @@ -152,6 +155,11 @@ private static bool GssInitSecurityContext(
if ((status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) &&
(status != Interop.NetSecurityNative.Status.GSS_S_CONTINUE_NEEDED))
{
if (newContext)
{
context.Dispose();
context = null;
}
throw new Interop.NetSecurityNative.GssApiException(status, minorStatus);
}

Expand All @@ -165,6 +173,78 @@ private static bool GssInitSecurityContext(
return status == Interop.NetSecurityNative.Status.GSS_S_COMPLETE;
}

private static bool GssAcceptSecurityContext(
ref SafeGssContextHandle context,
byte[] buffer,
out byte[] outputBuffer,
out uint outFlags)
{
bool newContext = false;
if (context == null)
{
newContext = true;
context = new SafeGssContextHandle();
}

Interop.NetSecurityNative.GssBuffer token = default(Interop.NetSecurityNative.GssBuffer);
Interop.NetSecurityNative.Status status;

try
{
Interop.NetSecurityNative.Status minorStatus;
status = Interop.NetSecurityNative.AcceptSecContext(out minorStatus,
ref context,
buffer,
buffer?.Length ?? 0,
ref token,
out outFlags);

if ((status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) &&
(status != Interop.NetSecurityNative.Status.GSS_S_CONTINUE_NEEDED))
{
if (newContext)
{
context.Dispose();
context = null;
}
throw new Interop.NetSecurityNative.GssApiException(status, minorStatus);
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
}

outputBuffer = token.ToByteArray();
}
finally
{
token.Dispose();
}

return status == Interop.NetSecurityNative.Status.GSS_S_COMPLETE;
}

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);
}

return Encoding.UTF8.GetString(token.ToByteArray());
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
}
finally
{
token.Dispose();
}
}

private static SecurityStatusPal EstablishSecurityContext(
SafeFreeNegoCredentials credential,
ref SafeDeleteContext context,
Expand Down Expand Up @@ -293,7 +373,61 @@ internal static SecurityStatusPal AcceptSecurityContext(
ref byte[] resultBlob,
ref ContextFlagsPal contextFlags)
{
throw new PlatformNotSupportedException(SR.net_nego_server_not_supported);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we delete the associated string from the .resx, or is it still needed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it can be deleted.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's still referenced in a few places.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at the places it's used. We can leave the string in the .resx for now. But I think we should be able to eventually fix the other PlatformNotSupportedException's later and remove the string.

if (securityContext == null)
{
securityContext = new SafeDeleteNegoContext((SafeFreeNegoCredentials)credentialsHandle);
}

SafeDeleteNegoContext negoContext = (SafeDeleteNegoContext)securityContext;
try
{
SafeGssContextHandle contextHandle = negoContext.GssContext;
bool done = GssAcceptSecurityContext(
ref contextHandle,
incomingBlob,
out resultBlob,
out uint outputFlags);

Debug.Assert(resultBlob != null, "Unexpected null buffer returned by GssApi");
Debug.Assert(negoContext.GssContext == null || contextHandle == negoContext.GssContext);

// Save the inner context handle for further calls to NetSecurity
Debug.Assert(negoContext.GssContext == null || contextHandle == negoContext.GssContext);
if (null == negoContext.GssContext)
{
negoContext.SetGssContext(contextHandle);
}

contextFlags = ContextFlagsAdapterPal.GetContextFlagsPalFromInterop(
(Interop.NetSecurityNative.GssFlags)outputFlags, isServer: true);

SecurityStatusPalErrorCode errorCode = done ?
(negoContext.IsNtlmUsed && resultBlob.Length > 0 ? SecurityStatusPalErrorCode.OK : SecurityStatusPalErrorCode.CompleteNeeded) :
SecurityStatusPalErrorCode.ContinueNeeded;

return new SecurityStatusPal(errorCode);
}
catch (Exception ex)
{
if (NetEventSource.IsEnabled) NetEventSource.Error(null, ex);
return new SecurityStatusPal(SecurityStatusPalErrorCode.InternalError, ex);
}
}

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.IsEnabled) NetEventSource.Error(null, ex);
throw;
}
}

internal static Win32Exception CreateExceptionFromError(SecurityStatusPal statusCode)
Expand All @@ -314,11 +448,6 @@ internal static SafeFreeCredentials AcquireDefaultCredential(string package, boo

internal static SafeFreeCredentials AcquireCredentialsHandle(string package, bool isServer, NetworkCredential credential)
{
if (isServer)
{
throw new PlatformNotSupportedException(SR.net_nego_server_not_supported);
}

bool isEmptyCredential = string.IsNullOrWhiteSpace(credential.UserName) ||
string.IsNullOrWhiteSpace(credential.Password);
bool ntlmOnly = string.Equals(package, NegotiationInfoClass.NTLM, StringComparison.OrdinalIgnoreCase);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,15 @@ public SafeGssContextHandle GssContext
get { return _context; }
}

public SafeDeleteNegoContext(SafeFreeNegoCredentials credential, string targetName)
public SafeDeleteNegoContext(SafeFreeNegoCredentials credential)
: base(credential)
{
Debug.Assert((null != credential), "Null credential in SafeDeleteNegoContext");
}

public SafeDeleteNegoContext(SafeFreeNegoCredentials credential, string targetName)
: this(credential)
{
try
{
// Convert any "SERVICE/HOST" style of targetName to use "SERVICE@HOST" style.
Expand Down
6 changes: 6 additions & 0 deletions src/Common/tests/System/Net/Capability.Security.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ public static bool IsDomainAvailable()
return !string.IsNullOrWhiteSpace(Configuration.Security.ActiveDirectoryName);
}

public static bool IsNegotiateClientAvailable()
{
return !(Configuration.Security.NegotiateClient == null)
&& !(Configuration.Security.NegotiateClientUser == null);
}

public static bool IsNegotiateServerAvailable()
{
return !(Configuration.Security.NegotiateServer == null);
Expand Down
4 changes: 4 additions & 0 deletions src/Common/tests/System/Net/Configuration.Security.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public static partial class Security

public static Uri NegotiateServer => GetUriValue("COREFX_NET_SECURITY_NEGOSERVERURI");

public static Uri NegotiateClient => GetUriValue("COREFX_NET_SECURITY_NEGOCLIENTURI");

public static string NegotiateClientUser => GetValue("COREFX_NET_SECURITY_NEGOCLIENTUSER");

public static string TlsRenegotiationServer => GetValue("COREFX_NET_SECURITY_TLSREGOTIATIONSERVER", "corefx-net-tls.azurewebsites.net");
Tratcher marked this conversation as resolved.
Show resolved Hide resolved

// This should be set if hostnames for all certificates within corefx-testdata have been set to point to 127.0.0.1.
Expand Down
18 changes: 12 additions & 6 deletions src/Common/tests/System/Threading/Tasks/TaskTimeoutExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,39 @@ public static async Task WithCancellation(this Task task, CancellationToken canc
}
}

public static async Task TimeoutAfter(this Task task, int millisecondsTimeout)
public static Task TimeoutAfter(this Task task, int millisecondsTimeout)
=> task.TimeoutAfter(TimeSpan.FromMilliseconds(millisecondsTimeout));

public static async Task TimeoutAfter(this Task task, TimeSpan timeout)
{
var cts = new CancellationTokenSource();

if (task == await Task.WhenAny(task, Task.Delay(millisecondsTimeout, cts.Token)).ConfigureAwait(false))
if (task == await Task.WhenAny(task, Task.Delay(timeout, cts.Token)).ConfigureAwait(false))
{
cts.Cancel();
await task.ConfigureAwait(false);
}
else
{
throw new TimeoutException($"Task timed out after {millisecondsTimeout}ms");
throw new TimeoutException($"Task timed out after {timeout}");
}
}

public static async Task<TResult> TimeoutAfter<TResult>(this Task<TResult> task, int millisecondsTimeout)
public static Task<TResult> TimeoutAfter<TResult>(this Task<TResult> task, int millisecondsTimeout)
=> task.TimeoutAfter(TimeSpan.FromMilliseconds(millisecondsTimeout));

public static async Task<TResult> TimeoutAfter<TResult>(this Task<TResult> task, TimeSpan timeout)
{
var cts = new CancellationTokenSource();

if (task == await Task<TResult>.WhenAny(task, Task<TResult>.Delay(millisecondsTimeout, cts.Token)).ConfigureAwait(false))
if (task == await Task<TResult>.WhenAny(task, Task<TResult>.Delay(timeout, cts.Token)).ConfigureAwait(false))
{
cts.Cancel();
return await task.ConfigureAwait(false);
}
else
{
throw new TimeoutException($"Task timed out after {millisecondsTimeout}ms");
throw new TimeoutException($"Task timed out after {timeout}");
}
}

Expand Down
43 changes: 41 additions & 2 deletions src/Native/Unix/System.Net.Security.Native/pal_gssapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,8 @@ uint32_t NetSecurityNative_AcceptSecContext(uint32_t* minorStatus,
GssCtxId** contextHandle,
uint8_t* inputBytes,
uint32_t inputLength,
PAL_GssBuffer* outBuffer)
PAL_GssBuffer* outBuffer,
uint32_t* retFlags)
{
assert(minorStatus != NULL);
assert(contextHandle != NULL);
Expand All @@ -266,14 +267,52 @@ uint32_t NetSecurityNative_AcceptSecContext(uint32_t* minorStatus,
NULL,
NULL,
&gssBuffer,
0,
retFlags,
NULL,
NULL);

NetSecurityNative_MoveBuffer(&gssBuffer, outBuffer);
return majorStatus;
}

uint32_t NetSecurityNative_GetUser(uint32_t* minorStatus,
GssCtxId* contextHandle,
PAL_GssBuffer* outBuffer)
{
assert(minorStatus != NULL);
assert(contextHandle != NULL);
assert(outBuffer != NULL);

gss_name_t srcName = GSS_C_NO_NAME;

uint32_t majorStatus = gss_inquire_context(minorStatus,
contextHandle,
&srcName,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL);

if (majorStatus == GSS_S_COMPLETE)
{
GssBuffer gssBuffer = {.length = 0, .value = NULL};
majorStatus = gss_display_name(minorStatus, srcName, &gssBuffer, NULL);
if (majorStatus == GSS_S_COMPLETE)
{
NetSecurityNative_MoveBuffer(&gssBuffer, outBuffer);
}
}

if (srcName != NULL)
{
majorStatus = gss_release_name(minorStatus, &srcName);
}

return majorStatus;
}

uint32_t NetSecurityNative_ReleaseCred(uint32_t* minorStatus, GssCredId** credHandle)
{
assert(minorStatus != NULL);
Expand Down
10 changes: 9 additions & 1 deletion src/Native/Unix/System.Net.Security.Native/pal_gssapi.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ DLLEXPORT uint32_t NetSecurityNative_AcceptSecContext(uint32_t* minorStatus,
GssCtxId** contextHandle,
uint8_t* inputBytes,
uint32_t inputLength,
PAL_GssBuffer* outBuffer);
PAL_GssBuffer* outBuffer,
uint32_t* retFlags);

/*

Expand Down Expand Up @@ -167,3 +168,10 @@ DLLEXPORT uint32_t NetSecurityNative_InitiateCredWithPassword(uint32_t* minorSta
Shims the gss_indicate_mechs method to detect if NTLM mech is installed.
*/
DLLEXPORT uint32_t NetSecurityNative_IsNtlmInstalled(void);

/*
Shims gss_inquire_context and gss_display_name to get the remote user principal name.
*/
DLLEXPORT uint32_t NetSecurityNative_GetUser(uint32_t* minorStatus,
GssCtxId* contextHandle,
PAL_GssBuffer* outBuffer);
Loading