From facff9e8d04d9e7ddf53eabea365306266f48865 Mon Sep 17 00:00:00 2001 From: Hemanth Kapila Date: Fri, 26 Feb 2016 17:17:28 +0530 Subject: [PATCH] Enable NegotiateStream For Unix Enabling NegotiateStream for Unix using native gssapi implementation. The native libraries used are mit-krb5 on linux and the built-in GSS.Framework for OSX. Also included are the tests which rely on configuring and deploying KDC on the host machine. --- .../src/Interop/Unix/Interop.Libraries.cs | 1 + .../Interop.GssApi.cs | 128 +++++ .../Interop.GssApiException.cs | 63 +++ .../Interop.GssBuffer.cs | 67 +++ .../Interop.NetSecurityNative.cs | 170 ++++++ .../SecuritySafeHandles.cs | 97 ++++ .../Unix/libssl/SecuritySafeHandles.cs | 89 ++-- .../Windows/sspicli/NegotiationInfoClass.cs | 10 +- .../Win32/SafeHandles/GssSafeHandles.cs | 149 ++++++ .../Generic/BidirectionalDictionary.cs | 52 ++ src/Native/CMakeLists.txt | 1 + src/Native/Common/pal_config.h.in | 2 + .../System.Net.Security.Native/CMakeLists.txt | 30 ++ .../System.Net.Security.Native/pal_gssapi.cpp | 320 ++++++++++++ .../System.Net.Security.Native/pal_gssapi.h | 151 ++++++ src/Native/configure.cmake | 16 + ...runtime.native.System.Net.Security.pkgproj | 19 + ...runtime.native.System.Net.Security.pkgproj | 19 + ...runtime.native.System.Net.Security.pkgproj | 19 + .../runtime.native.System.Net.Security.builds | 28 + ...runtime.native.System.Net.Security.pkgproj | 27 + ...runtime.native.System.Net.Security.pkgproj | 19 + .../pkg/System.Net.Security.pkgproj | 3 + .../src/Resources/Strings.resx | 31 +- .../src/System.Net.Security.csproj | 37 +- .../Net/CertificateValidationPal.Unix.cs | 6 +- .../System/Net/ContextFlagsAdapterPal.Unix.cs | 63 +++ .../Net/ContextFlagsAdapterPal.Windows.cs | 77 +++ .../src/System/Net/ContextFlagsPal.cs | 35 ++ .../src/System/Net/NTAuthentication.cs | 400 ++------------ .../src/System/Net/NegotiateStreamPal.Unix.cs | 298 +++++++++++ .../System/Net/NegotiateStreamPal.Windows.cs | 458 ++++++++++++++++ .../src/System/Net/NegotiationInfoClass.cs | 14 + .../Net/SecureProtocols/NegoState.Unix.cs | 158 ------ .../{NegoState.Windows.cs => NegoState.cs} | 160 +++--- .../Net/SecurityStatusAdapterPal.Windows.cs | 82 +++ .../src/System/Net/SslStreamPal.Unix.cs | 14 +- .../src/System/Net/SslStreamPal.Windows.cs | 196 +------ .../tests/FunctionalTests/KerberosTest.cs | 490 ++++++++++++++++++ .../NegotiateStreamStreamToStreamTest.cs | 7 - .../FunctionalTests/Resources/Strings.resx | 138 +++++ .../System.Net.Security.Tests.csproj | 51 +- .../FunctionalTests/TestConfiguration.cs | 6 + .../UnixGssFakeNegotiateStream.cs | 166 ++++++ .../UnixGssFakeStreamFramer.cs | 108 ++++ .../tests/Scripts/kdc.conf | 16 + .../tests/Scripts/kdc.conf.centos | 11 + .../tests/Scripts/kdc.conf.opensuse | 18 + .../tests/Scripts/kdc.conf.ubuntu | 16 + .../tests/Scripts/krb5.conf | 12 + .../tests/Scripts/setup-kdc.sh | 276 ++++++++++ 51 files changed, 3952 insertions(+), 872 deletions(-) create mode 100644 src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.GssApi.cs create mode 100644 src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.GssApiException.cs create mode 100644 src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.GssBuffer.cs create mode 100644 src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs create mode 100644 src/Common/src/Interop/Unix/System.Net.Security.Native/SecuritySafeHandles.cs create mode 100644 src/Common/src/Microsoft/Win32/SafeHandles/GssSafeHandles.cs create mode 100644 src/Common/src/System/Collections/Generic/BidirectionalDictionary.cs create mode 100644 src/Native/System.Net.Security.Native/CMakeLists.txt create mode 100644 src/Native/System.Net.Security.Native/pal_gssapi.cpp create mode 100644 src/Native/System.Net.Security.Native/pal_gssapi.h create mode 100644 src/Native/pkg/runtime.native.System.Net.Security/debian/runtime.native.System.Net.Security.pkgproj create mode 100644 src/Native/pkg/runtime.native.System.Net.Security/osx/runtime.native.System.Net.Security.pkgproj create mode 100644 src/Native/pkg/runtime.native.System.Net.Security/rhel/runtime.native.System.Net.Security.pkgproj create mode 100644 src/Native/pkg/runtime.native.System.Net.Security/runtime.native.System.Net.Security.builds create mode 100644 src/Native/pkg/runtime.native.System.Net.Security/runtime.native.System.Net.Security.pkgproj create mode 100644 src/Native/pkg/runtime.native.System.Net.Security/ubuntu/runtime.native.System.Net.Security.pkgproj create mode 100644 src/System.Net.Security/src/System/Net/ContextFlagsAdapterPal.Unix.cs create mode 100644 src/System.Net.Security/src/System/Net/ContextFlagsAdapterPal.Windows.cs create mode 100644 src/System.Net.Security/src/System/Net/ContextFlagsPal.cs create mode 100644 src/System.Net.Security/src/System/Net/NegotiateStreamPal.Unix.cs create mode 100644 src/System.Net.Security/src/System/Net/NegotiateStreamPal.Windows.cs create mode 100644 src/System.Net.Security/src/System/Net/NegotiationInfoClass.cs delete mode 100644 src/System.Net.Security/src/System/Net/SecureProtocols/NegoState.Unix.cs rename src/System.Net.Security/src/System/Net/SecureProtocols/{NegoState.Windows.cs => NegoState.cs} (85%) create mode 100644 src/System.Net.Security/src/System/Net/SecurityStatusAdapterPal.Windows.cs create mode 100644 src/System.Net.Security/tests/FunctionalTests/KerberosTest.cs create mode 100644 src/System.Net.Security/tests/FunctionalTests/Resources/Strings.resx create mode 100644 src/System.Net.Security/tests/FunctionalTests/UnixGssFakeNegotiateStream.cs create mode 100644 src/System.Net.Security/tests/FunctionalTests/UnixGssFakeStreamFramer.cs create mode 100644 src/System.Net.Security/tests/Scripts/kdc.conf create mode 100644 src/System.Net.Security/tests/Scripts/kdc.conf.centos create mode 100644 src/System.Net.Security/tests/Scripts/kdc.conf.opensuse create mode 100644 src/System.Net.Security/tests/Scripts/kdc.conf.ubuntu create mode 100644 src/System.Net.Security/tests/Scripts/krb5.conf create mode 100755 src/System.Net.Security/tests/Scripts/setup-kdc.sh diff --git a/src/Common/src/Interop/Unix/Interop.Libraries.cs b/src/Common/src/Interop/Unix/Interop.Libraries.cs index 1a297d2310c8..cbe8fc1f5a8f 100644 --- a/src/Common/src/Interop/Unix/Interop.Libraries.cs +++ b/src/Common/src/Interop/Unix/Interop.Libraries.cs @@ -9,6 +9,7 @@ private static partial class Libraries // Shims internal const string SystemNative = "System.Native"; internal const string HttpNative = "System.Net.Http.Native"; + internal const string NetSecurityNative = "System.Net.Security.Native"; internal const string CryptoNative = "System.Security.Cryptography.Native"; internal const string GlobalizationNative = "System.Globalization.Native"; internal const string CompressionNative = "System.IO.Compression.Native"; diff --git a/src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.GssApi.cs b/src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.GssApi.cs new file mode 100644 index 000000000000..9ea4f217aa65 --- /dev/null +++ b/src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.GssApi.cs @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + internal static class GssApi + { + internal static bool EstablishSecurityContext( + ref SafeGssContextHandle context, + SafeGssCredHandle credential, + bool isNtlm, + SafeGssNameHandle targetName, + Interop.NetSecurityNative.GssFlags inFlags, + byte[] buffer, + out byte[] outputBuffer, + out uint outFlags) + { + outputBuffer = null; + outFlags = 0; + + // 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. + if (context == null) + { + context = new SafeGssContextHandle(); + } + + Interop.NetSecurityNative.GssBuffer token = default(Interop.NetSecurityNative.GssBuffer); + Interop.NetSecurityNative.Status status; + + try + { + Interop.NetSecurityNative.Status minorStatus; + status = NetSecurityNative.InitSecContext(out minorStatus, + credential, + ref context, + isNtlm, + targetName, + (uint)inFlags, + buffer, + (buffer == null) ? 0 : buffer.Length, + ref token, + out outFlags); + + if ((status != NetSecurityNative.Status.GSS_S_COMPLETE) && (status != NetSecurityNative.Status.GSS_S_CONTINUE_NEEDED)) + { + throw new NetSecurityNative.GssApiException(status, minorStatus); + } + + outputBuffer = token.ToByteArray(); + } + finally + { + token.Dispose(); + } + + return status == NetSecurityNative.Status.GSS_S_COMPLETE; + } + + internal static byte[] Encrypt( + SafeGssContextHandle context, + bool encrypt, + byte[] buffer, + int offset, + int count) + { + Debug.Assert((buffer != null) && (buffer.Length > 0), "Invalid input buffer passed to Encrypt"); + Debug.Assert((offset >= 0) && (offset < buffer.Length), "Invalid input offset passed to Encrypt"); + Debug.Assert((count >= 0) && (count <= (buffer.Length - offset)), "Invalid input count passed to Encrypt"); + + Interop.NetSecurityNative.GssBuffer encryptedBuffer = default(Interop.NetSecurityNative.GssBuffer); + try + { + + NetSecurityNative.Status minorStatus; + NetSecurityNative.Status status = NetSecurityNative.WrapBuffer(out minorStatus, context, encrypt, buffer, offset, count, ref encryptedBuffer); + if (status != NetSecurityNative.Status.GSS_S_COMPLETE) + { + throw new NetSecurityNative.GssApiException(status, minorStatus); + } + + return encryptedBuffer.ToByteArray(); + } + finally + { + encryptedBuffer.Dispose(); + } + } + + internal static int Decrypt( + SafeGssContextHandle context, + byte[] buffer, + int offset, + int count) + { + Debug.Assert((buffer != null) && (buffer.Length > 0), "Invalid input buffer passed to Decrypt"); + Debug.Assert((offset >= 0) && (offset <= buffer.Length), "Invalid input offset passed to Decrypt"); + Debug.Assert((count >= 0) && (count <= (buffer.Length - offset)), "Invalid input count passed to Decrypt"); + + Interop.NetSecurityNative.GssBuffer decryptedBuffer = default(Interop.NetSecurityNative.GssBuffer); + try + { + NetSecurityNative.Status minorStatus; + NetSecurityNative.Status status = NetSecurityNative.UnwrapBuffer(out minorStatus, context, buffer, offset, count, ref decryptedBuffer); + if (status != NetSecurityNative.Status.GSS_S_COMPLETE) + { + throw new NetSecurityNative.GssApiException(status, minorStatus); + } + + return decryptedBuffer.Copy(buffer, offset); + } + finally + { + decryptedBuffer.Dispose(); + } + } + } +} + + diff --git a/src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.GssApiException.cs b/src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.GssApiException.cs new file mode 100644 index 000000000000..985a971dd4fa --- /dev/null +++ b/src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.GssApiException.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class NetSecurityNative + { + internal sealed class GssApiException : Exception + { + private readonly Status _minorStatus; + + public Status MinorStatus + { + get { return _minorStatus;} + } + + public GssApiException(string message) : base(message) + { + } + + public GssApiException(Status majorStatus, Status minorStatus) + : base(GetGssApiDisplayStatus(majorStatus, minorStatus)) + { + HResult = (int)majorStatus; + _minorStatus = minorStatus; + } + + private static string GetGssApiDisplayStatus(Status majorStatus, Status minorStatus) + { + string majorError = GetGssApiDisplayStatus(majorStatus, isMinor: false); + string minorError = GetGssApiDisplayStatus(minorStatus, isMinor: true); + + return (majorError != null && minorError != null) ? + SR.Format(SR.net_gssapi_operation_failed_detailed, majorError, minorError) : + SR.Format(SR.net_gssapi_operation_failed, majorStatus.ToString("x"), minorStatus.ToString("x")); + } + + private static string GetGssApiDisplayStatus(Status status, bool isMinor) + { + GssBuffer displayBuffer = default(GssBuffer); + + try + { + Interop.NetSecurityNative.Status minStat; + Interop.NetSecurityNative.Status displayCallStatus = isMinor ? + DisplayMinorStatus(out minStat, status, ref displayBuffer): + DisplayMajorStatus(out minStat, status, ref displayBuffer); + return (Status.GSS_S_COMPLETE != displayCallStatus) ? null : Marshal.PtrToStringAnsi(displayBuffer._data); + } + finally + { + displayBuffer.Dispose(); + } + } + } + } +} diff --git a/src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.GssBuffer.cs b/src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.GssBuffer.cs new file mode 100644 index 000000000000..c6a98c868dec --- /dev/null +++ b/src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.GssBuffer.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + internal static partial class NetSecurityNative + { + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct GssBuffer : IDisposable + { + internal UInt64 _length; + internal IntPtr _data; + + internal int Copy(byte[] destination, int offset) + { + Debug.Assert(destination != null, "target destination cannot be null"); + Debug.Assert((offset >= 0 && offset < destination.Length) || destination.Length == 0, "invalid offset " + offset); + + if (_data == IntPtr.Zero || _length == 0) + { + return 0; + } + + // Using Convert.ToInt32 to throw an exception in the unlikely event of too large value of _length + int sourceLength = Convert.ToInt32(_length); + int destinationAvailable = destination.Length - offset; // amount of space in the given buffer + if (sourceLength > destinationAvailable) + { + throw new NetSecurityNative.GssApiException(SR.Format(SR.net_context_buffer_too_small, sourceLength, destinationAvailable)); + } + + Marshal.Copy(_data, destination, offset, sourceLength); + return sourceLength; + } + + internal byte[] ToByteArray() + { + if (_data == IntPtr.Zero || _length == 0) + { + return Array.Empty(); + } + + int destinationLength = Convert.ToInt32(_length); + byte[] destination = new byte[destinationLength]; + Marshal.Copy(_data, destination, 0, destinationLength); + return destination; + } + + public void Dispose() + { + if (_data != IntPtr.Zero) + { + Interop.NetSecurityNative.ReleaseGssBuffer(_data, _length); + _data = IntPtr.Zero; + } + + _length = 0; + } + } + } +} diff --git a/src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs b/src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs new file mode 100644 index 000000000000..654e40b4bfdb --- /dev/null +++ b/src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs @@ -0,0 +1,170 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + internal static partial class NetSecurityNative + { + [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_ReleaseGssBuffer")] + internal static extern void ReleaseGssBuffer( + IntPtr bufferPtr, + UInt64 length); + + [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_DisplayMinorStatus")] + internal static extern Status DisplayMinorStatus( + out Status minorStatus, + Status statusValue, + ref GssBuffer buffer); + + [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_DisplayMajorStatus")] + internal static extern Status DisplayMajorStatus( + out Status minorStatus, + Status statusValue, + ref GssBuffer buffer); + + [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_ImportUserName")] + internal static extern Status ImportUserName( + out Status minorStatus, + string inputName, + int inputNameByteCount, + out SafeGssNameHandle outputName); + + [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_ImportPrincipalName")] + internal static extern Status ImportPrincipalName( + out Status minorStatus, + string inputName, + int inputNameByteCount, + out SafeGssNameHandle outputName); + + [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_ReleaseName")] + internal static extern Status ReleaseName( + out Status minorStatus, + ref IntPtr inputName); + + [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_InitiateCredSpNego")] + internal static extern Status InitiateCredSpNego( + out Status minorStatus, + SafeGssNameHandle desiredName, + out SafeGssCredHandle outputCredHandle); + + [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_InitiateCredWithPassword")] + internal static extern Status InitiateCredWithPassword( + out Status minorStatus, + SafeGssNameHandle desiredName, + string password, + int passwordLen, + out SafeGssCredHandle outputCredHandle); + + [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_ReleaseCred")] + internal static extern Status ReleaseCred( + out Status minorStatus, + ref IntPtr credHandle); + + [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_InitSecContext")] + internal static extern Status InitSecContext( + out Status minorStatus, + SafeGssCredHandle initiatorCredHandle, + ref SafeGssContextHandle contextHandle, + bool isNtlm, + SafeGssNameHandle targetName, + uint reqFlags, + byte[] inputBytes, + int inputLength, + ref GssBuffer token, + out uint retFlags); + + [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_AcceptSecContext")] + internal static extern Status AcceptSecContext( + out Status minorStatus, + ref SafeGssContextHandle acceptContextHandle, + byte[] inputBytes, + int inputLength, + ref GssBuffer token); + + [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_DeleteSecContext")] + internal static extern Status DeleteSecContext( + out Status minorStatus, + ref IntPtr contextHandle); + + [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_Wrap")] + private static extern Status Wrap( + out Status minorStatus, + SafeGssContextHandle contextHandle, + bool isEncrypt, + byte[] inputBytes, + int offset, + int count, + ref GssBuffer outBuffer); + + [DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_Unwrap")] + private static extern Status Unwrap( + out Status minorStatus, + SafeGssContextHandle contextHandle, + byte[] inputBytes, + int offset, + int count, + ref GssBuffer outBuffer); + + internal static Status WrapBuffer( + out Status minorStatus, + SafeGssContextHandle contextHandle, + bool isEncrypt, + byte[] inputBytes, + int offset, + int count, + ref GssBuffer outBuffer) + { + Debug.Assert(inputBytes != null, "inputBytes must be valid value"); + Debug.Assert(offset >= 0 && offset <= inputBytes.Length, "offset must be valid"); + Debug.Assert(count >= 0 && count <= (inputBytes.Length - offset), "count must be valid"); + + return Wrap(out minorStatus, contextHandle, isEncrypt, inputBytes, offset, count, ref outBuffer); + } + + internal static Status UnwrapBuffer( + out Status minorStatus, + SafeGssContextHandle contextHandle, + byte[] inputBytes, + int offset, + int count, + ref GssBuffer outBuffer) + { + Debug.Assert(inputBytes != null, "inputBytes must be valid value"); + Debug.Assert(offset >= 0 && offset <= inputBytes.Length, "offset must be valid"); + Debug.Assert(count >= 0 && count <= inputBytes.Length, "count must be valid"); + + return Unwrap(out minorStatus, contextHandle, inputBytes, offset, count, ref outBuffer); + } + + internal enum Status : uint + { + GSS_S_COMPLETE = 0, + GSS_S_CONTINUE_NEEDED = 1 + } + + [Flags] + internal enum GssFlags : uint + { + GSS_C_DELEG_FLAG = 0x1, + GSS_C_MUTUAL_FLAG = 0x2, + GSS_C_REPLAY_FLAG = 0x4, + GSS_C_SEQUENCE_FLAG = 0x8, + GSS_C_CONF_FLAG = 0x10, + GSS_C_INTEG_FLAG = 0x20, + GSS_C_ANON_FLAG = 0x40, + GSS_C_PROT_READY_FLAG = 0x80, + GSS_C_TRANS_FLAG = 0x100, + GSS_C_DCE_STYLE = 0x1000, + GSS_C_IDENTIFY_FLAG = 0x2000, + GSS_C_EXTENDED_ERROR_FLAG = 0x4000, + GSS_C_DELEG_POLICY_FLAG = 0x8000 + } + } +} diff --git a/src/Common/src/Interop/Unix/System.Net.Security.Native/SecuritySafeHandles.cs b/src/Common/src/Interop/Unix/System.Net.Security.Native/SecuritySafeHandles.cs new file mode 100644 index 000000000000..fe87454c816e --- /dev/null +++ b/src/Common/src/Interop/Unix/System.Net.Security.Native/SecuritySafeHandles.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +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; + + public SafeGssCredHandle GssCredential + { + get { return _credential; } + } + + public SafeFreeNegoCredentials(string username, string password, string domain) : base(IntPtr.Zero, true) + { + bool ignore = false; + _credential = SafeGssCredHandle.Create(username, password, domain); + _credential.DangerousAddRef(ref ignore); + } + + public override bool IsInvalid + { + get { return (null == _credential); } + } + + protected override bool ReleaseHandle() + { + _credential.DangerousRelease(); + _credential = null; + return true; + } + } + + internal sealed class SafeDeleteNegoContext : SafeDeleteContext + { + private SafeGssNameHandle _targetName; + private SafeGssContextHandle _context; + + public SafeGssNameHandle TargetName + { + get { return _targetName; } + } + + public SafeGssContextHandle GssContext + { + get { return _context; } + } + + public SafeDeleteNegoContext(SafeFreeNegoCredentials credential, string targetName) + : base(credential) + { + Debug.Assert((null != credential), "Null credential in SafeDeleteNegoContext"); + try + { + _targetName = SafeGssNameHandle.CreatePrincipal(targetName); + } + catch + { + Dispose(); + throw; + } + } + + public void SetGssContext(SafeGssContextHandle context) + { + Debug.Assert(context != null && !context.IsInvalid, "Invalid context passed to SafeDeleteNegoContext"); + _context = context; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (null != _context) + { + _context.Dispose(); + _context = null; + } + + if (_targetName != null) + { + _targetName.Dispose(); + _targetName = null; + } + } + base.Dispose(disposing); + } + } +} diff --git a/src/Common/src/Interop/Unix/libssl/SecuritySafeHandles.cs b/src/Common/src/Interop/Unix/libssl/SecuritySafeHandles.cs index c0a7a062462d..de3568abd4c7 100644 --- a/src/Common/src/Interop/Unix/libssl/SecuritySafeHandles.cs +++ b/src/Common/src/Interop/Unix/libssl/SecuritySafeHandles.cs @@ -56,12 +56,19 @@ protected override bool ReleaseHandle() // Implementation of handles dependable on FreeCredentialsHandle // #if DEBUG - internal sealed class SafeFreeCredentials : DebugSafeHandle + internal abstract class SafeFreeCredentials : DebugSafeHandle { #else - internal sealed class SafeFreeCredentials : SafeHandle + internal abstract class SafeFreeCredentials : SafeHandle { #endif + protected SafeFreeCredentials(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle) + { + } + } + + internal sealed class SafeFreeSslCredentials : SafeFreeCredentials + { private SafeX509Handle _certHandle; private SafeEvpPKeyHandle _certKeyHandle; private SslProtocols _protocols = SslProtocols.None; @@ -87,7 +94,7 @@ internal EncryptionPolicy Policy get { return _policy; } } - public SafeFreeCredentials(X509Certificate certificate, SslProtocols protocols, EncryptionPolicy policy) + public SafeFreeSslCredentials(X509Certificate certificate, SslProtocols protocols, EncryptionPolicy policy) : base(IntPtr.Zero, true) { Debug.Assert( @@ -207,14 +214,45 @@ protected override bool ReleaseHandle() } #if DEBUG - internal sealed class SafeDeleteContext : DebugSafeHandle + internal abstract class SafeDeleteContext : DebugSafeHandle { #else - internal sealed class SafeDeleteContext : SafeHandle + internal abstract class SafeDeleteContext : SafeHandle { #endif - private readonly SafeFreeCredentials _credential; - private readonly SafeSslHandle _sslContext; + private SafeFreeCredentials _credential; + + protected SafeDeleteContext(SafeFreeCredentials credential) + : base(IntPtr.Zero, true) + { + Debug.Assert((null != credential), "Invalid credential passed to SafeDeleteContext"); + + // When a credential handle is first associated with the context we keep credential + // ref count bumped up to ensure ordered finalization. The credential properties + // are used in the SSL/NEGO data structures and should survive the lifetime of + // the SSL/NEGO context + bool ignore = false; + _credential = credential; + _credential.DangerousAddRef(ref ignore); + } + + public override bool IsInvalid + { + get { return (null == _credential); } + } + + protected override bool ReleaseHandle() + { + Debug.Assert((null != _credential), "Null credential in SafeDeleteContext"); + _credential.DangerousRelease(); + _credential = null; + return true; + } + } + + internal sealed class SafeDeleteSslContext : SafeDeleteContext + { + private SafeSslHandle _sslContext; public SafeSslHandle SslContext { @@ -224,18 +262,10 @@ public SafeSslHandle SslContext } } - public SafeDeleteContext(SafeFreeCredentials credential, bool isServer, bool remoteCertRequired) - : base(IntPtr.Zero, true) + public SafeDeleteSslContext(SafeFreeSslCredentials credential, bool isServer, bool remoteCertRequired) + : base(credential) { - Debug.Assert((null != credential) && !credential.IsInvalid, "Invalid credential used in SafeDeleteContext"); - - // When a credential handle is first associated with the context we keep credential - // ref count bumped up to ensure ordered finalization. The certificate handle and - // key handle are used in the SSL data structures and should survive the lifetime of - // the SSL context - bool gotCredRef = false; - _credential = credential; - _credential.DangerousAddRef(ref gotCredRef); + Debug.Assert((null != credential) && !credential.IsInvalid, "Invalid credential used in SafeDeleteSslContext"); try { @@ -249,11 +279,8 @@ public SafeDeleteContext(SafeFreeCredentials credential, bool isServer, bool rem } catch(Exception ex) { - if (gotCredRef) - { - _credential.DangerousRelease(); - } Debug.Write("Exception Caught. - " + ex); + Dispose(); throw; } } @@ -266,27 +293,19 @@ public override bool IsInvalid } } - protected override bool ReleaseHandle() - { - Debug.Assert((null != _credential) && !_credential.IsInvalid, "Invalid credential saved in SafeDeleteContext"); - _credential.DangerousRelease(); - return true; - } - protected override void Dispose(bool disposing) { if (disposing) { - _sslContext.Dispose(); + if (null != _sslContext) + { + _sslContext.Dispose(); + _sslContext = null; + } } base.Dispose(disposing); } - - public override string ToString() - { - return IsInvalid ? String.Empty : handle.ToString(); - } } internal sealed class SafeFreeContextBufferChannelBinding : ChannelBinding diff --git a/src/Common/src/Interop/Windows/sspicli/NegotiationInfoClass.cs b/src/Common/src/Interop/Windows/sspicli/NegotiationInfoClass.cs index d9e9b9bf9e32..e420e979428e 100644 --- a/src/Common/src/Interop/Windows/sspicli/NegotiationInfoClass.cs +++ b/src/Common/src/Interop/Windows/sspicli/NegotiationInfoClass.cs @@ -8,12 +8,8 @@ namespace System.Net { // This class is used to determine if NTLM or // Kerberos are used in the context of a Negotiate handshake - internal class NegotiationInfoClass + internal partial class NegotiationInfoClass { - internal const string NTLM = "NTLM"; - internal const string Kerberos = "Kerberos"; - internal const string WDigest = "WDigest"; - internal const string Negotiate = "Negotiate"; internal string AuthenticationPackage; internal NegotiationInfoClass(SafeHandle safeHandle, int negotiationState) @@ -57,10 +53,6 @@ internal NegotiationInfoClass(SafeHandle safeHandle, int negotiationState) { AuthenticationPackage = NTLM; } - else if (string.Compare(name, WDigest, StringComparison.OrdinalIgnoreCase) == 0) - { - AuthenticationPackage = WDigest; - } else { AuthenticationPackage = name; diff --git a/src/Common/src/Microsoft/Win32/SafeHandles/GssSafeHandles.cs b/src/Common/src/Microsoft/Win32/SafeHandles/GssSafeHandles.cs new file mode 100644 index 000000000000..bb45523be8e3 --- /dev/null +++ b/src/Common/src/Microsoft/Win32/SafeHandles/GssSafeHandles.cs @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace Microsoft.Win32.SafeHandles +{ + /// + /// Wrapper around a gss_name_t_desc* + /// + internal sealed class SafeGssNameHandle : SafeHandle + { + public static SafeGssNameHandle CreateUser(string name) + { + Debug.Assert(!string.IsNullOrEmpty(name), "Invalid user name passed to SafeGssNameHandle create"); + SafeGssNameHandle retHandle; + Interop.NetSecurityNative.Status minorStatus; + Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.ImportUserName( + out minorStatus, name, Encoding.UTF8.GetByteCount(name), out retHandle); + + if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) + { + retHandle.Dispose(); + throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); + } + + return retHandle; + } + + public static SafeGssNameHandle CreatePrincipal(string name) + { + Debug.Assert(!string.IsNullOrEmpty(name), "Invalid principal passed to SafeGssNameHandle create"); + SafeGssNameHandle retHandle; + Interop.NetSecurityNative.Status minorStatus; + Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.ImportPrincipalName( + out minorStatus, name, Encoding.UTF8.GetByteCount(name), out retHandle); + + if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) + { + retHandle.Dispose(); + throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); + } + + return retHandle; + } + + public override bool IsInvalid + { + get { return handle == IntPtr.Zero; } + } + + protected override bool ReleaseHandle() + { + Interop.NetSecurityNative.Status minorStatus; + Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.ReleaseName(out minorStatus, ref handle); + SetHandle(IntPtr.Zero); + return status == Interop.NetSecurityNative.Status.GSS_S_COMPLETE; + } + + private SafeGssNameHandle() + : base(IntPtr.Zero, true) + { + } + } + + /// + /// Wrapper around a gss_cred_id_t_desc_struct* + /// + internal class SafeGssCredHandle : SafeHandle + { + /// + /// returns the handle for the given credentials. + /// The method returns an invalid handle if the username is null or empty. + /// + public static SafeGssCredHandle Create(string username, string password, string domain) + { + if (string.IsNullOrEmpty(username)) + { + return new SafeGssCredHandle(); + } + + SafeGssCredHandle retHandle = null; + using (SafeGssNameHandle userHandle = SafeGssNameHandle.CreateUser(username)) + { + Interop.NetSecurityNative.Status status; + Interop.NetSecurityNative.Status minorStatus; + if (string.IsNullOrEmpty(password)) + { + status = Interop.NetSecurityNative.InitiateCredSpNego(out minorStatus, userHandle, out retHandle); + } + else + { + status = Interop.NetSecurityNative.InitiateCredWithPassword(out minorStatus, userHandle, password, Encoding.UTF8.GetByteCount(password), out retHandle); + } + + if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) + { + retHandle.Dispose(); + throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); + } + } + + return retHandle; + } + + private SafeGssCredHandle() + : base(IntPtr.Zero, true) + { + } + + public override bool IsInvalid + { + get { return handle == IntPtr.Zero; } + } + + protected override bool ReleaseHandle() + { + Interop.NetSecurityNative.Status minorStatus; + Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.ReleaseCred(out minorStatus, ref handle); + SetHandle(IntPtr.Zero); + return status == Interop.NetSecurityNative.Status.GSS_S_COMPLETE; + } + } + + internal sealed class SafeGssContextHandle : SafeHandle + { + public SafeGssContextHandle() + : base(IntPtr.Zero, true) + { + } + + public override bool IsInvalid + { + get { return handle == IntPtr.Zero; } + } + + protected override bool ReleaseHandle() + { + Interop.NetSecurityNative.Status minorStatus; + Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.DeleteSecContext(out minorStatus, ref handle); + SetHandle(IntPtr.Zero); + return status == Interop.NetSecurityNative.Status.GSS_S_COMPLETE; + } + } +} diff --git a/src/Common/src/System/Collections/Generic/BidirectionalDictionary.cs b/src/Common/src/System/Collections/Generic/BidirectionalDictionary.cs new file mode 100644 index 000000000000..108cda60bd7b --- /dev/null +++ b/src/Common/src/System/Collections/Generic/BidirectionalDictionary.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under item1 or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; + +namespace System.Collections.Generic +{ + internal sealed class BidirectionalDictionary : IEnumerable> + { + private readonly Dictionary _forward; + private readonly Dictionary _backward; + + public BidirectionalDictionary(int capacity) + { + _forward = new Dictionary(capacity); + _backward = new Dictionary(capacity); + } + + public void Add(T1 item1, T2 item2) + { + Debug.Assert(!_backward.ContainsKey(item2), "No added item1 should ever have existing item2"); + _forward.Add(item1, item2); + _backward.Add(item2, item1); + } + + public bool TryGetForward(T1 item1, out T2 item2) + { + return _forward.TryGetValue(item1, out item2); + } + + public bool TryGetBackward(T2 item2, out T1 item1) + { + return _backward.TryGetValue(item2, out item1); + } + + public Dictionary.Enumerator GetEnumerator() + { + return _forward.GetEnumerator(); + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Native/CMakeLists.txt b/src/Native/CMakeLists.txt index 0b23fc234378..a812386faf3b 100644 --- a/src/Native/CMakeLists.txt +++ b/src/Native/CMakeLists.txt @@ -99,4 +99,5 @@ include(configure.cmake) add_subdirectory(System.IO.Compression.Native) add_subdirectory(System.Native) add_subdirectory(System.Net.Http.Native) +add_subdirectory(System.Net.Security.Native) add_subdirectory(System.Security.Cryptography.Native) diff --git a/src/Native/Common/pal_config.h.in b/src/Native/Common/pal_config.h.in index cf9d6b80ea36..f7222183d1ff 100644 --- a/src/Native/Common/pal_config.h.in +++ b/src/Native/Common/pal_config.h.in @@ -54,6 +54,8 @@ #cmakedefine01 HAVE_CURLPIPE_MULTIPLEX #cmakedefine01 HAVE_TCP_H_TCPSTATE_ENUM #cmakedefine01 HAVE_TCP_FSM_H +#cmakedefine01 HAVE_GSSFW_HEADERS +#cmakedefine01 HAVE_GSS_SPNEGO_MECHANISM // Mac OS X has stat64, but it is deprecated since plain stat now // provides the same 64-bit aware struct when targeting OS X > 10.5 diff --git a/src/Native/System.Net.Security.Native/CMakeLists.txt b/src/Native/System.Net.Security.Native/CMakeLists.txt new file mode 100644 index 000000000000..29a3e6a5d482 --- /dev/null +++ b/src/Native/System.Net.Security.Native/CMakeLists.txt @@ -0,0 +1,30 @@ +project(System.Net.Security.Native) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +add_definitions(-DPIC=1) + +if (HAVE_GSSFW_HEADERS) + find_library(LIBGSS NAMES GSS) +else() + find_library(LIBGSS NAMES gssapi_krb5) +endif() + +if(LIBGSS STREQUAL LIBGSS-NOTFOUND) + message(FATAL_ERROR "Cannot find libgssapi_krb5 and System.Net.Security.Native cannot build without it. Try installing libkrb5-dev (or the appropriate package for your platform)") +endif() + +set(NATIVEGSS_SOURCES + pal_gssapi.cpp +) + +add_library(System.Net.Security.Native + SHARED + ${NATIVEGSS_SOURCES} +) + +target_link_libraries(System.Net.Security.Native + ${LIBGSS} +) + +install (TARGETS System.Net.Security.Native DESTINATION .) diff --git a/src/Native/System.Net.Security.Native/pal_gssapi.cpp b/src/Native/System.Net.Security.Native/pal_gssapi.cpp new file mode 100644 index 000000000000..81833e2b2471 --- /dev/null +++ b/src/Native/System.Net.Security.Native/pal_gssapi.cpp @@ -0,0 +1,320 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "pal_types.h" +#include "pal_utilities.h" +#include "pal_gssapi.h" + +#if HAVE_GSSFW_HEADERS +#include +#else +#include +#include +#endif + +#include +#include + +static_assert(PAL_GSS_C_DELEG_FLAG == GSS_C_DELEG_FLAG, ""); +static_assert(PAL_GSS_C_MUTUAL_FLAG == GSS_C_MUTUAL_FLAG, ""); +static_assert(PAL_GSS_C_REPLAY_FLAG == GSS_C_REPLAY_FLAG, ""); +static_assert(PAL_GSS_C_SEQUENCE_FLAG == GSS_C_SEQUENCE_FLAG, ""); +static_assert(PAL_GSS_C_CONF_FLAG == GSS_C_CONF_FLAG, ""); +static_assert(PAL_GSS_C_INTEG_FLAG == GSS_C_INTEG_FLAG, ""); +static_assert(PAL_GSS_C_ANON_FLAG == GSS_C_ANON_FLAG, ""); +static_assert(PAL_GSS_C_PROT_READY_FLAG == GSS_C_PROT_READY_FLAG, ""); +static_assert(PAL_GSS_C_TRANS_FLAG == GSS_C_TRANS_FLAG, ""); +static_assert(PAL_GSS_C_DCE_STYLE == GSS_C_DCE_STYLE, ""); +static_assert(PAL_GSS_C_IDENTIFY_FLAG == GSS_C_IDENTIFY_FLAG, ""); +static_assert(PAL_GSS_C_EXTENDED_ERROR_FLAG == GSS_C_EXTENDED_ERROR_FLAG, ""); +static_assert(PAL_GSS_C_DELEG_POLICY_FLAG == GSS_C_DELEG_POLICY_FLAG, ""); + +static_assert(PAL_GSS_COMPLETE == GSS_S_COMPLETE, ""); +static_assert(PAL_GSS_CONTINUE_NEEDED == GSS_S_CONTINUE_NEEDED, ""); + +#if !HAVE_GSS_SPNEGO_MECHANISM +static char gss_spnego_oid_value[] = "\x2b\x06\x01\x05\x05\x02"; // Binary representation of SPNEGO Oid (RFC 4178) +#endif + +// transfers ownership of the underlying data from gssBuffer to PAL_GssBuffer +static void NetSecurityNative_MoveBuffer(gss_buffer_t gssBuffer, struct PAL_GssBuffer* targetBuffer) +{ + assert(gssBuffer != nullptr); + assert(targetBuffer != nullptr); + + targetBuffer->length = gssBuffer->length; + targetBuffer->data = static_cast(gssBuffer->value); +} + +static uint32_t NetSecurityNative_AcquireCredSpNego(uint32_t* minorStatus, + GssName* desiredName, + gss_cred_usage_t credUsage, + GssCredId** outputCredHandle) +{ + assert(minorStatus != nullptr); + assert(desiredName != nullptr); + assert(outputCredHandle != nullptr); + +#if HAVE_GSS_SPNEGO_MECHANISM + gss_OID_set_desc gss_mech_spnego_OID_set_desc = {.count = 1, .elements = GSS_SPNEGO_MECHANISM}; +#else + gss_OID_desc gss_mech_spnego_OID_desc = {.length = 6, .elements = static_cast(gss_spnego_oid_value)}; + gss_OID_set_desc gss_mech_spnego_OID_set_desc = {.count = 1, .elements = &gss_mech_spnego_OID_desc}; +#endif + return gss_acquire_cred( + minorStatus, desiredName, 0, &gss_mech_spnego_OID_set_desc, credUsage, outputCredHandle, nullptr, nullptr); +} + +extern "C" uint32_t +NetSecurityNative_InitiateCredSpNego(uint32_t* minorStatus, GssName* desiredName, GssCredId** outputCredHandle) +{ + return NetSecurityNative_AcquireCredSpNego(minorStatus, desiredName, GSS_C_INITIATE, outputCredHandle); +} + +extern "C" uint32_t NetSecurityNative_DeleteSecContext(uint32_t* minorStatus, GssCtxId** contextHandle) +{ + assert(minorStatus != nullptr); + assert(contextHandle != nullptr); + + return gss_delete_sec_context(minorStatus, contextHandle, GSS_C_NO_BUFFER); +} + +static uint32_t NetSecurityNative_DisplayStatus(uint32_t* minorStatus, + uint32_t statusValue, + int statusType, + struct PAL_GssBuffer* outBuffer) +{ + assert(minorStatus != nullptr); + assert(outBuffer != nullptr); + + uint32_t messageContext; + GssBuffer gssBuffer{.length = 0, .value = nullptr}; + uint32_t majorStatus = + gss_display_status(minorStatus, statusValue, statusType, GSS_C_NO_OID, &messageContext, &gssBuffer); + + NetSecurityNative_MoveBuffer(&gssBuffer, outBuffer); + return majorStatus; +} + +extern "C" uint32_t NetSecurityNative_DisplayMinorStatus(uint32_t* minorStatus, + uint32_t statusValue, + struct PAL_GssBuffer* outBuffer) +{ + return NetSecurityNative_DisplayStatus(minorStatus, statusValue, GSS_C_MECH_CODE, outBuffer); +} + +extern "C" uint32_t NetSecurityNative_DisplayMajorStatus(uint32_t* minorStatus, + uint32_t statusValue, + struct PAL_GssBuffer* outBuffer) +{ + return NetSecurityNative_DisplayStatus(minorStatus, statusValue, GSS_C_GSS_CODE, outBuffer); +} + +extern "C" uint32_t +NetSecurityNative_ImportUserName(uint32_t* minorStatus, char* inputName, uint32_t inputNameLen, GssName** outputName) +{ + assert(minorStatus != nullptr); + assert(inputName != nullptr); + assert(outputName != nullptr); + + GssBuffer inputNameBuffer{.length = inputNameLen, .value = inputName}; + gss_OID nameType = const_cast(GSS_C_NT_USER_NAME); + return gss_import_name(minorStatus, &inputNameBuffer, nameType, outputName); +} + +extern "C" uint32_t NetSecurityNative_ImportPrincipalName(uint32_t* minorStatus, + char* inputName, + uint32_t inputNameLen, + GssName** outputName) +{ + assert(minorStatus != nullptr); + assert(inputName != nullptr); + assert(outputName != nullptr); + + GssBuffer inputNameBuffer{.length = inputNameLen, .value = inputName}; + gss_OID nameType = const_cast(GSS_KRB5_NT_PRINCIPAL_NAME); + return gss_import_name(minorStatus, &inputNameBuffer, nameType, outputName); +} + +extern "C" uint32_t NetSecurityNative_InitSecContext(uint32_t* minorStatus, + GssCredId* claimantCredHandle, + GssCtxId** contextHandle, + uint32_t isNtlm, + GssName* targetName, + uint32_t reqFlags, + uint8_t* inputBytes, + uint32_t inputLength, + struct PAL_GssBuffer* outBuffer, + uint32_t* retFlags) +{ + assert(minorStatus != nullptr); + assert(contextHandle != nullptr); + assert(isNtlm == 0 || isNtlm == 1); + assert(targetName != nullptr); + assert(inputBytes != nullptr || inputLength == 0); + assert(outBuffer != nullptr); + assert(retFlags != nullptr); + +// Note: claimantCredHandle can be null + +#if HAVE_GSS_SPNEGO_MECHANISM + gss_OID desiredMech = isNtlm ? GSS_NTLM_MECHANISM : GSS_SPNEGO_MECHANISM; +#else + assert(!isNtlm && "NTLM is not supported by MIT libgssapi_krb5"); + (void)isNtlm; // unused + + gss_OID_desc gss_mech_spnego_OID_desc = {.length = 6, .elements = static_cast(gss_spnego_oid_value)}; + gss_OID desiredMech = &gss_mech_spnego_OID_desc; +#endif + + GssBuffer inputToken{.length = inputLength, .value = inputBytes}; + GssBuffer gssBuffer{.length = 0, .value = nullptr}; + + uint32_t majorStatus = gss_init_sec_context(minorStatus, + claimantCredHandle, + contextHandle, + targetName, + desiredMech, + reqFlags, + 0, + GSS_C_NO_CHANNEL_BINDINGS, + &inputToken, + nullptr, + &gssBuffer, + retFlags, + nullptr); + + NetSecurityNative_MoveBuffer(&gssBuffer, outBuffer); + return majorStatus; +} + +extern "C" uint32_t NetSecurityNative_AcceptSecContext(uint32_t* minorStatus, + GssCtxId** contextHandle, + uint8_t* inputBytes, + uint32_t inputLength, + struct PAL_GssBuffer* outBuffer) +{ + assert(minorStatus != nullptr); + assert(contextHandle != nullptr); + assert(inputBytes != nullptr || inputLength == 0); + assert(outBuffer != nullptr); + + GssBuffer inputToken{.length = UnsignedCast(inputLength), .value = inputBytes}; + GssBuffer gssBuffer{.length = 0, .value = nullptr}; + + uint32_t majorStatus = gss_accept_sec_context(minorStatus, + contextHandle, + GSS_C_NO_CREDENTIAL, + &inputToken, + GSS_C_NO_CHANNEL_BINDINGS, + nullptr, + nullptr, + &gssBuffer, + 0, + nullptr, + nullptr); + + NetSecurityNative_MoveBuffer(&gssBuffer, outBuffer); + return majorStatus; +} + +extern "C" uint32_t NetSecurityNative_ReleaseCred(uint32_t* minorStatus, GssCredId** credHandle) +{ + assert(minorStatus != nullptr); + assert(credHandle != nullptr); + + return gss_release_cred(minorStatus, credHandle); +} + +extern "C" void NetSecurityNative_ReleaseGssBuffer(void* buffer, uint64_t length) +{ + assert(buffer != nullptr); + + uint32_t minorStatus; + GssBuffer gssBuffer{.length = length, .value = buffer}; + gss_release_buffer(&minorStatus, &gssBuffer); +} + +extern "C" uint32_t NetSecurityNative_ReleaseName(uint32_t* minorStatus, GssName** inputName) +{ + assert(minorStatus != nullptr); + assert(inputName != nullptr); + + return gss_release_name(minorStatus, inputName); +} + +extern "C" uint32_t NetSecurityNative_Wrap(uint32_t* minorStatus, + GssCtxId* contextHandle, + int32_t isEncrypt, + uint8_t* inputBytes, + int32_t offset, + int32_t count, + struct PAL_GssBuffer* outBuffer) +{ + assert(minorStatus != nullptr); + assert(contextHandle != nullptr); + assert(isEncrypt == 1 || isEncrypt == 0); + assert(inputBytes != nullptr); + assert(offset >= 0); + assert(count >= 0); + assert(outBuffer != nullptr); + // count refers to the length of the input message. That is, number of bytes of inputBytes + // starting at offset that need to be wrapped. + + int confState; + GssBuffer inputMessageBuffer{.length = UnsignedCast(count), .value = inputBytes + offset}; + GssBuffer gssBuffer; + uint32_t majorStatus = + gss_wrap(minorStatus, contextHandle, isEncrypt, GSS_C_QOP_DEFAULT, &inputMessageBuffer, &confState, &gssBuffer); + + NetSecurityNative_MoveBuffer(&gssBuffer, outBuffer); + return majorStatus; +} + +extern "C" uint32_t NetSecurityNative_Unwrap(uint32_t* minorStatus, + GssCtxId* contextHandle, + uint8_t* inputBytes, + int32_t offset, + int32_t count, + struct PAL_GssBuffer* outBuffer) +{ + assert(minorStatus != nullptr); + assert(contextHandle != nullptr); + assert(inputBytes != nullptr); + assert(offset >= 0); + assert(count >= 0); + assert(outBuffer != nullptr); + + // count refers to the length of the input message. That is, the number of bytes of inputBytes + // starting at offset that need to be wrapped. + GssBuffer inputMessageBuffer{.length = UnsignedCast(count), .value = inputBytes + offset}; + GssBuffer gssBuffer{.length = 0, .value = nullptr}; + uint32_t majorStatus = gss_unwrap(minorStatus, contextHandle, &inputMessageBuffer, &gssBuffer, nullptr, nullptr); + NetSecurityNative_MoveBuffer(&gssBuffer, outBuffer); + return majorStatus; +} + +static uint32_t NetSecurityNative_AcquireCredWithPassword(uint32_t* minorStatus, + GssName* desiredName, + char* password, + uint32_t passwdLen, + gss_cred_usage_t credUsage, + GssCredId** outputCredHandle) +{ + assert(minorStatus != nullptr); + assert(desiredName != nullptr); + assert(password != nullptr); + assert(outputCredHandle != nullptr); + + GssBuffer passwordBuffer{.length = passwdLen, .value = password}; + return gss_acquire_cred_with_password( + minorStatus, desiredName, &passwordBuffer, 0, nullptr, credUsage, outputCredHandle, nullptr, nullptr); +} + +extern "C" uint32_t NetSecurityNative_InitiateCredWithPassword( + uint32_t* minorStatus, GssName* desiredName, char* password, uint32_t passwdLen, GssCredId** outputCredHandle) +{ + return NetSecurityNative_AcquireCredWithPassword( + minorStatus, desiredName, password, passwdLen, GSS_C_INITIATE, outputCredHandle); +} diff --git a/src/Native/System.Net.Security.Native/pal_gssapi.h b/src/Native/System.Net.Security.Native/pal_gssapi.h new file mode 100644 index 000000000000..6e27f8b4abcf --- /dev/null +++ b/src/Native/System.Net.Security.Native/pal_gssapi.h @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once + +#if HAVE_GSSFW_HEADERS +typedef struct gss_name_t_desc_struct GssName; +typedef struct gss_ctx_id_t_desc_struct GssCtxId; +typedef struct gss_cred_id_t_desc_struct GssCredId; +typedef struct gss_buffer_desc_struct GssBuffer; +#else +typedef struct gss_name_struct GssName; +typedef struct gss_ctx_id_struct GssCtxId; +typedef struct gss_cred_id_struct GssCredId; +typedef struct gss_buffer_desc_struct GssBuffer; +#endif + +enum PAL_GssStatus : uint32_t +{ + PAL_GSS_COMPLETE = 0, + PAL_GSS_CONTINUE_NEEDED = 1 +}; + +enum PAL_GssFlags : uint32_t +{ + PAL_GSS_C_DELEG_FLAG = 0x1, + PAL_GSS_C_MUTUAL_FLAG = 0x2, + PAL_GSS_C_REPLAY_FLAG = 0x4, + PAL_GSS_C_SEQUENCE_FLAG = 0x8, + PAL_GSS_C_CONF_FLAG = 0x10, + PAL_GSS_C_INTEG_FLAG = 0x20, + PAL_GSS_C_ANON_FLAG = 0x40, + PAL_GSS_C_PROT_READY_FLAG = 0x80, + PAL_GSS_C_TRANS_FLAG = 0x100, + PAL_GSS_C_DCE_STYLE = 0x1000, + PAL_GSS_C_IDENTIFY_FLAG = 0x2000, + PAL_GSS_C_EXTENDED_ERROR_FLAG = 0x4000, + PAL_GSS_C_DELEG_POLICY_FLAG = 0x8000 +}; + +struct PAL_GssBuffer +{ + uint64_t length; + uint8_t* data; +}; + +/* +Shims the gss_release_buffer method. +*/ +extern "C" void NetSecurityNative_ReleaseGssBuffer(void* buffer, uint64_t length); + +/* +Shims the gss_display_status method for minor status (status_type = GSS_C_MECH_CODE). +*/ +extern "C" uint32_t NetSecurityNative_DisplayMinorStatus(uint32_t* minorStatus, + uint32_t statusValue, + struct PAL_GssBuffer* outBuffer); + +/* +Shims the gss_display_status method for major status (status_type = GSS_C_GSS_CODE). +*/ +extern "C" uint32_t NetSecurityNative_DisplayMajorStatus(uint32_t* minorStatus, + uint32_t statusValue, + struct PAL_GssBuffer* outBuffer); + +/* +Shims the gss_import_name method with nametype = GSS_C_NT_USER_NAME. +*/ +extern "C" uint32_t +NetSecurityNative_ImportUserName(uint32_t* minorStatus, char* inputName, uint32_t inputNameLen, GssName** outputName); + +/* +Shims the gss_import_name method with nametype = GSS_C_NT_USER_NAME. +*/ +extern "C" uint32_t NetSecurityNative_ImportPrincipalName(uint32_t* minorStatus, + char* inputName, + uint32_t inputNameLen, + GssName** outputName); + +/* +Shims the gss_release_name method. +*/ +extern "C" uint32_t NetSecurityNative_ReleaseName(uint32_t* minorStatus, GssName** inputName); + +/* +Shims the gss_acquire_cred method with SPNEGO oids with GSS_C_INITIATE. +*/ +extern "C" uint32_t +NetSecurityNative_InitiateCredSpNego(uint32_t* minorStatus, GssName* desiredName, GssCredId** outputCredHandle); + +/* +Shims the gss_release_cred method. +*/ +extern "C" uint32_t NetSecurityNative_ReleaseCred(uint32_t* minorStatus, GssCredId** credHandle); + +/* +Shims the gss_init_sec_context method with SPNEGO oids. +*/ +extern "C" uint32_t NetSecurityNative_InitSecContext(uint32_t* minorStatus, + GssCredId* claimantCredHandle, + GssCtxId** contextHandle, + uint32_t isNtlm, + GssName* targetName, + uint32_t reqFlags, + uint8_t* inputBytes, + uint32_t inputLength, + struct PAL_GssBuffer* outBuffer, + uint32_t* retFlags); + +/* +Shims the gss_accept_sec_context method. +*/ +extern "C" uint32_t NetSecurityNative_AcceptSecContext(uint32_t* minorStatus, + GssCtxId** contextHandle, + uint8_t* inputBytes, + uint32_t inputLength, + struct PAL_GssBuffer* outBuffer); + +/* + +Shims the gss_delete_sec_context method. +*/ +extern "C" uint32_t NetSecurityNative_DeleteSecContext(uint32_t* minorStatus, GssCtxId** contextHandle); + +/* +Shims the gss_wrap method. +*/ +extern "C" uint32_t NetSecurityNative_Wrap(uint32_t* minorStatus, + GssCtxId* contextHandle, + int32_t isEncrypt, + uint8_t* inputBytes, + int32_t offset, + int32_t count, + struct PAL_GssBuffer* outBuffer); + +/* +Shims the gss_unwrap method. +*/ +extern "C" uint32_t NetSecurityNative_Unwrap(uint32_t* minorStatus, + GssCtxId* contextHandle, + uint8_t* inputBytes, + int32_t offset, + int32_t count, + struct PAL_GssBuffer* outBuffer); + +/* +Shims the gss_acquire_cred_with_password method with GSS_C_INITIATE. +*/ +extern "C" uint32_t NetSecurityNative_InitiateCredWithPassword( + uint32_t* minorStatus, GssName* desiredName, char* password, uint32_t passwdLen, GssCredId** outputCredHandle); diff --git a/src/Native/configure.cmake b/src/Native/configure.cmake index 8ff797c27968..149747c0539e 100644 --- a/src/Native/configure.cmake +++ b/src/Native/configure.cmake @@ -421,6 +421,22 @@ check_cxx_source_compiles( " HAVE_CURLPIPE_MULTIPLEX) +check_include_files( + GSS/GSS.h + HAVE_GSSFW_HEADERS) + +if (HAVE_GSSFW_HEADERS) + check_symbol_exists( + GSS_SPNEGO_MECHANISM + "GSS/GSS.h" + HAVE_GSS_SPNEGO_MECHANISM) +else () + check_symbol_exists( + GSS_SPNEGO_MECHANISM + "gssapi/gssapi.h" + HAVE_GSS_SPNEGO_MECHANISM) +endif () + set (CMAKE_REQUIRED_LIBRARIES) configure_file( diff --git a/src/Native/pkg/runtime.native.System.Net.Security/debian/runtime.native.System.Net.Security.pkgproj b/src/Native/pkg/runtime.native.System.Net.Security/debian/runtime.native.System.Net.Security.pkgproj new file mode 100644 index 000000000000..df4464800f1e --- /dev/null +++ b/src/Native/pkg/runtime.native.System.Net.Security/debian/runtime.native.System.Net.Security.pkgproj @@ -0,0 +1,19 @@ + + + + + + 1.0.1 + debian.8-$(PackagePlatform) + + x64; + + + + + runtimes/$(PackageTargetRuntime)/native + + + + + diff --git a/src/Native/pkg/runtime.native.System.Net.Security/osx/runtime.native.System.Net.Security.pkgproj b/src/Native/pkg/runtime.native.System.Net.Security/osx/runtime.native.System.Net.Security.pkgproj new file mode 100644 index 000000000000..6da2f0354158 --- /dev/null +++ b/src/Native/pkg/runtime.native.System.Net.Security/osx/runtime.native.System.Net.Security.pkgproj @@ -0,0 +1,19 @@ + + + + + + 1.0.1 + osx.10.10-$(PackagePlatform) + + x64; + + + + + runtimes/$(PackageTargetRuntime)/native + + + + + diff --git a/src/Native/pkg/runtime.native.System.Net.Security/rhel/runtime.native.System.Net.Security.pkgproj b/src/Native/pkg/runtime.native.System.Net.Security/rhel/runtime.native.System.Net.Security.pkgproj new file mode 100644 index 000000000000..d9f0f3579002 --- /dev/null +++ b/src/Native/pkg/runtime.native.System.Net.Security/rhel/runtime.native.System.Net.Security.pkgproj @@ -0,0 +1,19 @@ + + + + + + 1.0.1 + rhel.7-$(PackagePlatform) + + x64; + + + + + runtimes/$(PackageTargetRuntime)/native + + + + + diff --git a/src/Native/pkg/runtime.native.System.Net.Security/runtime.native.System.Net.Security.builds b/src/Native/pkg/runtime.native.System.Net.Security/runtime.native.System.Net.Security.builds new file mode 100644 index 000000000000..272d8410f7f5 --- /dev/null +++ b/src/Native/pkg/runtime.native.System.Net.Security/runtime.native.System.Net.Security.builds @@ -0,0 +1,28 @@ + + + + + + + + Linux + amd64 + + + Linux + amd64 + + + Linux + amd64 + + + OSX + amd64 + + + + diff --git a/src/Native/pkg/runtime.native.System.Net.Security/runtime.native.System.Net.Security.pkgproj b/src/Native/pkg/runtime.native.System.Net.Security/runtime.native.System.Net.Security.pkgproj new file mode 100644 index 000000000000..4a7dc46130e1 --- /dev/null +++ b/src/Native/pkg/runtime.native.System.Net.Security/runtime.native.System.Net.Security.pkgproj @@ -0,0 +1,27 @@ + + + + + + 4.0.1 + true + true + + + + + amd64 + + + amd64 + + + amd64 + + + amd64 + + + + + diff --git a/src/Native/pkg/runtime.native.System.Net.Security/ubuntu/runtime.native.System.Net.Security.pkgproj b/src/Native/pkg/runtime.native.System.Net.Security/ubuntu/runtime.native.System.Net.Security.pkgproj new file mode 100644 index 000000000000..c9987159a169 --- /dev/null +++ b/src/Native/pkg/runtime.native.System.Net.Security/ubuntu/runtime.native.System.Net.Security.pkgproj @@ -0,0 +1,19 @@ + + + + + + 1.0.1 + ubuntu.14.04-$(PackagePlatform) + + x64; + + + + + runtimes/$(PackageTargetRuntime)/native + + + + + diff --git a/src/System.Net.Security/pkg/System.Net.Security.pkgproj b/src/System.Net.Security/pkg/System.Net.Security.pkgproj index 4ee3049e4649..01e3d950e3d5 100644 --- a/src/System.Net.Security/pkg/System.Net.Security.pkgproj +++ b/src/System.Net.Security/pkg/System.Net.Security.pkgproj @@ -19,5 +19,8 @@ win7 + + + \ No newline at end of file diff --git a/src/System.Net.Security/src/Resources/Strings.resx b/src/System.Net.Security/src/Resources/Strings.resx index c3776d261e2f..be553506a4f9 100644 --- a/src/System.Net.Security/src/Resources/Strings.resx +++ b/src/System.Net.Security/src/Resources/Strings.resx @@ -1,4 +1,4 @@ - + + + + + + + @@ -145,13 +151,12 @@ + - - - - + + @@ -199,6 +204,9 @@ Common\Interop\Windows\sspicli\SecSizes.cs + + Common\System\Collections\Generic\BidirectionalDictionary.cs + Common\System\Net\Logging\SecurityEventSource.Windows.cs @@ -236,7 +244,8 @@ - + + @@ -302,6 +311,24 @@ Common\Interop\Unix\System.Net.Security.Native\Interop.Initialization.cs + + Common\Interop\Unix\System.Net.Security.Native\Interop.GssApi.cs + + + Common\Interop\Unix\System.Net.Security.Native\Interop.GssApiException.cs + + + Common\Interop\Unix\System.Net.Security.Native\Interop.GssBuffer.cs + + + Common\Interop\Unix\System.Net.Security.Native\SecuritySafeHandles.cs + + + Common\Microsoft\Win32\SafeHandles\GssSafeHandles.cs + + + Common\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.cs + Common\Microsoft\Win32\SafeHandles\SafeX509Handles.Unix.cs diff --git a/src/System.Net.Security/src/System/Net/CertificateValidationPal.Unix.cs b/src/System.Net.Security/src/System/Net/CertificateValidationPal.Unix.cs index 4c68992cb047..29f02ace7398 100644 --- a/src/System.Net.Security/src/System/Net/CertificateValidationPal.Unix.cs +++ b/src/System.Net.Security/src/System/Net/CertificateValidationPal.Unix.cs @@ -113,7 +113,7 @@ internal static X509Certificate2 GetRemoteCertificate(SafeDeleteContext security remoteCertificateStore = new X509Certificate2Collection(); using (SafeSharedX509StackHandle chainStack = - Interop.OpenSsl.GetPeerCertificateChain(securityContext.SslContext)) + Interop.OpenSsl.GetPeerCertificateChain(((SafeDeleteSslContext)securityContext).SslContext)) { if (!chainStack.IsInvalid) { @@ -163,7 +163,7 @@ internal static X509Certificate2 GetRemoteCertificate(SafeDeleteContext security // internal static string[] GetRequestCertificateAuthorities(SafeDeleteContext securityContext) { - using (SafeSharedX509NameStackHandle names = Interop.Ssl.SslGetClientCAList(securityContext.SslContext)) + using (SafeSharedX509NameStackHandle names = Interop.Ssl.SslGetClientCAList(((SafeDeleteSslContext)securityContext).SslContext)) { if (names.IsInvalid) { @@ -257,7 +257,7 @@ private static int QueryContextRemoteCertificate(SafeDeleteContext securityConte remoteCertContext = null; try { - SafeX509Handle remoteCertificate = Interop.OpenSsl.GetPeerCertificate(securityContext.SslContext); + SafeX509Handle remoteCertificate = Interop.OpenSsl.GetPeerCertificate(((SafeDeleteSslContext)securityContext).SslContext); // Note that cert ownership is transferred to SafeFreeCertContext remoteCertContext = new SafeFreeCertContext(remoteCertificate); return 0; diff --git a/src/System.Net.Security/src/System/Net/ContextFlagsAdapterPal.Unix.cs b/src/System.Net.Security/src/System/Net/ContextFlagsAdapterPal.Unix.cs new file mode 100644 index 000000000000..fa0f772ca64f --- /dev/null +++ b/src/System.Net.Security/src/System/Net/ContextFlagsAdapterPal.Unix.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace System.Net +{ + internal static class ContextFlagsAdapterPal + { + private 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[] + { + // GSS_C_INTEG_FLAG is set if either AcceptIntegrity (used by server) or InitIntegrity (used by client) is set + new ContextFlagMapping(Interop.NetSecurityNative.GssFlags.GSS_C_INTEG_FLAG, ContextFlagsPal.AcceptIntegrity | ContextFlagsPal.InitIntegrity), + 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) + }; + + + internal static ContextFlagsPal GetContextFlagsPalFromInterop(Interop.NetSecurityNative.GssFlags gssFlags) + { + ContextFlagsPal flags = ContextFlagsPal.None; + 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) + { + Interop.NetSecurityNative.GssFlags gssFlags = 0; + foreach (ContextFlagMapping mapping in s_contextFlagMapping) + { + if ((flags & mapping.ContextFlag) == mapping.ContextFlag) + { + gssFlags |= mapping.GssFlags; + } + } + + return gssFlags; + } + } +} diff --git a/src/System.Net.Security/src/System/Net/ContextFlagsAdapterPal.Windows.cs b/src/System.Net.Security/src/System/Net/ContextFlagsAdapterPal.Windows.cs new file mode 100644 index 000000000000..c560b2d15e5a --- /dev/null +++ b/src/System.Net.Security/src/System/Net/ContextFlagsAdapterPal.Windows.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace System.Net +{ + internal static class ContextFlagsAdapterPal + { + private 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/System.Net.Security/src/System/Net/ContextFlagsPal.cs b/src/System.Net.Security/src/System/Net/ContextFlagsPal.cs new file mode 100644 index 000000000000..d4df40b5974c --- /dev/null +++ b/src/System.Net.Security/src/System/Net/ContextFlagsPal.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +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/System.Net.Security/src/System/Net/NTAuthentication.cs b/src/System.Net.Security/src/System/Net/NTAuthentication.cs index 4c977eee2775..a9bb64cd818f 100644 --- a/src/System.Net.Security/src/System/Net/NTAuthentication.cs +++ b/src/System.Net.Security/src/System/Net/NTAuthentication.cs @@ -24,12 +24,11 @@ internal class NTAuthentication private string _clientSpecifiedSpn; private int _tokenSize; - private Interop.SspiCli.ContextFlags _requestedContextFlags; - private Interop.SspiCli.ContextFlags _contextFlags; + private ContextFlagsPal _requestedContextFlags; + private ContextFlagsPal _contextFlags; private bool _isCompleted; private string _protocolName; - private SecSizes _sizes; private string _lastProtocolName; private string _package; @@ -58,10 +57,10 @@ internal string AssociatedName { if (!(IsValidContext && IsCompleted)) { - throw new Win32Exception((int)Interop.SecurityStatus.InvalidHandle); + throw new Win32Exception((int)SecurityStatusPalErrorCode.InvalidHandle); } - string name = SSPIWrapper.QueryContextAttributes(GlobalSSPI.SSPIAuth, _securityContext, Interop.SspiCli.ContextAttribute.Names) as string; + string name = NegotiateStreamPal.QueryContextAssociatedName(_securityContext); if (GlobalLog.IsEnabled) { GlobalLog.Print("NTAuthentication: The context is associated with [" + name + "]"); @@ -74,7 +73,7 @@ internal bool IsConfidentialityFlag { get { - return (_contextFlags & Interop.SspiCli.ContextFlags.Confidentiality) != 0; + return (_contextFlags & ContextFlagsPal.Confidentiality) != 0; } } @@ -82,7 +81,7 @@ internal bool IsIntegrityFlag { get { - return (_contextFlags & (_isServer ? Interop.SspiCli.ContextFlags.AcceptIntegrity : Interop.SspiCli.ContextFlags.InitIntegrity)) != 0; + return (_contextFlags & (_isServer ? ContextFlagsPal.AcceptIntegrity : ContextFlagsPal.InitIntegrity)) != 0; } } @@ -90,7 +89,7 @@ internal bool IsMutualAuthFlag { get { - return (_contextFlags & Interop.SspiCli.ContextFlags.MutualAuth) != 0; + return (_contextFlags & ContextFlagsPal.MutualAuth) != 0; } } @@ -98,7 +97,7 @@ internal bool IsDelegationFlag { get { - return (_contextFlags & Interop.SspiCli.ContextFlags.Delegate) != 0; + return (_contextFlags & ContextFlagsPal.Delegate) != 0; } } @@ -106,7 +105,7 @@ internal bool IsIdentifyFlag { get { - return (_contextFlags & (_isServer ? Interop.SspiCli.ContextFlags.AcceptIdentify : Interop.SspiCli.ContextFlags.InitIdentify)) != 0; + return (_contextFlags & (_isServer ? ContextFlagsPal.AcceptIdentify : ContextFlagsPal.InitIdentify)) != 0; } } @@ -175,66 +174,34 @@ internal string ProtocolName // Note: May return string.Empty if the auth is not done yet or failed. if (_protocolName == null) { - NegotiationInfoClass negotiationInfo = null; + string negotiationAuthenticationPackage = null; if (IsValidContext) { - negotiationInfo = SSPIWrapper.QueryContextAttributes(GlobalSSPI.SSPIAuth, _securityContext, Interop.SspiCli.ContextAttribute.NegotiationInfo) as NegotiationInfoClass; + negotiationAuthenticationPackage = NegotiateStreamPal.QueryContextAuthenticationPackage(_securityContext); if (IsCompleted) { - if (negotiationInfo != null) - { - // Cache it only when it's completed. - _protocolName = negotiationInfo.AuthenticationPackage; - } + _protocolName = negotiationAuthenticationPackage; } } - - return negotiationInfo == null ? string.Empty : negotiationInfo.AuthenticationPackage; + return negotiationAuthenticationPackage ?? string.Empty; } return _protocolName; } } - internal SecSizes Sizes - { - get - { - if (!(IsCompleted && IsValidContext)) - { - if (GlobalLog.IsEnabled) - { - GlobalLog.AssertFormat("NTAuthentication#{0}::MaxDataSize|The context is not completed or invalid.", LoggingHash.HashString(this)); - } - - Debug.Fail("NTAuthentication#" + LoggingHash.HashString(this) + "::MaxDataSize |The context is not completed or invalid."); - } - - if (_sizes == null) - { - _sizes = SSPIWrapper.QueryContextAttributes( - GlobalSSPI.SSPIAuth, - _securityContext, - Interop.SspiCli.ContextAttribute.Sizes - ) as SecSizes; - } - - return _sizes; - } - } - // // 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, Interop.SspiCli.ContextFlags requestedContextFlags, ChannelBinding channelBinding) + internal NTAuthentication(bool isServer, string package, NetworkCredential credential, string spn, ContextFlagsPal requestedContextFlags, ChannelBinding channelBinding) { Initialize(isServer, package, credential, spn, requestedContextFlags, channelBinding); } private class InitializeCallbackContext { - internal InitializeCallbackContext(NTAuthentication thisPtr, bool isServer, string package, NetworkCredential credential, string spn, Interop.SspiCli.ContextFlags requestedContextFlags, ChannelBinding channelBinding) + internal InitializeCallbackContext(NTAuthentication thisPtr, bool isServer, string package, NetworkCredential credential, string spn, ContextFlagsPal requestedContextFlags, ChannelBinding channelBinding) { ThisPtr = thisPtr; IsServer = isServer; @@ -250,7 +217,7 @@ internal InitializeCallbackContext(NTAuthentication thisPtr, bool isServer, stri internal readonly string Package; internal readonly NetworkCredential Credential; internal readonly string Spn; - internal readonly Interop.SspiCli.ContextFlags RequestedContextFlags; + internal readonly ContextFlagsPal RequestedContextFlags; internal readonly ChannelBinding ChannelBinding; } @@ -260,14 +227,14 @@ private static void InitializeCallback(object state) context.ThisPtr.Initialize(context.IsServer, context.Package, context.Credential, context.Spn, context.RequestedContextFlags, context.ChannelBinding); } - private void Initialize(bool isServer, string package, NetworkCredential credential, string spn, Interop.SspiCli.ContextFlags requestedContextFlags, ChannelBinding channelBinding) + private void Initialize(bool isServer, string package, NetworkCredential credential, string spn, ContextFlagsPal requestedContextFlags, ChannelBinding channelBinding) { if (GlobalLog.IsEnabled) { GlobalLog.Print("NTAuthentication#" + LoggingHash.HashString(this) + "::.ctor() package:" + LoggingHash.ObjectToString(package) + " spn:" + LoggingHash.ObjectToString(spn) + " flags :" + requestedContextFlags.ToString()); } - _tokenSize = SSPIWrapper.GetVerifyPackageInfo(GlobalSSPI.SSPIAuth, package, true).MaxToken; + _tokenSize = NegotiateStreamPal.QueryMaxTokenSize(package); _isServer = isServer; _spn = spn; _securityContext = null; @@ -292,55 +259,17 @@ private void Initialize(bool isServer, string package, NetworkCredential credent GlobalLog.Print("NTAuthentication#" + LoggingHash.HashString(this) + "::.ctor(): using DefaultCredentials"); } - _credentialsHandle = SSPIWrapper.AcquireDefaultCredential( - GlobalSSPI.SSPIAuth, - package, - (_isServer ? Interop.SspiCli.CredentialUse.Inbound : Interop.SspiCli.CredentialUse.Outbound)); + _credentialsHandle = NegotiateStreamPal.AcquireDefaultCredential(package, _isServer); } else { - unsafe - { - SafeSspiAuthDataHandle authData = null; - try - { - Interop.SecurityStatus result = Interop.SspiCli.SspiEncodeStringsAsAuthIdentity( - credential.UserName, credential.Domain, - credential.Password, out authData); - - if (result != Interop.SecurityStatus.OK) - { - if (NetEventSource.Log.IsEnabled()) - { - NetEventSource.PrintError( - NetEventSource.ComponentType.Security, - SR.Format( - SR.net_log_operation_failed_with_error, - "SspiEncodeStringsAsAuthIdentity()", - String.Format(CultureInfo.CurrentCulture, "0x{0:X}", (int)result))); - } - - throw new Win32Exception((int)result); - } - - _credentialsHandle = SSPIWrapper.AcquireCredentialsHandle(GlobalSSPI.SSPIAuth, - package, (_isServer ? Interop.SspiCli.CredentialUse.Inbound : Interop.SspiCli.CredentialUse.Outbound), ref authData); - } - finally - { - if (authData != null) - { - authData.Dispose(); - } - } - } + _credentialsHandle = NegotiateStreamPal.AcquireCredentialsHandle(package, _isServer, credential); } } - // 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. - internal SecurityContextTokenHandle GetContextToken(out Interop.SecurityStatus status) + internal SafeDeleteContext GetContext(out SecurityStatusPal status) { + status = new SecurityStatusPal(SecurityStatusPalErrorCode.OK); if (!(IsCompleted && IsValidContext)) { if (GlobalLog.IsEnabled) @@ -363,28 +292,11 @@ internal SecurityContextTokenHandle GetContextToken(out Interop.SecurityStatus s if (!IsValidContext) { - throw new Win32Exception((int)Interop.SecurityStatus.InvalidHandle); - } - - SecurityContextTokenHandle token = null; - status = (Interop.SecurityStatus)SSPIWrapper.QuerySecurityContextToken( - GlobalSSPI.SSPIAuth, - _securityContext, - out token); - - return token; - } - - internal SecurityContextTokenHandle GetContextToken() - { - Interop.SecurityStatus status; - SecurityContextTokenHandle token = GetContextToken(out status); - if (status != Interop.SecurityStatus.OK) - { - throw new Win32Exception((int)status); + status = new SecurityStatusPal(SecurityStatusPalErrorCode.InvalidHandle); + return null; } - return token; + return _securityContext; } internal void CloseContext() @@ -396,7 +308,7 @@ internal void CloseContext() } // Accepts an incoming binary security blob and returns an outgoing binary security blob. - internal byte[] GetOutgoingBlob(byte[] incomingBlob, bool throwOnError, out Interop.SecurityStatus statusCode) + internal byte[] GetOutgoingBlob(byte[] incomingBlob, bool throwOnError, out SecurityStatusPal statusCode) { if (GlobalLog.IsEnabled) { @@ -429,35 +341,30 @@ internal byte[] GetOutgoingBlob(byte[] incomingBlob, bool throwOnError, out Inte if (!_isServer) { // client session - statusCode = (Interop.SecurityStatus)SSPIWrapper.InitializeSecurityContext( - GlobalSSPI.SSPIAuth, + statusCode = NegotiateStreamPal.InitializeSecurityContext( _credentialsHandle, ref _securityContext, _spn, _requestedContextFlags, - Interop.SspiCli.Endianness.Network, inSecurityBufferArray, outSecurityBuffer, ref _contextFlags); if (GlobalLog.IsEnabled) { - GlobalLog.Print("NTAuthentication#" + LoggingHash.HashString(this) + "::GetOutgoingBlob() SSPIWrapper.InitializeSecurityContext() returns statusCode:0x" + ((int)statusCode).ToString("x8", NumberFormatInfo.InvariantInfo) + " (" + statusCode.ToString() + ")"); + GlobalLog.Print("NTAuthentication#" + LoggingHash.HashString(this) + "::GetOutgoingBlob() SSPIWrapper.InitializeSecurityContext() returns statusCode:0x" + ((int)statusCode.ErrorCode).ToString("x8", NumberFormatInfo.InvariantInfo) + " (" + statusCode.ToString() + ")"); } - if (statusCode == Interop.SecurityStatus.CompleteNeeded) + if (statusCode.ErrorCode == SecurityStatusPalErrorCode.CompleteNeeded) { var inSecurityBuffers = new SecurityBuffer[1]; inSecurityBuffers[0] = outSecurityBuffer; - statusCode = (Interop.SecurityStatus)SSPIWrapper.CompleteAuthToken( - GlobalSSPI.SSPIAuth, - ref _securityContext, - inSecurityBuffers); + statusCode = NegotiateStreamPal.CompleteAuthToken(ref _securityContext, inSecurityBuffers); if (GlobalLog.IsEnabled) { - GlobalLog.Print("NTAuthentication#" + LoggingHash.HashString(this) + "::GetOutgoingDigestBlob() SSPIWrapper.CompleteAuthToken() returns statusCode:0x" + ((int)statusCode).ToString("x8", NumberFormatInfo.InvariantInfo) + " (" + statusCode.ToString() + ")"); + GlobalLog.Print("NTAuthentication#" + LoggingHash.HashString(this) + "::GetOutgoingDigestBlob() SSPIWrapper.CompleteAuthToken() returns statusCode:0x" + ((int)statusCode.ErrorCode).ToString("x8", NumberFormatInfo.InvariantInfo) + " (" + statusCode.ToString() + ")"); } outSecurityBuffer.token = null; @@ -466,19 +373,17 @@ internal byte[] GetOutgoingBlob(byte[] incomingBlob, bool throwOnError, out Inte else { // Server session. - statusCode = (Interop.SecurityStatus)SSPIWrapper.AcceptSecurityContext( - GlobalSSPI.SSPIAuth, + statusCode = NegotiateStreamPal.AcceptSecurityContext( _credentialsHandle, ref _securityContext, _requestedContextFlags, - Interop.SspiCli.Endianness.Network, inSecurityBufferArray, outSecurityBuffer, ref _contextFlags); if (GlobalLog.IsEnabled) { - GlobalLog.Print("NTAuthentication#" + LoggingHash.HashString(this) + "::GetOutgoingBlob() SSPIWrapper.AcceptSecurityContext() returns statusCode:0x" + ((int)statusCode).ToString("x8", NumberFormatInfo.InvariantInfo) + " (" + statusCode.ToString() + ")"); + GlobalLog.Print("NTAuthentication#" + LoggingHash.HashString(this) + "::GetOutgoingBlob() SSPIWrapper.AcceptSecurityContext() returns statusCode:0x" + ((int)statusCode.ErrorCode).ToString("x8", NumberFormatInfo.InvariantInfo) + " (" + statusCode.ToString() + ")"); } } } @@ -497,13 +402,13 @@ internal byte[] GetOutgoingBlob(byte[] incomingBlob, bool throwOnError, out Inte } - if (((int)statusCode & unchecked((int)0x80000000)) != 0) + if (NegoState.IsError(statusCode)) { CloseContext(); _isCompleted = true; if (throwOnError) { - var exception = new Win32Exception((int)statusCode); + Exception exception = NegotiateStreamPal.CreateExceptionFromError(statusCode); if (GlobalLog.IsEnabled) { GlobalLog.Leave("NTAuthentication#" + LoggingHash.HashString(this) + "::GetOutgoingBlob", "Win32Exception:" + exception); @@ -513,7 +418,7 @@ internal byte[] GetOutgoingBlob(byte[] incomingBlob, bool throwOnError, out Inte if (GlobalLog.IsEnabled) { - GlobalLog.Leave("NTAuthentication#" + LoggingHash.HashString(this) + "::GetOutgoingBlob", "null statusCode:0x" + ((int)statusCode).ToString("x8", NumberFormatInfo.InvariantInfo) + " (" + statusCode.ToString() + ")"); + GlobalLog.Leave("NTAuthentication#" + LoggingHash.HashString(this) + "::GetOutgoingBlob", "null statusCode:0x" + ((int)statusCode.ErrorCode).ToString("x8", NumberFormatInfo.InvariantInfo) + " (" + statusCode.ToString() + ")"); } return null; } @@ -523,28 +428,16 @@ internal byte[] GetOutgoingBlob(byte[] incomingBlob, bool throwOnError, out Inte SSPIHandleCache.CacheCredential(_credentialsHandle); } - // The return value from SSPI will tell us correctly if the - // handshake is over or not: http://msdn.microsoft.com/library/psdk/secspi/sspiref_67p0.htm - // we also have to consider the case in which SSPI formed a new context, in this case we're done as well. - if (statusCode == Interop.SecurityStatus.OK) + // The return value will tell us correctly if the handshake is over or not + if (statusCode.ErrorCode == SecurityStatusPalErrorCode.OK) { // Success. - if (statusCode != Interop.SecurityStatus.OK) - { - if (GlobalLog.IsEnabled) - { - GlobalLog.AssertFormat("NTAuthentication#{0}::GetOutgoingBlob()|statusCode:[0x{1:x8}] ({2}) m_SecurityContext#{3}::Handle:[{4}] [STATUS != OK]", LoggingHash.HashString(this), (int)statusCode, statusCode, LoggingHash.HashString(_securityContext), LoggingHash.ObjectToString(_securityContext)); - } - - Debug.Fail("NTAuthentication#" + LoggingHash.HashString(this) + "::GetOutgoingBlob()|statusCode:[0x" + ((int)statusCode).ToString("x8") + "] (" + statusCode + ") m_SecurityContext#" + LoggingHash.HashString(_securityContext) + "::Handle:[" + LoggingHash.ObjectToString(_securityContext) + "] [STATUS != OK]"); - } - _isCompleted = true; } else if (GlobalLog.IsEnabled) { // We need to continue. - GlobalLog.Print("NTAuthentication#" + LoggingHash.HashString(this) + "::GetOutgoingBlob() need continue statusCode:[0x" + ((int)statusCode).ToString("x8", NumberFormatInfo.InvariantInfo) + "] (" + statusCode.ToString() + ") m_SecurityContext#" + LoggingHash.HashString(_securityContext) + "::Handle:" + LoggingHash.ObjectToString(_securityContext) + "]"); + GlobalLog.Print("NTAuthentication#" + LoggingHash.HashString(this) + "::GetOutgoingBlob() need continue statusCode:[0x" + ((int)statusCode.ErrorCode).ToString("x8", NumberFormatInfo.InvariantInfo) + "] (" + statusCode.ToString() + ") m_SecurityContext#" + LoggingHash.HashString(_securityContext) + "::Handle:" + LoggingHash.ObjectToString(_securityContext) + "]"); } if (GlobalLog.IsEnabled) @@ -557,163 +450,20 @@ internal byte[] GetOutgoingBlob(byte[] incomingBlob, bool throwOnError, out Inte internal int Encrypt(byte[] buffer, int offset, int count, ref byte[] output, uint sequenceNumber) { - SecSizes sizes = Sizes; - - try - { - int maxCount = checked(Int32.MaxValue - 4 - sizes.BlockSize - sizes.SecurityTrailer); - - if (count > maxCount || count < 0) - { - throw new ArgumentOutOfRangeException(nameof(count), SR.Format(SR.net_io_out_range, maxCount)); - } - } - catch (Exception e) - { - if (!ExceptionCheck.IsFatal(e)) - { - if (GlobalLog.IsEnabled) - { - GlobalLog.Assert("NTAuthentication#" + LoggingHash.HashString(this) + "::Encrypt", "Arguments out of range."); - } - - Debug.Fail("NTAuthentication#" + LoggingHash.HashString(this) + "::Encrypt", "Arguments out of range."); - } - - throw; - } - - int resultSize = count + sizes.SecurityTrailer + sizes.BlockSize; - if (output == null || output.Length < resultSize + 4) - { - output = new byte[resultSize + 4]; - } - - // Make a copy of user data for in-place encryption. - Buffer.BlockCopy(buffer, offset, output, 4 + sizes.SecurityTrailer, count); - - // Prepare buffers TOKEN(signature), DATA and Padding. - var securityBuffer = new SecurityBuffer[3]; - securityBuffer[0] = new SecurityBuffer(output, 4, sizes.SecurityTrailer, SecurityBufferType.Token); - securityBuffer[1] = new SecurityBuffer(output, 4 + sizes.SecurityTrailer, count, SecurityBufferType.Data); - securityBuffer[2] = new SecurityBuffer(output, 4 + sizes.SecurityTrailer + count, sizes.BlockSize, SecurityBufferType.Padding); - - int errorCode; - if (IsConfidentialityFlag) - { - errorCode = SSPIWrapper.EncryptMessage(GlobalSSPI.SSPIAuth, _securityContext, securityBuffer, sequenceNumber); - } - else - { - if (IsNTLM) - { - securityBuffer[1].type |= SecurityBufferType.ReadOnlyFlag; - } - - errorCode = SSPIWrapper.MakeSignature(GlobalSSPI.SSPIAuth, _securityContext, securityBuffer, 0); - } - - if (errorCode != 0) - { - if (GlobalLog.IsEnabled) - { - GlobalLog.Print("NTAuthentication#" + LoggingHash.HashString(this) + "::Encrypt() throw Error = " + errorCode.ToString("x", NumberFormatInfo.InvariantInfo)); - } - throw new Win32Exception(errorCode); - } - - // Compacting the result. - resultSize = securityBuffer[0].size; - bool forceCopy = false; - if (resultSize != sizes.SecurityTrailer) - { - forceCopy = true; - Buffer.BlockCopy(output, securityBuffer[1].offset, output, 4 + resultSize, securityBuffer[1].size); - } - - resultSize += securityBuffer[1].size; - if (securityBuffer[2].size != 0 && (forceCopy || resultSize != (count + sizes.SecurityTrailer))) - { - Buffer.BlockCopy(output, securityBuffer[2].offset, output, 4 + resultSize, securityBuffer[2].size); - } - - resultSize += securityBuffer[2].size; - - unchecked - { - output[0] = (byte)((resultSize) & 0xFF); - output[1] = (byte)(((resultSize) >> 8) & 0xFF); - output[2] = (byte)(((resultSize) >> 16) & 0xFF); - output[3] = (byte)(((resultSize) >> 24) & 0xFF); - } - - return resultSize + 4; + return NegotiateStreamPal.Encrypt( + _securityContext, + buffer, + offset, + count, + IsConfidentialityFlag, + IsNTLM, + ref output, + sequenceNumber); } internal int Decrypt(byte[] payload, int offset, int count, out int newOffset, uint expectedSeqNumber) { - if (offset < 0 || offset > (payload == null ? 0 : payload.Length)) - { - if (GlobalLog.IsEnabled) - { - GlobalLog.Assert("NTAuthentication#" + LoggingHash.HashString(this) + "::Decrypt", "Argument 'offset' out of range."); - } - - Debug.Fail("NTAuthentication#" + LoggingHash.HashString(this) + "::Decrypt", "Argument 'offset' out of range."); - - throw new ArgumentOutOfRangeException(nameof(offset)); - } - - if (count < 0 || count > (payload == null ? 0 : payload.Length - offset)) - { - if (GlobalLog.IsEnabled) - { - GlobalLog.Assert("NTAuthentication#" + LoggingHash.HashString(this) + "::Decrypt", "Argument 'count' out of range."); - } - - Debug.Fail("NTAuthentication#" + LoggingHash.HashString(this) + "::Decrypt", "Argument 'count' out of range."); - - throw new ArgumentOutOfRangeException(nameof(count)); - } - - if (IsNTLM) - { - return DecryptNtlm(payload, offset, count, out newOffset, expectedSeqNumber); - } - - // - // Kerberos and up - // - var securityBuffer = new SecurityBuffer[2]; - securityBuffer[0] = new SecurityBuffer(payload, offset, count, SecurityBufferType.Stream); - securityBuffer[1] = new SecurityBuffer(0, SecurityBufferType.Data); - - int errorCode; - if (IsConfidentialityFlag) - { - errorCode = SSPIWrapper.DecryptMessage(GlobalSSPI.SSPIAuth, _securityContext, securityBuffer, expectedSeqNumber); - } - else - { - errorCode = SSPIWrapper.VerifySignature(GlobalSSPI.SSPIAuth, _securityContext, securityBuffer, expectedSeqNumber); - } - - if (errorCode != 0) - { - if (GlobalLog.IsEnabled) - { - GlobalLog.Print("NTAuthentication#" + LoggingHash.HashString(this) + "::Decrypt() throw Error = " + errorCode.ToString("x", NumberFormatInfo.InvariantInfo)); - } - throw new Win32Exception(errorCode); - } - - if (securityBuffer[1].type != SecurityBufferType.Data) - { - throw new InternalException(); - } - - newOffset = securityBuffer[1].offset; - return securityBuffer[1].size; + return NegotiateStreamPal.Decrypt(_securityContext, payload, offset, count, IsConfidentialityFlag, IsNTLM, out newOffset, expectedSeqNumber); } private string GetClientSpecifiedSpn() @@ -728,8 +478,7 @@ private string GetClientSpecifiedSpn() Debug.Fail("NTAuthentication: Trying to get the client SPN before handshaking is done!"); } - string spn = SSPIWrapper.QueryContextAttributes(GlobalSSPI.SSPIAuth, _securityContext, - Interop.SspiCli.ContextAttribute.ClientSpecifiedSpn) as string; + string spn = NegotiateStreamPal.QueryContextClientSpecifiedSpn(_securityContext); if (GlobalLog.IsEnabled) { @@ -740,54 +489,7 @@ private string GetClientSpecifiedSpn() private int DecryptNtlm(byte[] payload, int offset, int count, out int newOffset, uint expectedSeqNumber) { - // For the most part the arguments are verified in Encrypt(). - if (count < 16) - { - if (GlobalLog.IsEnabled) - { - GlobalLog.Assert("NTAuthentication#" + LoggingHash.HashString(this) + "::DecryptNtlm", "Argument 'count' out of range."); - } - - Debug.Fail("NTAuthentication#" + LoggingHash.HashString(this) + "::DecryptNtlm", "Argument 'count' out of range."); - - throw new ArgumentOutOfRangeException(nameof(count)); - } - - var securityBuffer = new SecurityBuffer[2]; - securityBuffer[0] = new SecurityBuffer(payload, offset, 16, SecurityBufferType.Token); - securityBuffer[1] = new SecurityBuffer(payload, offset + 16, count - 16, SecurityBufferType.Data); - - int errorCode; - SecurityBufferType realDataType = SecurityBufferType.Data; - - if (IsConfidentialityFlag) - { - errorCode = SSPIWrapper.DecryptMessage(GlobalSSPI.SSPIAuth, _securityContext, securityBuffer, expectedSeqNumber); - } - else - { - realDataType |= SecurityBufferType.ReadOnlyFlag; - securityBuffer[1].type = realDataType; - errorCode = SSPIWrapper.VerifySignature(GlobalSSPI.SSPIAuth, _securityContext, securityBuffer, expectedSeqNumber); - } - - if (errorCode != 0) - { - if (GlobalLog.IsEnabled) - { - GlobalLog.Print("NTAuthentication#" + LoggingHash.HashString(this) + "::Decrypt() throw Error = " + errorCode.ToString("x", NumberFormatInfo.InvariantInfo)); - } - - throw new Win32Exception(errorCode); - } - - if (securityBuffer[1].type != realDataType) - { - throw new InternalException(); - } - - newOffset = securityBuffer[1].offset; - return securityBuffer[1].size; + return NegotiateStreamPal.DecryptNtlm(_securityContext, payload, offset, count, IsConfidentialityFlag, out newOffset, expectedSeqNumber); } } } diff --git a/src/System.Net.Security/src/System/Net/NegotiateStreamPal.Unix.cs b/src/System.Net.Security/src/System/Net/NegotiateStreamPal.Unix.cs new file mode 100644 index 000000000000..b1769368f2e0 --- /dev/null +++ b/src/System.Net.Security/src/System/Net/NegotiateStreamPal.Unix.cs @@ -0,0 +1,298 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// 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.Authentication; +using System.Security.Authentication.ExtendedProtection; +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 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 IIdentity GetIdentity(NTAuthentication context) + { + Debug.Assert(!context.IsServer, "GetIdentity: Server is not supported"); + + string name = context.Spn; + string protocol = context.ProtocolName; + + return new GenericIdentity(name, protocol); + + } + + internal static string QueryContextAssociatedName(SafeDeleteContext securityContext) + { + throw new PlatformNotSupportedException(SR.net_nego_server_not_supported); + } + + internal static string QueryContextAuthenticationPackage(SafeDeleteContext securityContext) + { + return NegotiationInfoClass.Kerberos; + } + + internal static int QueryMaxTokenSize(string package) + { + // This value is not used on Unix + return 0; + } + + internal static string QueryContextClientSpecifiedSpn(SafeDeleteContext securityContext) + { + throw new PlatformNotSupportedException(SR.net_nego_server_not_supported); + } + + 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) + { + if (isServer) + { + throw new PlatformNotSupportedException(SR.net_nego_server_not_supported); + } + + bool isNtlm = string.Equals(package, NegotiationInfoClass.NTLM); + + if (isNtlm) + { + throw new PlatformNotSupportedException(SR.net_nego_ntlm_not_supported); + } + + try + { + // Note: In client case, equivalent of default credentials is to use previous, cached Kerberos TGT to get service-specific ticket. + return (string.IsNullOrEmpty(credential.UserName) || string.IsNullOrEmpty(credential.Password)) ? + new SafeFreeNegoCredentials(string.Empty, string.Empty, string.Empty) : + new SafeFreeNegoCredentials(credential.UserName, credential.Password, credential.Domain); + } + catch(Exception ex) + { + throw new Win32Exception(NTE_FAIL, ex.Message); + } + } + + internal static SecurityStatusPal InitializeSecurityContext( + SafeFreeCredentials credentialsHandle, + ref SafeDeleteContext securityContext, + string spn, + ContextFlagsPal requestedContextFlags, + SecurityBuffer[] inSecurityBufferArray, + SecurityBuffer outSecurityBuffer, + ref ContextFlagsPal contextFlags) + { + // TODO (Issue #3718): The second buffer can contain a channel binding which is not supported + if ((null != inSecurityBufferArray) && (inSecurityBufferArray.Length > 1)) + { + throw new PlatformNotSupportedException(SR.net_nego_channel_binding_not_supported); + } + + if ((null != inSecurityBufferArray) && (inSecurityBufferArray.Length > 0) && (inSecurityBufferArray[0].type == SecurityBufferType.ChannelBindings)) + { + throw new PlatformNotSupportedException(SR.net_nego_channel_binding_not_supported); + } + + return EstablishSecurityContext( + (SafeFreeNegoCredentials)credentialsHandle, + ref securityContext, + false, + spn, + requestedContextFlags, + ((inSecurityBufferArray != null && inSecurityBufferArray.Length != 0) ? inSecurityBufferArray[0] : null), + outSecurityBuffer, + ref contextFlags); + } + + internal static SecurityStatusPal CompleteAuthToken( + ref SafeDeleteContext securityContext, + SecurityBuffer[] inSecurityBufferArray) + { + return new SecurityStatusPal(SecurityStatusPalErrorCode.OK); + } + + internal static SecurityStatusPal AcceptSecurityContext( + SafeFreeCredentials credentialsHandle, + ref SafeDeleteContext securityContext, + ContextFlagsPal requestedContextFlags, + SecurityBuffer[] inSecurityBufferArray, + SecurityBuffer outSecurityBuffer, + ref ContextFlagsPal contextFlags) + { + throw new PlatformNotSupportedException(SR.net_nego_server_not_supported); + } + + internal static void ValidateImpersonationLevel(TokenImpersonationLevel impersonationLevel) + { + if (impersonationLevel != TokenImpersonationLevel.Identification) + { + throw new ArgumentOutOfRangeException(nameof(impersonationLevel), impersonationLevel.ToString(), + SR.net_auth_supported_impl_levels); + } + } + + internal static Win32Exception CreateExceptionFromError(SecurityStatusPal statusCode) + { + return new Win32Exception(NTE_FAIL, statusCode.ToString()); + } + + internal static int Encrypt( + SafeDeleteContext securityContext, + byte[] buffer, + int offset, + int count, + bool isConfidential, + bool isNtlm, + ref byte[] output, + uint sequenceNumber) + { + Debug.Assert(!isNtlm, "Encrypt: NTLM is not yet supported"); + SafeDeleteNegoContext gssContext = (SafeDeleteNegoContext) securityContext; + byte[] tempOutput = Interop.GssApi.Encrypt(gssContext.GssContext, isConfidential, buffer, offset, count); + + // Create space for prefixing with the length + const int prefixLength = 4; + output = new byte[tempOutput.Length + prefixLength]; + Array.Copy(tempOutput, 0, output, prefixLength, tempOutput.Length); + int resultSize = tempOutput.Length; + unchecked + { + output[0] = (byte)((resultSize) & 0xFF); + output[1] = (byte)(((resultSize) >> 8) & 0xFF); + output[2] = (byte)(((resultSize) >> 16) & 0xFF); + output[3] = (byte)(((resultSize) >> 24) & 0xFF); + } + + return resultSize + 4; + } + + internal static int Decrypt( + SafeDeleteContext securityContext, + byte[] buffer, + int offset, + int count, + bool isConfidential, + bool isNtlm, + out int newOffset, + uint sequenceNumber) + { + if (offset < 0 || offset > (buffer == null ? 0 : buffer.Length)) + { + if (GlobalLog.IsEnabled) + { + GlobalLog.Assert("NTAuthentication#" + LoggingHash.HashString(securityContext) + "::Decrypt", "Argument 'offset' out of range."); + } + + Debug.Fail("NTAuthentication#" + LoggingHash.HashString(securityContext) + "::Decrypt", "Argument 'offset' out of range."); + + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (count < 0 || count > (buffer == null ? 0 : buffer.Length - offset)) + { + if (GlobalLog.IsEnabled) + { + GlobalLog.Assert("NTAuthentication#" + LoggingHash.HashString(securityContext) + "::Decrypt", "Argument 'count' out of range."); + } + + Debug.Fail("NTAuthentication#" + LoggingHash.HashString(securityContext) + "::Decrypt", "Argument 'count' out of range."); + + throw new ArgumentOutOfRangeException(nameof(count)); + } + + newOffset = offset; + return Interop.GssApi.Decrypt(((SafeDeleteNegoContext)securityContext).GssContext, buffer, offset, count); + } + + internal static int DecryptNtlm( + SafeDeleteContext securityContext, + byte[] buffer, + int offset, + int count, + bool isConfidential, + out int newOffset, + uint sequenceNumber) + { + throw new PlatformNotSupportedException(SR.net_nego_ntlm_not_supported); + } + + private static SecurityStatusPal EstablishSecurityContext( + SafeFreeNegoCredentials credential, + ref SafeDeleteContext context, + bool isNtlm, + string targetName, + ContextFlagsPal inFlags, + SecurityBuffer inputBuffer, + SecurityBuffer outputBuffer, + ref ContextFlagsPal outFlags) + { + Debug.Assert(!isNtlm, "EstablishSecurityContext: NTLM is not yet supported"); + + if (context == null) + { + context = new SafeDeleteNegoContext(credential, targetName); + } + + SafeDeleteNegoContext negoContext = (SafeDeleteNegoContext)context; + try + { + Interop.NetSecurityNative.GssFlags inputFlags = ContextFlagsAdapterPal.GetInteropFromContextFlagsPal(inFlags); + uint outputFlags; + SafeGssContextHandle contextHandle = negoContext.GssContext; + bool done = Interop.GssApi.EstablishSecurityContext( + ref contextHandle, + credential.GssCredential, + isNtlm, + negoContext.TargetName, + inputFlags, + inputBuffer?.token, + out outputBuffer.token, + out outputFlags); + + Debug.Assert(outputBuffer.token != null, "Unexpected null buffer returned by GssApi"); + outputBuffer.size = outputBuffer.token.Length; + outputBuffer.offset = 0; + + outFlags = ContextFlagsAdapterPal.GetContextFlagsPalFromInterop((Interop.NetSecurityNative.GssFlags)outputFlags); + + // 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); + } + + SecurityStatusPalErrorCode errorCode = done ? SecurityStatusPalErrorCode.CompleteNeeded : SecurityStatusPalErrorCode.ContinueNeeded; + return new SecurityStatusPal(errorCode); + } + catch(Exception ex) + { + //TODO (Issue #5890): Print exception until issue is fixed + Debug.Write("Exception Caught. - " + ex); + if (GlobalLog.IsEnabled) + { + GlobalLog.Print("Exception Caught. - " + ex); + } + + return new SecurityStatusPal(SecurityStatusPalErrorCode.InternalError, ex); + } + } + } +} diff --git a/src/System.Net.Security/src/System/Net/NegotiateStreamPal.Windows.cs b/src/System.Net.Security/src/System/Net/NegotiateStreamPal.Windows.cs new file mode 100644 index 000000000000..b26d411c4200 --- /dev/null +++ b/src/System.Net.Security/src/System/Net/NegotiateStreamPal.Windows.cs @@ -0,0 +1,458 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Security; +using System.Security.Principal; +using System.Threading; +using System.ComponentModel; +using System.Security.Authentication; +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 class NegotiateStreamPal + { + internal static IIdentity GetIdentity(NTAuthentication context) + { + IIdentity result = null; + string name = context.IsServer ? context.AssociatedName : context.Spn; + string protocol = context.ProtocolName; + + if (context.IsServer) + { + SecurityContextTokenHandle token = null; + try + { + SecurityStatusPal status; + SafeDeleteContext securityContext = context.GetContext(out status); + if (status.ErrorCode != SecurityStatusPalErrorCode.OK) + { + throw new Win32Exception((int)SecurityStatusAdapterPal.GetInteropFromSecurityStatusPal(status)); + } + + // 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.SecurityStatus winStatus = (Interop.SecurityStatus)SSPIWrapper.QuerySecurityContextToken( + GlobalSSPI.SSPIAuth, + securityContext, + out token); + if (winStatus != Interop.SecurityStatus.OK) + { + throw new Win32Exception((int)winStatus); + } + string authtype = context.ProtocolName; + + // TODO #5241: + // The following call was also specifying WindowsAccountType.Normal, true. + // WindowsIdentity.IsAuthenticated is no longer supported in CoreFX. + result = new WindowsIdentity(token.DangerousGetHandle(), authtype); + return result; + } + catch (SecurityException) + { + // Ignore and construct generic Identity if failed due to security problem. + } + finally + { + if (token != null) + { + token.Dispose(); + } + } + } + + // On the client we don't have access to the remote side identity. + result = new GenericIdentity(name, protocol); + return result; + } + + internal static string QueryContextAssociatedName(SafeDeleteContext securityContext) + { + return SSPIWrapper.QueryContextAttributes(GlobalSSPI.SSPIAuth, securityContext, Interop.SspiCli.ContextAttribute.Names) as string; + } + + internal static string QueryContextAuthenticationPackage(SafeDeleteContext securityContext) + { + var negotiationInfoClass = SSPIWrapper.QueryContextAttributes(GlobalSSPI.SSPIAuth, securityContext, Interop.SspiCli.ContextAttribute.NegotiationInfo) as NegotiationInfoClass; + return negotiationInfoClass?.AuthenticationPackage; + } + + internal static int QueryMaxTokenSize(string package) + { + return SSPIWrapper.GetVerifyPackageInfo(GlobalSSPI.SSPIAuth, package, true).MaxToken; + } + + internal static string QueryContextClientSpecifiedSpn(SafeDeleteContext securityContext) + { + return SSPIWrapper.QueryContextAttributes(GlobalSSPI.SSPIAuth, securityContext, Interop.SspiCli.ContextAttribute.ClientSpecifiedSpn) as string; + } + + internal static SafeFreeCredentials AcquireDefaultCredential(string package, bool isServer) + { + return SSPIWrapper.AcquireDefaultCredential( + GlobalSSPI.SSPIAuth, + package, + (isServer ? Interop.SspiCli.CredentialUse.Inbound : Interop.SspiCli.CredentialUse.Outbound)); + } + + internal unsafe static SafeFreeCredentials AcquireCredentialsHandle(string package, bool isServer, NetworkCredential credential) + { + SafeSspiAuthDataHandle authData = null; + try + { + Interop.SecurityStatus result = Interop.SspiCli.SspiEncodeStringsAsAuthIdentity( + credential.UserName, credential.Domain, + credential.Password, out authData); + + if (result != Interop.SecurityStatus.OK) + { + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.PrintError( + NetEventSource.ComponentType.Security, + SR.Format( + SR.net_log_operation_failed_with_error, + "SspiEncodeStringsAsAuthIdentity()", + String.Format(CultureInfo.CurrentCulture, "0x{0:X}", (int)result))); + } + + throw new Win32Exception((int)result); + } + + return SSPIWrapper.AcquireCredentialsHandle(GlobalSSPI.SSPIAuth, + package, (isServer ? Interop.SspiCli.CredentialUse.Inbound : Interop.SspiCli.CredentialUse.Outbound), ref authData); + } + finally + { + if (authData != null) + { + authData.Dispose(); + } + } + } + + internal static SecurityStatusPal InitializeSecurityContext( + SafeFreeCredentials credentialsHandle, + ref SafeDeleteContext securityContext, + string spn, + ContextFlagsPal requestedContextFlags, + SecurityBuffer[] inSecurityBufferArray, + SecurityBuffer outSecurityBuffer, + ref ContextFlagsPal contextFlags) + { + Interop.SspiCli.ContextFlags outContextFlags = Interop.SspiCli.ContextFlags.Zero; + Interop.SecurityStatus winStatus = (Interop.SecurityStatus)SSPIWrapper.InitializeSecurityContext( + GlobalSSPI.SSPIAuth, + credentialsHandle, + ref securityContext, + spn, + ContextFlagsAdapterPal.GetInteropFromContextFlagsPal(requestedContextFlags), + Interop.SspiCli.Endianness.Network, + inSecurityBufferArray, + outSecurityBuffer, + ref outContextFlags); + + contextFlags = ContextFlagsAdapterPal.GetContextFlagsPalFromInterop(outContextFlags); + return SecurityStatusAdapterPal.GetSecurityStatusPalFromInterop(winStatus); + } + + internal static SecurityStatusPal CompleteAuthToken( + ref SafeDeleteContext securityContext, + SecurityBuffer[] inSecurityBufferArray) + { + Interop.SecurityStatus winStatus = (Interop.SecurityStatus)SSPIWrapper.CompleteAuthToken( + GlobalSSPI.SSPIAuth, + ref securityContext, + inSecurityBufferArray); + return SecurityStatusAdapterPal.GetSecurityStatusPalFromInterop(winStatus); + } + + internal static SecurityStatusPal AcceptSecurityContext( + SafeFreeCredentials credentialsHandle, + ref SafeDeleteContext securityContext, + ContextFlagsPal requestedContextFlags, + SecurityBuffer[] inSecurityBufferArray, + SecurityBuffer outSecurityBuffer, + ref ContextFlagsPal contextFlags) + { + Interop.SspiCli.ContextFlags outContextFlags = Interop.SspiCli.ContextFlags.Zero; + Interop.SecurityStatus winStatus = (Interop.SecurityStatus)SSPIWrapper.AcceptSecurityContext( + GlobalSSPI.SSPIAuth, + credentialsHandle, + ref securityContext, + ContextFlagsAdapterPal.GetInteropFromContextFlagsPal(requestedContextFlags), + Interop.SspiCli.Endianness.Network, + inSecurityBufferArray, + outSecurityBuffer, + ref outContextFlags); + + contextFlags = ContextFlagsAdapterPal.GetContextFlagsPalFromInterop(outContextFlags); + return SecurityStatusAdapterPal.GetSecurityStatusPalFromInterop(winStatus); + } + + 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); + } + } + + internal static Win32Exception CreateExceptionFromError(SecurityStatusPal statusCode) + { + return new Win32Exception((int)SecurityStatusAdapterPal.GetInteropFromSecurityStatusPal(statusCode)); + } + + internal static int Encrypt( + SafeDeleteContext securityContext, + byte[] buffer, + int offset, + int count, + bool isConfidential, + bool isNtlm, + ref byte[] output, + uint sequenceNumber) + { + SecSizes sizes = SSPIWrapper.QueryContextAttributes( + GlobalSSPI.SSPIAuth, + securityContext, + Interop.SspiCli.ContextAttribute.Sizes + ) as SecSizes; + + try + { + int maxCount = checked(Int32.MaxValue - 4 - sizes.BlockSize - sizes.SecurityTrailer); + + if (count > maxCount || count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), SR.Format(SR.net_io_out_range, maxCount)); + } + } + catch (Exception e) + { + if (!ExceptionCheck.IsFatal(e)) + { + if (GlobalLog.IsEnabled) + { + GlobalLog.Assert("NTAuthentication#" + LoggingHash.HashString(securityContext) + "::Encrypt", "Arguments out of range."); + } + + Debug.Fail("NTAuthentication#" + LoggingHash.HashString(securityContext) + "::Encrypt", "Arguments out of range."); + } + + throw; + } + + int resultSize = count + sizes.SecurityTrailer + sizes.BlockSize; + if (output == null || output.Length < resultSize + 4) + { + output = new byte[resultSize + 4]; + } + + // Make a copy of user data for in-place encryption. + Buffer.BlockCopy(buffer, offset, output, 4 + sizes.SecurityTrailer, count); + + // Prepare buffers TOKEN(signature), DATA and Padding. + var securityBuffer = new SecurityBuffer[3]; + securityBuffer[0] = new SecurityBuffer(output, 4, sizes.SecurityTrailer, SecurityBufferType.Token); + securityBuffer[1] = new SecurityBuffer(output, 4 + sizes.SecurityTrailer, count, SecurityBufferType.Data); + securityBuffer[2] = new SecurityBuffer(output, 4 + sizes.SecurityTrailer + count, sizes.BlockSize, SecurityBufferType.Padding); + + int errorCode; + if (isConfidential) + { + errorCode = SSPIWrapper.EncryptMessage(GlobalSSPI.SSPIAuth, securityContext, securityBuffer, sequenceNumber); + } + else + { + if (isNtlm) + { + securityBuffer[1].type |= SecurityBufferType.ReadOnlyFlag; + } + + errorCode = SSPIWrapper.MakeSignature(GlobalSSPI.SSPIAuth, securityContext, securityBuffer, 0); + } + + if (errorCode != 0) + { + if (GlobalLog.IsEnabled) + { + GlobalLog.Print("NTAuthentication#" + LoggingHash.HashString(securityContext) + "::Encrypt() throw Error = " + errorCode.ToString("x", NumberFormatInfo.InvariantInfo)); + } + throw new Win32Exception(errorCode); + } + + // Compacting the result. + resultSize = securityBuffer[0].size; + bool forceCopy = false; + if (resultSize != sizes.SecurityTrailer) + { + forceCopy = true; + Buffer.BlockCopy(output, securityBuffer[1].offset, output, 4 + resultSize, securityBuffer[1].size); + } + + resultSize += securityBuffer[1].size; + if (securityBuffer[2].size != 0 && (forceCopy || resultSize != (count + sizes.SecurityTrailer))) + { + Buffer.BlockCopy(output, securityBuffer[2].offset, output, 4 + resultSize, securityBuffer[2].size); + } + + resultSize += securityBuffer[2].size; + unchecked + { + // TODO (Issue #6063): Should be offset by offset + output[0] = (byte)((resultSize) & 0xFF); + output[1] = (byte)(((resultSize) >> 8) & 0xFF); + output[2] = (byte)(((resultSize) >> 16) & 0xFF); + output[3] = (byte)(((resultSize) >> 24) & 0xFF); + } + + return resultSize + 4; + } + + internal static int Decrypt( + SafeDeleteContext securityContext, + byte[] buffer, + int offset, + int count, + bool isConfidential, + bool isNtlm, + out int newOffset, + uint sequenceNumber) + { + if (offset < 0 || offset > (buffer == null ? 0 : buffer.Length)) + { + if (GlobalLog.IsEnabled) + { + GlobalLog.Assert("NTAuthentication#" + LoggingHash.HashString(securityContext) + "::Decrypt", "Argument 'offset' out of range."); + } + + Debug.Fail("NTAuthentication#" + LoggingHash.HashString(securityContext) + "::Decrypt", "Argument 'offset' out of range."); + + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + if (count < 0 || count > (buffer == null ? 0 : buffer.Length - offset)) + { + if (GlobalLog.IsEnabled) + { + GlobalLog.Assert("NTAuthentication#" + LoggingHash.HashString(securityContext) + "::Decrypt", "Argument 'count' out of range."); + } + + Debug.Fail("NTAuthentication#" + LoggingHash.HashString(securityContext) + "::Decrypt", "Argument 'count' out of range."); + + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (isNtlm) + { + return DecryptNtlm(securityContext, buffer, offset, count, isConfidential, out newOffset, sequenceNumber); + } + + // + // Kerberos and up + // + var securityBuffer = new SecurityBuffer[2]; + securityBuffer[0] = new SecurityBuffer(buffer, offset, count, SecurityBufferType.Stream); + securityBuffer[1] = new SecurityBuffer(0, SecurityBufferType.Data); + + int errorCode; + if (isConfidential) + { + errorCode = SSPIWrapper.DecryptMessage(GlobalSSPI.SSPIAuth, securityContext, securityBuffer, sequenceNumber); + } + else + { + errorCode = SSPIWrapper.VerifySignature(GlobalSSPI.SSPIAuth, securityContext, securityBuffer, sequenceNumber); + } + + if (errorCode != 0) + { + if (GlobalLog.IsEnabled) + { + GlobalLog.Print("NTAuthentication#"+ "::Decrypt() throw Error = " + errorCode.ToString("x", NumberFormatInfo.InvariantInfo)); + } + throw new Win32Exception(errorCode); + } + + if (securityBuffer[1].type != SecurityBufferType.Data) + { + throw new InternalException(); + } + + newOffset = securityBuffer[1].offset; + return securityBuffer[1].size; + } + + internal static int DecryptNtlm( + SafeDeleteContext securityContext, + byte[] buffer, + int offset, + int count, + bool isConfidential, + out int newOffset, + uint sequenceNumber) + { + const int ntlmSignatureLength = 16; + // For the most part the arguments are verified in Decrypt(). + if (count < ntlmSignatureLength) + { + if (GlobalLog.IsEnabled) + { + GlobalLog.Assert("NTAuthentication#" + LoggingHash.HashString(securityContext) + "::DecryptNtlm", "Argument 'count' out of range."); + } + + Debug.Fail("NTAuthentication#" + LoggingHash.HashString(securityContext) + "::DecryptNtlm", "Argument 'count' out of range."); + + throw new ArgumentOutOfRangeException(nameof(count)); + } + + var securityBuffer = new SecurityBuffer[2]; + securityBuffer[0] = new SecurityBuffer(buffer, offset, ntlmSignatureLength, SecurityBufferType.Token); + securityBuffer[1] = new SecurityBuffer(buffer, offset + ntlmSignatureLength, count - ntlmSignatureLength, SecurityBufferType.Data); + + int errorCode; + SecurityBufferType realDataType = SecurityBufferType.Data; + + if (isConfidential) + { + errorCode = SSPIWrapper.DecryptMessage(GlobalSSPI.SSPIAuth, securityContext, securityBuffer, sequenceNumber); + } + else + { + realDataType |= SecurityBufferType.ReadOnlyFlag; + securityBuffer[1].type = realDataType; + errorCode = SSPIWrapper.VerifySignature(GlobalSSPI.SSPIAuth, securityContext, securityBuffer, sequenceNumber); + } + + if (errorCode != 0) + { + if (GlobalLog.IsEnabled) + { + GlobalLog.Print("NTAuthentication#" + LoggingHash.HashString(securityContext) + "::Decrypt() throw Error = " + errorCode.ToString("x", NumberFormatInfo.InvariantInfo)); + } + throw new Win32Exception(errorCode); + } + + if (securityBuffer[1].type != realDataType) + { + throw new InternalException(); + } + + newOffset = securityBuffer[1].offset; + return securityBuffer[1].size; + } + } +} diff --git a/src/System.Net.Security/src/System/Net/NegotiationInfoClass.cs b/src/System.Net.Security/src/System/Net/NegotiationInfoClass.cs new file mode 100644 index 000000000000..f3d9884006da --- /dev/null +++ b/src/System.Net.Security/src/System/Net/NegotiationInfoClass.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace System.Net +{ + // This class is used to determine if NTLM or + // Kerberos are used in the context of a Negotiate handshake + internal partial class NegotiationInfoClass + { + internal const string NTLM = "NTLM"; + internal const string Kerberos = "Kerberos"; + internal const string Negotiate = "Negotiate"; + } +} diff --git a/src/System.Net.Security/src/System/Net/SecureProtocols/NegoState.Unix.cs b/src/System.Net.Security/src/System/Net/SecureProtocols/NegoState.Unix.cs deleted file mode 100644 index c15d01c123ff..000000000000 --- a/src/System.Net.Security/src/System/Net/SecureProtocols/NegoState.Unix.cs +++ /dev/null @@ -1,158 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// 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.Security.Authentication; -using System.Security.Authentication.ExtendedProtection; - -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. - // - // This is part of the NegotiateStream PAL. - // - internal class NegoState - { - internal const int MaxReadFrameSize = 64 * 1024; - internal const int MaxWriteDataSize = 63 * 1024; // 1k for the framing and trailer that is always less as per SSPI. - - internal NegoState(Stream innerStream, bool leaveStreamOpen) - { - throw new PlatformNotSupportedException(); - } - - internal static string DefaultPackage - { - get - { - throw new PlatformNotSupportedException(); - } - } - - internal void ValidateCreateContext(string package, - NetworkCredential credential, - string servicePrincipalName, - ExtendedProtectionPolicy policy, - ProtectionLevel protectionLevel, - TokenImpersonationLevel impersonationLevel) - { - throw new PlatformNotSupportedException(); - } - - internal void ValidateCreateContext( - string package, - bool isServer, - NetworkCredential credential, - string servicePrincipalName, - ChannelBinding channelBinding, - ProtectionLevel protectionLevel, - TokenImpersonationLevel impersonationLevel - ) - { - throw new PlatformNotSupportedException(); - } - - internal bool IsAuthenticated - { - get - { - throw new PlatformNotSupportedException(); - } - } - - internal bool IsMutuallyAuthenticated - { - get - { - throw new PlatformNotSupportedException(); - } - } - - internal bool IsEncrypted - { - get - { - throw new PlatformNotSupportedException(); - } - } - - internal bool IsSigned - { - get - { - throw new PlatformNotSupportedException(); - } - } - - internal bool IsServer - { - get - { - throw new PlatformNotSupportedException(); - } - } - - internal bool CanGetSecureStream - { - get - { - throw new PlatformNotSupportedException(); - } - } - - internal TokenImpersonationLevel AllowedImpersonation - { - get - { - throw new PlatformNotSupportedException(); - } - } - - internal IIdentity GetIdentity() - { - throw new PlatformNotSupportedException(); - } - - internal void CheckThrow(bool authSucessCheck) - { - throw new PlatformNotSupportedException(); - } - - // - // This is to not depend on GC&SafeHandle class if the context is not needed anymore. - // - internal void Close() - { - throw new PlatformNotSupportedException(); - } - - internal void ProcessAuthentication(LazyAsyncResult lazyResult) - { - throw new PlatformNotSupportedException(); - } - - internal void EndProcessAuthentication(IAsyncResult result) - { - throw new PlatformNotSupportedException(); - } - - - internal int EncryptData(byte[] buffer, int offset, int count, ref byte[] outBuffer) - { - throw new PlatformNotSupportedException(); - } - - internal int DecryptData(byte[] buffer, int offset, int count, out int newOffset) - { - throw new PlatformNotSupportedException(); - } - } -} diff --git a/src/System.Net.Security/src/System/Net/SecureProtocols/NegoState.Windows.cs b/src/System.Net.Security/src/System/Net/SecureProtocols/NegoState.cs similarity index 85% rename from src/System.Net.Security/src/System/Net/SecureProtocols/NegoState.Windows.cs rename to src/System.Net.Security/src/System/Net/SecureProtocols/NegoState.cs index b496656a3413..e0c520b7818e 100644 --- a/src/System.Net.Security/src/System/Net/SecureProtocols/NegoState.Windows.cs +++ b/src/System.Net.Security/src/System/Net/SecureProtocols/NegoState.cs @@ -16,14 +16,10 @@ 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. - // - // This is part of the NegotiateStream PAL. + // user data encryption // internal class NegoState { - private const int ERROR_TRUST_FAILURE = 1790; // Used to serialize protectionLevel or impersonationLevel mismatch error to the remote side. - private static readonly byte[] s_emptyMessage = new byte[0]; private static readonly AsyncCallback s_readCallback = new AsyncCallback(ReadCallback); private static readonly AsyncCallback s_writeCallback = new AsyncCallback(WriteCallback); @@ -38,6 +34,7 @@ internal class NegoState private int _nestedAuth; + internal const int ERROR_TRUST_FAILURE = 1790; // Used to serialize protectionLevel or impersonationLevel mismatch error to the remote side. internal const int MaxReadFrameSize = 64 * 1024; internal const int MaxWriteDataSize = 63 * 1024; // 1k for the framing and trailer that is always less as per SSPI. @@ -72,6 +69,12 @@ internal static string DefaultPackage } } + internal IIdentity GetIdentity() + { + CheckThrow(true); + return NegotiateStreamPal.GetIdentity(_context); + } + internal void ValidateCreateContext( string package, NetworkCredential credential, @@ -127,13 +130,7 @@ internal void ValidateCreateContext( throw new ArgumentNullException(nameof(servicePrincipalName)); } - if (impersonationLevel != TokenImpersonationLevel.Identification && - impersonationLevel != TokenImpersonationLevel.Impersonation && - impersonationLevel != TokenImpersonationLevel.Delegation) - { - throw new ArgumentOutOfRangeException(nameof(impersonationLevel), impersonationLevel.ToString(), SR.net_auth_supported_impl_levels); - } - + NegotiateStreamPal.ValidateImpersonationLevel(impersonationLevel); if (_context != null && IsServer != isServer) { throw new InvalidOperationException(SR.net_auth_client_server); @@ -149,35 +146,34 @@ internal void ValidateCreateContext( _writeSequenceNumber = 0; _readSequenceNumber = 0; - Interop.SspiCli.ContextFlags flags = Interop.SspiCli.ContextFlags.Connection; + ContextFlagsPal flags = ContextFlagsPal.Connection; // A workaround for the client when talking to Win9x on the server side. if (protectionLevel == ProtectionLevel.None && !isServer) { package = NegotiationInfoClass.NTLM; } - else if (protectionLevel == ProtectionLevel.EncryptAndSign) { - flags |= Interop.SspiCli.ContextFlags.Confidentiality; + flags |= ContextFlagsPal.Confidentiality; } else if (protectionLevel == ProtectionLevel.Sign) { // Assuming user expects NT4 SP4 and above. - flags |= Interop.SspiCli.ContextFlags.ReplayDetect | Interop.SspiCli.ContextFlags.SequenceDetect | Interop.SspiCli.ContextFlags.InitIntegrity; + flags |= (ContextFlagsPal.ReplayDetect | ContextFlagsPal.SequenceDetect | ContextFlagsPal.InitIntegrity); } if (isServer) { if (_extendedProtectionPolicy.PolicyEnforcement == PolicyEnforcement.WhenSupported) { - flags |= Interop.SspiCli.ContextFlags.AllowMissingBindings; + flags |= ContextFlagsPal.AllowMissingBindings; } if (_extendedProtectionPolicy.PolicyEnforcement != PolicyEnforcement.Never && _extendedProtectionPolicy.ProtectionScenario == ProtectionScenario.TrustedProxy) { - flags |= Interop.SspiCli.ContextFlags.ProxyBindings; + flags |= ContextFlagsPal.ProxyBindings; } } else @@ -185,17 +181,17 @@ internal void ValidateCreateContext( // Server side should not request any of these flags. if (protectionLevel != ProtectionLevel.None) { - flags |= Interop.SspiCli.ContextFlags.MutualAuth; + flags |= ContextFlagsPal.MutualAuth; } if (impersonationLevel == TokenImpersonationLevel.Identification) { - flags |= Interop.SspiCli.ContextFlags.InitIdentify; + flags |= ContextFlagsPal.InitIdentify; } if (impersonationLevel == TokenImpersonationLevel.Delegation) { - flags |= Interop.SspiCli.ContextFlags.Delegate; + flags |= ContextFlagsPal.Delegate; } } @@ -313,48 +309,6 @@ private bool HandshakeComplete } } - internal IIdentity GetIdentity() - { - CheckThrow(true); - - IIdentity result = null; - string name = _context.IsServer ? _context.AssociatedName : _context.Spn; - string protocol = "NTLM"; - - protocol = _context.ProtocolName; - - if (_context.IsServer) - { - SecurityContextTokenHandle token = null; - try - { - token = _context.GetContextToken(); - string authtype = _context.ProtocolName; - - // TODO #5241: - // The following call was also specifying WindowsAccountType.Normal, true. - // WindowsIdentity.IsAuthenticated is no longer supported in CoreFX. - result = new WindowsIdentity(token.DangerousGetHandle(), authtype); - return result; - } - catch (SecurityException) - { - // Ignore and construct generic Identity if failed due to security problem. - } - finally - { - if (token != null) - { - token.Dispose(); - } - } - } - - // On the client we don't have access to the remote side identity. - result = new GenericIdentity(name, protocol); - return result; - } - internal void CheckThrow(bool authSucessCheck) { if (_exception != null) @@ -483,16 +437,16 @@ private bool CheckSpn() // private void StartSendBlob(byte[] message, LazyAsyncResult lazyResult) { - Win32Exception win32exception = null; + Exception exception = null; if (message != s_emptyMessage) { - message = GetOutgoingBlob(message, ref win32exception); + message = GetOutgoingBlob(message, ref exception); } - if (win32exception != null) + if (exception != null) { // Signal remote side on a failed attempt. - StartSendAuthResetSignal(lazyResult, message, win32exception); + StartSendAuthResetSignal(lazyResult, message, exception); return; } @@ -500,7 +454,7 @@ private void StartSendBlob(byte[] message, LazyAsyncResult lazyResult) { if (_context.IsServer && !CheckSpn()) { - Exception exception = new AuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch); + exception = new AuthenticationException(SR.net_auth_bad_client_creds_or_target_mismatch); int statusCode = ERROR_TRUST_FAILURE; message = new byte[8]; //sizeof(long) @@ -516,7 +470,7 @@ private void StartSendBlob(byte[] message, LazyAsyncResult lazyResult) if (PrivateImpersonationLevel < _expectedImpersonationLevel) { - Exception exception = new AuthenticationException(SR.Format(SR.net_auth_context_expectation, _expectedImpersonationLevel.ToString(), PrivateImpersonationLevel.ToString())); + exception = new AuthenticationException(SR.Format(SR.net_auth_context_expectation, _expectedImpersonationLevel.ToString(), PrivateImpersonationLevel.ToString())); int statusCode = ERROR_TRUST_FAILURE; message = new byte[8]; //sizeof(long) @@ -534,7 +488,7 @@ private void StartSendBlob(byte[] message, LazyAsyncResult lazyResult) if (result < _expectedProtectionLevel) { - Exception exception = new AuthenticationException(SR.Format(SR.net_auth_context_expectation, result.ToString(), _expectedProtectionLevel.ToString())); + exception = new AuthenticationException(SR.Format(SR.net_auth_context_expectation, result.ToString(), _expectedProtectionLevel.ToString())); int statusCode = ERROR_TRUST_FAILURE; message = new byte[8]; //sizeof(long) @@ -642,7 +596,6 @@ private void ProcessReceivedBlob(byte[] message, LazyAsyncResult lazyResult) // Process Header information. if (_framer.ReadHeader.MessageId == FrameHeader.HandshakeErrId) { - Win32Exception e = null; if (message.Length >= 8) // sizeof(long) { // Try to recover remote win32 Exception. @@ -652,23 +605,10 @@ private void ProcessReceivedBlob(byte[] message, LazyAsyncResult lazyResult) error = (error << 8) + message[i]; } - e = new Win32Exception((int)error); + ThrowCredentialException(error); } - if (e != null) - { - if (e.NativeErrorCode == (int)Interop.SecurityStatus.LogonDenied) - { - throw new InvalidCredentialException(SR.net_auth_bad_client_creds, e); - } - - if (e.NativeErrorCode == ERROR_TRUST_FAILURE) - { - throw new AuthenticationException(SR.net_auth_context_expectation_remote, e); - } - } - - throw new AuthenticationException(SR.net_auth_alert, e); + throw new AuthenticationException(SR.net_auth_alert, null); } if (_framer.ReadHeader.MessageId == FrameHeader.HandshakeDoneId) @@ -715,9 +655,7 @@ private void StartSendAuthResetSignal(LazyAsyncResult lazyResult, byte[] message { _framer.WriteHeader.MessageId = FrameHeader.HandshakeErrId; - Win32Exception win32exception = exception as Win32Exception; - - if (win32exception != null && win32exception.NativeErrorCode == (int)Interop.SecurityStatus.LogonDenied) + if (IsLogonDeniedException(exception)) { if (IsServer) { @@ -838,20 +776,26 @@ private static void ReadCallback(IAsyncResult transportResult) } } - private unsafe byte[] GetOutgoingBlob(byte[] incomingBlob, ref Win32Exception e) + internal static bool IsError(SecurityStatusPal status) { - Interop.SecurityStatus statusCode; + return ((int)status.ErrorCode >= (int)SecurityStatusPalErrorCode.OutOfMemory); + } + + private unsafe byte[] GetOutgoingBlob(byte[] incomingBlob, ref Exception e) + { + SecurityStatusPal statusCode; byte[] message = _context.GetOutgoingBlob(incomingBlob, false, out statusCode); - if (((int)statusCode & unchecked((int)0x80000000)) != 0) + if (IsError(statusCode)) { - e = new System.ComponentModel.Win32Exception((int)statusCode); + e = NegotiateStreamPal.CreateExceptionFromError(statusCode); + uint error = (uint)e.HResult; - message = new byte[8]; //sizeof(long) + message = new byte[sizeof(long)]; for (int i = message.Length - 1; i >= 0; --i) { - message[i] = (byte)((uint)statusCode & 0xFF); - statusCode = (Interop.SecurityStatus)((uint)statusCode >> 8); + message[i] = (byte)(error & 0xFF); + error = (error >> 8); } } @@ -880,5 +824,29 @@ internal int DecryptData(byte[] buffer, int offset, int count, out int newOffset ++_readSequenceNumber; return _context.Decrypt(buffer, offset, count, out newOffset, _readSequenceNumber); } + + internal static void ThrowCredentialException(long error) + { + Win32Exception e = new Win32Exception((int)error); + + if (e.NativeErrorCode == (int)SecurityStatusPalErrorCode.LogonDenied) + { + throw new InvalidCredentialException(SR.net_auth_bad_client_creds, e); + } + + if (e.NativeErrorCode == NegoState.ERROR_TRUST_FAILURE) + { + throw new AuthenticationException(SR.net_auth_context_expectation_remote, e); + } + + throw new AuthenticationException(SR.net_auth_alert, e); + } + + internal static bool IsLogonDeniedException(Exception exception) + { + Win32Exception win32exception = exception as Win32Exception; + + return (win32exception != null) && (win32exception.NativeErrorCode == (int)SecurityStatusPalErrorCode.LogonDenied); + } } } diff --git a/src/System.Net.Security/src/System/Net/SecurityStatusAdapterPal.Windows.cs b/src/System.Net.Security/src/System/Net/SecurityStatusAdapterPal.Windows.cs new file mode 100644 index 000000000000..3ce34dc4a4cc --- /dev/null +++ b/src/System.Net.Security/src/System/Net/SecurityStatusAdapterPal.Windows.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Net +{ + internal static class SecurityStatusAdapterPal + { + private static readonly BidirectionalDictionary s_statusDictionary = new BidirectionalDictionary(39) + { + { Interop.SecurityStatus.AlgorithmMismatch, SecurityStatusPalErrorCode.AlgorithmMismatch }, + { Interop.SecurityStatus.BadBinding, SecurityStatusPalErrorCode.BadBinding }, + { Interop.SecurityStatus.BufferNotEnough, SecurityStatusPalErrorCode.BufferNotEnough }, + { Interop.SecurityStatus.CannotInstall, SecurityStatusPalErrorCode.CannotInstall }, + { Interop.SecurityStatus.CertExpired, SecurityStatusPalErrorCode.CertExpired }, + { Interop.SecurityStatus.CertUnknown, SecurityStatusPalErrorCode.CertUnknown }, + { Interop.SecurityStatus.CompAndContinue, SecurityStatusPalErrorCode.CompAndContinue }, + { Interop.SecurityStatus.CompleteNeeded, SecurityStatusPalErrorCode.CompleteNeeded }, + { Interop.SecurityStatus.ContextExpired, SecurityStatusPalErrorCode.ContextExpired }, + { Interop.SecurityStatus.CredentialsNeeded, SecurityStatusPalErrorCode.CredentialsNeeded }, + { Interop.SecurityStatus.ContinueNeeded, SecurityStatusPalErrorCode.ContinueNeeded }, + { Interop.SecurityStatus.IllegalMessage, SecurityStatusPalErrorCode.IllegalMessage }, + { Interop.SecurityStatus.CannotPack, SecurityStatusPalErrorCode.CannotPack }, + { Interop.SecurityStatus.IncompleteCredentials, SecurityStatusPalErrorCode.IncompleteCredentials }, + { Interop.SecurityStatus.IncompleteMessage, SecurityStatusPalErrorCode.IncompleteMessage }, + { Interop.SecurityStatus.InternalError, SecurityStatusPalErrorCode.InternalError }, + { Interop.SecurityStatus.InvalidHandle, SecurityStatusPalErrorCode.InvalidHandle }, + { Interop.SecurityStatus.InvalidToken, SecurityStatusPalErrorCode.InvalidToken }, + { Interop.SecurityStatus.LogonDenied, SecurityStatusPalErrorCode.LogonDenied }, + { Interop.SecurityStatus.NoAuthenticatingAuthority, SecurityStatusPalErrorCode.NoAuthenticatingAuthority }, + { Interop.SecurityStatus.NoImpersonation, SecurityStatusPalErrorCode.NoImpersonation }, + { Interop.SecurityStatus.NoCredentials, SecurityStatusPalErrorCode.NoCredentials }, + { Interop.SecurityStatus.NotOwner, SecurityStatusPalErrorCode.NotOwner }, + { Interop.SecurityStatus.OK, SecurityStatusPalErrorCode.OK }, + { Interop.SecurityStatus.OutOfMemory, SecurityStatusPalErrorCode.OutOfMemory }, + { Interop.SecurityStatus.OutOfSequence, SecurityStatusPalErrorCode.OutOfSequence }, + { Interop.SecurityStatus.PackageNotFound, SecurityStatusPalErrorCode.PackageNotFound }, + { Interop.SecurityStatus.MessageAltered, SecurityStatusPalErrorCode.MessageAltered }, + { Interop.SecurityStatus.QopNotSupported, SecurityStatusPalErrorCode.QopNotSupported }, + { Interop.SecurityStatus.Renegotiate, SecurityStatusPalErrorCode.Renegotiate }, + { Interop.SecurityStatus.SecurityQosFailed, SecurityStatusPalErrorCode.SecurityQosFailed }, + { Interop.SecurityStatus.SmartcardLogonRequired, SecurityStatusPalErrorCode.SmartcardLogonRequired }, + { Interop.SecurityStatus.TargetUnknown, SecurityStatusPalErrorCode.TargetUnknown }, + { Interop.SecurityStatus.TimeSkew, SecurityStatusPalErrorCode.TimeSkew }, + { Interop.SecurityStatus.UnknownCredentials, SecurityStatusPalErrorCode.UnknownCredentials }, + { Interop.SecurityStatus.UnsupportedPreauth, SecurityStatusPalErrorCode.UnsupportedPreauth }, + { Interop.SecurityStatus.Unsupported, SecurityStatusPalErrorCode.Unsupported }, + { Interop.SecurityStatus.UntrustedRoot, SecurityStatusPalErrorCode.UntrustedRoot }, + { Interop.SecurityStatus.WrongPrincipal, SecurityStatusPalErrorCode.WrongPrincipal } + }; + + internal static SecurityStatusPal GetSecurityStatusPalFromNativeInt(int win32SecurityStatus) + { + return GetSecurityStatusPalFromInterop((Interop.SecurityStatus) win32SecurityStatus); + } + + internal static SecurityStatusPal GetSecurityStatusPalFromInterop(Interop.SecurityStatus win32SecurityStatus) + { + SecurityStatusPalErrorCode statusCode; + + if (!s_statusDictionary.TryGetForward(win32SecurityStatus, out statusCode)) + { + Debug.Fail("Unknown Interop.SecurityStatus value: " + win32SecurityStatus); + throw new InternalException(); + } + return new SecurityStatusPal(statusCode); + } + + internal static Interop.SecurityStatus GetInteropFromSecurityStatusPal(SecurityStatusPal status) + { + Interop.SecurityStatus interopStatus; + if (!s_statusDictionary.TryGetBackward(status.ErrorCode, out interopStatus)) + { + Debug.Fail("Unknown SecurityStatus value: " + status); + throw new InternalException(); + } + return interopStatus; + } + } +} diff --git a/src/System.Net.Security/src/System/Net/SslStreamPal.Unix.cs b/src/System.Net.Security/src/System/Net/SslStreamPal.Unix.cs index 6f1b9fc2e48e..07540c09f2be 100644 --- a/src/System.Net.Security/src/System/Net/SslStreamPal.Unix.cs +++ b/src/System.Net.Security/src/System/Net/SslStreamPal.Unix.cs @@ -49,7 +49,7 @@ public static SecurityStatusPal InitializeSecurityContext(SafeFreeCredentials cr public static SafeFreeCredentials AcquireCredentialsHandle(X509Certificate certificate, SslProtocols protocols, EncryptionPolicy policy, bool isServer) { - return new SafeFreeCredentials(certificate, protocols, policy); + return new SafeFreeSslCredentials(certificate, protocols, policy); } public static SecurityStatusPal EncryptMessage(SafeDeleteContext securityContext, byte[] buffer, int size, int headerSize, int trailerSize, out int resultSize) @@ -72,7 +72,7 @@ public static SecurityStatusPal DecryptMessage(SafeDeleteContext securityContext public static SafeFreeContextBufferChannelBinding QueryContextChannelBinding(SafeDeleteContext securityContext, ChannelBindingKind attribute) { - SafeChannelBindingHandle bindingHandle = Interop.OpenSsl.QueryChannelBinding(securityContext.SslContext, attribute); + SafeChannelBindingHandle bindingHandle = Interop.OpenSsl.QueryChannelBinding(((SafeDeleteSslContext)securityContext).SslContext, attribute); var refHandle = bindingHandle == null ? null : new SafeFreeContextBufferChannelBinding(bindingHandle); return refHandle; } @@ -84,7 +84,7 @@ public static void QueryContextStreamSizes(SafeDeleteContext securityContext, ou public static void QueryContextConnectionInfo(SafeDeleteContext securityContext, out SslConnectionInfo connectionInfo) { - connectionInfo = new SslConnectionInfo(securityContext.SslContext); + connectionInfo = new SslConnectionInfo(((SafeDeleteSslContext)securityContext).SslContext); } private static SecurityStatusPal HandshakeInternal(SafeFreeCredentials credential, ref SafeDeleteContext context, @@ -96,7 +96,7 @@ private static SecurityStatusPal HandshakeInternal(SafeFreeCredentials credentia { if ((null == context) || context.IsInvalid) { - context = new SafeDeleteContext(credential, isServer, remoteCertRequired); + context = new SafeDeleteSslContext(credential as SafeFreeSslCredentials, isServer, remoteCertRequired); } byte[] output = null; @@ -105,11 +105,11 @@ private static SecurityStatusPal HandshakeInternal(SafeFreeCredentials credentia if (null == inputBuffer) { - done = Interop.OpenSsl.DoSslHandshake(context.SslContext, null, 0, 0, out output, out outputSize); + done = Interop.OpenSsl.DoSslHandshake(((SafeDeleteSslContext)context).SslContext, null, 0, 0, out output, out outputSize); } else { - done = Interop.OpenSsl.DoSslHandshake(context.SslContext, inputBuffer.token, inputBuffer.offset, inputBuffer.size, out output, out outputSize); + done = Interop.OpenSsl.DoSslHandshake(((SafeDeleteSslContext)context).SslContext, inputBuffer.token, inputBuffer.offset, inputBuffer.size, out output, out outputSize); } outputBuffer.size = outputSize; @@ -130,7 +130,7 @@ private static SecurityStatusPal EncryptDecryptHelper(SafeDeleteContext security try { Interop.Ssl.SslErrorCode errorCode = Interop.Ssl.SslErrorCode.SSL_ERROR_NONE; - SafeSslHandle scHandle = securityContext.SslContext; + SafeSslHandle scHandle = ((SafeDeleteSslContext)securityContext).SslContext; if (encrypt) { diff --git a/src/System.Net.Security/src/System/Net/SslStreamPal.Windows.cs b/src/System.Net.Security/src/System/Net/SslStreamPal.Windows.cs index 62628da23dfc..535eb4ce300d 100644 --- a/src/System.Net.Security/src/System/Net/SslStreamPal.Windows.cs +++ b/src/System.Net.Security/src/System/Net/SslStreamPal.Windows.cs @@ -28,7 +28,7 @@ internal static class SslStreamPal public static Exception GetException(SecurityStatusPal status) { - int win32Code = (int)GetInteropFromSecurityStatusPal(status.ErrorCode); + int win32Code = (int)SecurityStatusAdapterPal.GetInteropFromSecurityStatusPal(status); return new Win32Exception(win32Code); } @@ -53,7 +53,7 @@ public static SecurityStatusPal AcceptSecurityContext(ref SafeFreeCredentials cr outputBuffer, ref unusedAttributes); - return GetSecurityStatusPalFromWin32Int(errorCode); + return SecurityStatusAdapterPal.GetSecurityStatusPalFromNativeInt(errorCode); } public static SecurityStatusPal InitializeSecurityContext(ref SafeFreeCredentials credentialsHandle, ref SafeDeleteContext context, string targetName, SecurityBuffer inputBuffer, SecurityBuffer outputBuffer) @@ -71,7 +71,7 @@ public static SecurityStatusPal InitializeSecurityContext(ref SafeFreeCredential outputBuffer, ref unusedAttributes); - return GetSecurityStatusPalFromWin32Int(errorCode); + return SecurityStatusAdapterPal.GetSecurityStatusPalFromNativeInt(errorCode); } public static SecurityStatusPal InitializeSecurityContext(SafeFreeCredentials credentialsHandle, ref SafeDeleteContext context, string targetName, SecurityBuffer[] inputBuffers, SecurityBuffer outputBuffer) @@ -89,7 +89,7 @@ public static SecurityStatusPal InitializeSecurityContext(SafeFreeCredentials cr outputBuffer, ref unusedAttributes); - return GetSecurityStatusPalFromWin32Int(errorCode); + return SecurityStatusAdapterPal.GetSecurityStatusPalFromNativeInt(errorCode); } public static SafeFreeCredentials AcquireCredentialsHandle(X509Certificate certificate, SslProtocols protocols, EncryptionPolicy policy, bool isServer) @@ -152,7 +152,7 @@ public static SecurityStatusPal EncryptMessage(SafeDeleteContext securityContext resultSize = securityBuffer[0].size + securityBuffer[1].size + securityBuffer[2].size; } - return GetSecurityStatusPalFromWin32Int(errorCode); + return SecurityStatusAdapterPal.GetSecurityStatusPalFromNativeInt(errorCode); } public static SecurityStatusPal DecryptMessage(SafeDeleteContext securityContext, byte[] buffer, ref int offset, ref int count) @@ -184,7 +184,7 @@ public static SecurityStatusPal DecryptMessage(SafeDeleteContext securityContext } } - return new SecurityStatusPal(SecurityStatusPalErrorCodeFromInterop(errorCode)); + return SecurityStatusAdapterPal.GetSecurityStatusPalFromInterop(errorCode); } public unsafe static SafeFreeContextBufferChannelBinding QueryContextChannelBinding(SafeDeleteContext securityContext, ChannelBindingKind attribute) @@ -300,189 +300,5 @@ private static SafeFreeCredentials AcquireCredentialsHandle(Interop.SspiCli.Cred return SSPIWrapper.AcquireCredentialsHandle(GlobalSSPI.SSPISecureChannel, SecurityPackage, credUsage, secureCredential); } } - - private static SecurityStatusPal GetSecurityStatusPalFromWin32Int(int win32SecurityStatus) - { - return new SecurityStatusPal(SecurityStatusPalErrorCodeFromInterop((Interop.SecurityStatus)win32SecurityStatus)); - } - - private static SecurityStatusPalErrorCode SecurityStatusPalErrorCodeFromInterop(Interop.SecurityStatus win32SecurityStatus) - { - switch (win32SecurityStatus) - { - case Interop.SecurityStatus.OK: - return SecurityStatusPalErrorCode.OK; - case Interop.SecurityStatus.ContinueNeeded: - return SecurityStatusPalErrorCode.ContinueNeeded; - case Interop.SecurityStatus.CompleteNeeded: - return SecurityStatusPalErrorCode.CompleteNeeded; - case Interop.SecurityStatus.CompAndContinue: - return SecurityStatusPalErrorCode.CompAndContinue; - case Interop.SecurityStatus.ContextExpired: - return SecurityStatusPalErrorCode.ContextExpired; - case Interop.SecurityStatus.CredentialsNeeded: - return SecurityStatusPalErrorCode.CredentialsNeeded; - case Interop.SecurityStatus.Renegotiate: - return SecurityStatusPalErrorCode.Renegotiate; - case Interop.SecurityStatus.OutOfMemory: - return SecurityStatusPalErrorCode.OutOfMemory; - case Interop.SecurityStatus.InvalidHandle: - return SecurityStatusPalErrorCode.InvalidHandle; - case Interop.SecurityStatus.Unsupported: - return SecurityStatusPalErrorCode.Unsupported; - case Interop.SecurityStatus.TargetUnknown: - return SecurityStatusPalErrorCode.TargetUnknown; - case Interop.SecurityStatus.InternalError: - return SecurityStatusPalErrorCode.InternalError; - case Interop.SecurityStatus.PackageNotFound: - return SecurityStatusPalErrorCode.PackageNotFound; - case Interop.SecurityStatus.NotOwner: - return SecurityStatusPalErrorCode.NotOwner; - case Interop.SecurityStatus.CannotInstall: - return SecurityStatusPalErrorCode.CannotInstall; - case Interop.SecurityStatus.InvalidToken: - return SecurityStatusPalErrorCode.InvalidToken; - case Interop.SecurityStatus.CannotPack: - return SecurityStatusPalErrorCode.CannotPack; - case Interop.SecurityStatus.QopNotSupported: - return SecurityStatusPalErrorCode.QopNotSupported; - case Interop.SecurityStatus.NoImpersonation: - return SecurityStatusPalErrorCode.NoImpersonation; - case Interop.SecurityStatus.LogonDenied: - return SecurityStatusPalErrorCode.LogonDenied; - case Interop.SecurityStatus.UnknownCredentials: - return SecurityStatusPalErrorCode.UnknownCredentials; - case Interop.SecurityStatus.NoCredentials: - return SecurityStatusPalErrorCode.NoCredentials; - case Interop.SecurityStatus.MessageAltered: - return SecurityStatusPalErrorCode.MessageAltered; - case Interop.SecurityStatus.OutOfSequence: - return SecurityStatusPalErrorCode.OutOfSequence; - case Interop.SecurityStatus.NoAuthenticatingAuthority: - return SecurityStatusPalErrorCode.NoAuthenticatingAuthority; - case Interop.SecurityStatus.IncompleteMessage: - return SecurityStatusPalErrorCode.IncompleteMessage; - case Interop.SecurityStatus.IncompleteCredentials: - return SecurityStatusPalErrorCode.IncompleteCredentials; - case Interop.SecurityStatus.BufferNotEnough: - return SecurityStatusPalErrorCode.BufferNotEnough; - case Interop.SecurityStatus.WrongPrincipal: - return SecurityStatusPalErrorCode.WrongPrincipal; - case Interop.SecurityStatus.TimeSkew: - return SecurityStatusPalErrorCode.TimeSkew; - case Interop.SecurityStatus.UntrustedRoot: - return SecurityStatusPalErrorCode.UntrustedRoot; - case Interop.SecurityStatus.IllegalMessage: - return SecurityStatusPalErrorCode.IllegalMessage; - case Interop.SecurityStatus.CertUnknown: - return SecurityStatusPalErrorCode.CertUnknown; - case Interop.SecurityStatus.CertExpired: - return SecurityStatusPalErrorCode.CertExpired; - case Interop.SecurityStatus.AlgorithmMismatch: - return SecurityStatusPalErrorCode.AlgorithmMismatch; - case Interop.SecurityStatus.SecurityQosFailed: - return SecurityStatusPalErrorCode.SecurityQosFailed; - case Interop.SecurityStatus.SmartcardLogonRequired: - return SecurityStatusPalErrorCode.SmartcardLogonRequired; - case Interop.SecurityStatus.UnsupportedPreauth: - return SecurityStatusPalErrorCode.UnsupportedPreauth; - case Interop.SecurityStatus.BadBinding: - return SecurityStatusPalErrorCode.BadBinding; - default: - Debug.Fail("Unknown Interop.SecurityStatus value: " + win32SecurityStatus); - throw new InternalException(); - } - } - - private static Interop.SecurityStatus GetInteropFromSecurityStatusPal(SecurityStatusPalErrorCode status) - { - switch (status) - { - case SecurityStatusPalErrorCode.NotSet: - Debug.Fail("SecurityStatus NotSet"); - throw new InternalException(); - case SecurityStatusPalErrorCode.OK: - return Interop.SecurityStatus.OK; - case SecurityStatusPalErrorCode.ContinueNeeded: - return Interop.SecurityStatus.ContinueNeeded; - case SecurityStatusPalErrorCode.CompleteNeeded: - return Interop.SecurityStatus.CompleteNeeded; - case SecurityStatusPalErrorCode.CompAndContinue: - return Interop.SecurityStatus.CompAndContinue; - case SecurityStatusPalErrorCode.ContextExpired: - return Interop.SecurityStatus.ContextExpired; - case SecurityStatusPalErrorCode.CredentialsNeeded: - return Interop.SecurityStatus.CredentialsNeeded; - case SecurityStatusPalErrorCode.Renegotiate: - return Interop.SecurityStatus.Renegotiate; - case SecurityStatusPalErrorCode.OutOfMemory: - return Interop.SecurityStatus.OutOfMemory; - case SecurityStatusPalErrorCode.InvalidHandle: - return Interop.SecurityStatus.InvalidHandle; - case SecurityStatusPalErrorCode.Unsupported: - return Interop.SecurityStatus.Unsupported; - case SecurityStatusPalErrorCode.TargetUnknown: - return Interop.SecurityStatus.TargetUnknown; - case SecurityStatusPalErrorCode.InternalError: - return Interop.SecurityStatus.InternalError; - case SecurityStatusPalErrorCode.PackageNotFound: - return Interop.SecurityStatus.PackageNotFound; - case SecurityStatusPalErrorCode.NotOwner: - return Interop.SecurityStatus.NotOwner; - case SecurityStatusPalErrorCode.CannotInstall: - return Interop.SecurityStatus.CannotInstall; - case SecurityStatusPalErrorCode.InvalidToken: - return Interop.SecurityStatus.InvalidToken; - case SecurityStatusPalErrorCode.CannotPack: - return Interop.SecurityStatus.CannotPack; - case SecurityStatusPalErrorCode.QopNotSupported: - return Interop.SecurityStatus.QopNotSupported; - case SecurityStatusPalErrorCode.NoImpersonation: - return Interop.SecurityStatus.NoImpersonation; - case SecurityStatusPalErrorCode.LogonDenied: - return Interop.SecurityStatus.LogonDenied; - case SecurityStatusPalErrorCode.UnknownCredentials: - return Interop.SecurityStatus.UnknownCredentials; - case SecurityStatusPalErrorCode.NoCredentials: - return Interop.SecurityStatus.NoCredentials; - case SecurityStatusPalErrorCode.MessageAltered: - return Interop.SecurityStatus.MessageAltered; - case SecurityStatusPalErrorCode.OutOfSequence: - return Interop.SecurityStatus.OutOfSequence; - case SecurityStatusPalErrorCode.NoAuthenticatingAuthority: - return Interop.SecurityStatus.NoAuthenticatingAuthority; - case SecurityStatusPalErrorCode.IncompleteMessage: - return Interop.SecurityStatus.IncompleteMessage; - case SecurityStatusPalErrorCode.IncompleteCredentials: - return Interop.SecurityStatus.IncompleteCredentials; - case SecurityStatusPalErrorCode.BufferNotEnough: - return Interop.SecurityStatus.BufferNotEnough; - case SecurityStatusPalErrorCode.WrongPrincipal: - return Interop.SecurityStatus.WrongPrincipal; - case SecurityStatusPalErrorCode.TimeSkew: - return Interop.SecurityStatus.TimeSkew; - case SecurityStatusPalErrorCode.UntrustedRoot: - return Interop.SecurityStatus.UntrustedRoot; - case SecurityStatusPalErrorCode.IllegalMessage: - return Interop.SecurityStatus.IllegalMessage; - case SecurityStatusPalErrorCode.CertUnknown: - return Interop.SecurityStatus.CertUnknown; - case SecurityStatusPalErrorCode.CertExpired: - return Interop.SecurityStatus.CertExpired; - case SecurityStatusPalErrorCode.AlgorithmMismatch: - return Interop.SecurityStatus.AlgorithmMismatch; - case SecurityStatusPalErrorCode.SecurityQosFailed: - return Interop.SecurityStatus.SecurityQosFailed; - case SecurityStatusPalErrorCode.SmartcardLogonRequired: - return Interop.SecurityStatus.SmartcardLogonRequired; - case SecurityStatusPalErrorCode.UnsupportedPreauth: - return Interop.SecurityStatus.UnsupportedPreauth; - case SecurityStatusPalErrorCode.BadBinding: - return Interop.SecurityStatus.BadBinding; - default: - Debug.Fail("Unknown Interop.SecurityStatus value: " + status); - throw new InternalException(); - } - } } } diff --git a/src/System.Net.Security/tests/FunctionalTests/KerberosTest.cs b/src/System.Net.Security/tests/FunctionalTests/KerberosTest.cs new file mode 100644 index 000000000000..c205f6751539 --- /dev/null +++ b/src/System.Net.Security/tests/FunctionalTests/KerberosTest.cs @@ -0,0 +1,490 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Test.Common; +using System.Security.Authentication; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; + +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.Security.Tests +{ + public class KDCSetup : IDisposable + { + private const string Krb5ConfigFile = "/etc/krb5.conf"; + private const string KDestroyCmd = "kdestroy"; + private const string SudoCommand = "sudo"; + private const string ScriptName = "setup-kdc.sh"; + private const string ScriptUninstallArgs = "--uninstall --yes"; + private const string ScriptInstallArgs = "--password {0} --yes"; + private const int InstalledButUnconfiguredExitCode = 2; + private readonly bool _isKrbPreInstalled ; + public readonly string password; + + public KDCSetup() + { + _isKrbPreInstalled = File.Exists(Krb5ConfigFile) && + File.ReadAllText(Krb5ConfigFile).Contains(TestConfiguration.Realm); + if (!_isKrbPreInstalled) + { + password = Guid.NewGuid().ToString("N"); + int exitCode = RunSetupScript(string.Format(ScriptInstallArgs, password)); + if (exitCode != 0) + { + if (exitCode != InstalledButUnconfiguredExitCode) + { + Dispose(); + } + + Assert.True(false, "KDC setup failure"); + } + } + else + { + password = TestConfiguration.DefaultPassword; + } + } + + public void Dispose() + { + if (!_isKrbPreInstalled) + { + RunSetupScript(ScriptUninstallArgs); + } + } + + // checks for avilability of Kerberos related infrastructure + // on the host. Returns true available, false otherwise + public bool CheckAndClearCredentials(ITestOutputHelper output) + { + // Clear the credentials + var startInfo = new ProcessStartInfo(KDestroyCmd); + startInfo.UseShellExecute = true; + startInfo.CreateNoWindow = true; + startInfo.Arguments = "-A"; + using (Process clearCreds = Process.Start(startInfo)) + { + clearCreds.WaitForExit(); + output.WriteLine("kdestroy returned {0}", clearCreds.ExitCode); + return (clearCreds.ExitCode == 0); + } + } + + private static int RunSetupScript(string args = null) + { + ProcessStartInfo startInfo = new ProcessStartInfo(); + + // since ProcessStartInfo does not support Verb, we use sudo as + // the program to be run + startInfo.FileName = SudoCommand; + startInfo.Arguments = string.Format("bash {0} {1}", ScriptName, args); + using (Process kdcSetup = Process.Start(startInfo)) + { + kdcSetup.WaitForExit(); + return kdcSetup.ExitCode; + } + } + } + + [PlatformSpecific(PlatformID.Linux)] + public class KerberosTest : IDisposable, IClassFixture + { + private readonly byte[] _firstMessage = Encoding.UTF8.GetBytes("Sample First Message"); + private readonly byte[] _secondMessage = Encoding.UTF8.GetBytes("Sample Second Message"); + private readonly bool _isKrbAvailable; // tests are no-op if kerberos is not available on the host machine + private readonly KDCSetup _fixture; + private readonly ITestOutputHelper _output; + + public KerberosTest(KDCSetup fixture, ITestOutputHelper output) + { + _fixture = fixture; + _output = output; + _isKrbAvailable = _fixture.CheckAndClearCredentials(_output); + } + + [Fact, OuterLoop] + [PlatformSpecific(PlatformID.Linux)] + public void NegotiateStream_StreamToStream_KerberosAuthentication_Success() + { + if (!_isKrbAvailable) + { + _output.WriteLine("skipping NegotiateStream_StreamToStream_KerberosAuthentication_Success"); + return; + } + + VirtualNetwork network = new VirtualNetwork(); + + using (var clientStream = new VirtualNetworkStream(network, isServer: false)) + using (var serverStream = new VirtualNetworkStream(network, isServer: true)) + using (var client = new NegotiateStream(clientStream)) + using (var server = new UnixGssFakeNegotiateStream(serverStream)) + { + Assert.False(client.IsAuthenticated, "client is not authenticated"); + + Task[] auth = new Task[2]; + string user = string.Format("{0}@{1}", TestConfiguration.KerberosUser, TestConfiguration.Realm); + string target = string.Format("{0}@{1}", TestConfiguration.HostTarget, TestConfiguration.Realm); + NetworkCredential credential = new NetworkCredential(user, _fixture.password); + auth[0] = client.AuthenticateAsClientAsync(credential, target); + auth[1] = server.AuthenticateAsServerAsync(); + + bool finished = Task.WaitAll(auth, TestConfiguration.PassingTestTimeoutMilliseconds); + Assert.True(finished, "Handshake completed in the allotted time"); + + // Expected Client property values: + Assert.True(client.IsAuthenticated, "client.IsAuthenticated"); + Assert.Equal(TokenImpersonationLevel.Identification, client.ImpersonationLevel); + Assert.True(client.IsEncrypted, "client.IsEncrypted"); + Assert.True(client.IsMutuallyAuthenticated, "client.IsMutuallyAuthenticated"); + Assert.False(client.IsServer, "client.IsServer"); + Assert.True(client.IsSigned, "client.IsSigned"); + Assert.False(client.LeaveInnerStreamOpen, "client.LeaveInnerStreamOpen"); + + IIdentity serverIdentity = client.RemoteIdentity; + Assert.Equal("Kerberos", serverIdentity.AuthenticationType); + Assert.True(serverIdentity.IsAuthenticated, "serverIdentity.IsAuthenticated"); + IdentityValidator.AssertHasName(serverIdentity, target); + } + } + + [Fact, OuterLoop] + [PlatformSpecific(PlatformID.Linux)] + public void NegotiateStream_StreamToStream_AuthToHttpTarget_Success() + { + if (!_isKrbAvailable) + { + _output.WriteLine("skipping NegotiateStream_StreamToStream_AuthToHttpTarget_Success"); + return; + } + + VirtualNetwork network = new VirtualNetwork(); + + using (var clientStream = new VirtualNetworkStream(network, isServer: false)) + using (var serverStream = new VirtualNetworkStream(network, isServer: true)) + using (var client = new NegotiateStream(clientStream)) + using (var server = new UnixGssFakeNegotiateStream(serverStream)) + { + Assert.False(client.IsAuthenticated); + + Task[] auth = new Task[2]; + string user = string.Format("{0}@{1}", TestConfiguration.KerberosUser, TestConfiguration.Realm); + string target = string.Format("{0}@{1}",TestConfiguration.HttpTarget, TestConfiguration.Realm); + NetworkCredential credential = new NetworkCredential(user, _fixture.password); + auth[0] = client.AuthenticateAsClientAsync(credential, target); + auth[1] = server.AuthenticateAsServerAsync(); + + bool finished = Task.WaitAll(auth, TestConfiguration.PassingTestTimeoutMilliseconds); + Assert.True(finished, "Handshake completed in the allotted time"); + + // Expected Client property values: + Assert.True(client.IsAuthenticated, "client.IsAuthenticated"); + Assert.Equal(TokenImpersonationLevel.Identification, client.ImpersonationLevel); + Assert.True(client.IsEncrypted, "client.IsEncrypted"); + Assert.True(client.IsMutuallyAuthenticated, "client.IsMutuallyAuthenticated"); + Assert.False(client.IsServer, "client.IsServer"); + Assert.True(client.IsSigned, "client.IsSigned"); + Assert.False(client.LeaveInnerStreamOpen, "client.LeaveInnerStream"); + + IIdentity serverIdentity = client.RemoteIdentity; + Assert.Equal("Kerberos", serverIdentity.AuthenticationType); + Assert.True(serverIdentity.IsAuthenticated, "serverIdentity.IsAuthenticated"); + IdentityValidator.AssertHasName(serverIdentity, target); + } + } + + [Fact, OuterLoop] + [PlatformSpecific(PlatformID.Linux)] + public void NegotiateStream_StreamToStream_KerberosAuthWithoutRealm_Success() + { + if (!_isKrbAvailable) + { + _output.WriteLine("skipping NegotiateStream_StreamToStream_KerberosAuthWithoutRealm_Success"); + return; + } + + VirtualNetwork network = new VirtualNetwork(); + + using (var clientStream = new VirtualNetworkStream(network, isServer: false)) + using (var serverStream = new VirtualNetworkStream(network, isServer: true)) + using (var client = new NegotiateStream(clientStream)) + using (var server = new UnixGssFakeNegotiateStream(serverStream)) + { + Assert.False(client.IsAuthenticated); + + Task[] auth = new Task[2]; + NetworkCredential credential = new NetworkCredential(TestConfiguration.KerberosUser, _fixture.password); + auth[0] = client.AuthenticateAsClientAsync(credential, TestConfiguration.HostTarget); + auth[1] = server.AuthenticateAsServerAsync(); + + bool finished = Task.WaitAll(auth, TestConfiguration.PassingTestTimeoutMilliseconds); + Assert.True(finished, "Handshake completed in the allotted time"); + + // Expected Client property values: + Assert.True(client.IsAuthenticated, "client.IsAuthenticated"); + Assert.Equal(TokenImpersonationLevel.Identification, client.ImpersonationLevel); + Assert.True(client.IsEncrypted, "client.IsEncrypted"); + Assert.True(client.IsMutuallyAuthenticated, "client.IsMutuallyAuthenticated"); + Assert.False(client.IsServer, "client.IsServer"); + Assert.True(client.IsSigned, "client.IsSigned"); + Assert.False(client.LeaveInnerStreamOpen, "client.LeaveInnerStreamOpen"); + + IIdentity serverIdentity = client.RemoteIdentity; + Assert.Equal("Kerberos", serverIdentity.AuthenticationType); + Assert.True(serverIdentity.IsAuthenticated, "serverIdentity.IsAuthenticated"); + IdentityValidator.AssertHasName(serverIdentity, TestConfiguration.HostTarget); + } + } + + [Fact, OuterLoop] + [PlatformSpecific(PlatformID.Linux)] + public void NegotiateStream_StreamToStream_KerberosAuthDefaultCredentials_Success() + { + if (!_isKrbAvailable) + { + _output.WriteLine("skipping NegotiateStream_StreamToStream_KerberosAuthDefaultCredentials_Success"); + return; + } + + VirtualNetwork network = new VirtualNetwork(); + + using (var clientStream = new VirtualNetworkStream(network, isServer: false)) + using (var serverStream = new VirtualNetworkStream(network, isServer: true)) + using (var client = new NegotiateStream(clientStream)) + using (var server = new UnixGssFakeNegotiateStream(serverStream)) + { + Assert.False(client.IsAuthenticated, "client is not authenticated before AuthenticateAsClient call"); + + Task[] auth = new Task[2]; + string user = string.Format("{0}@{1}", TestConfiguration.KerberosUser, TestConfiguration.Realm); + string target = string.Format("{0}@{1}", TestConfiguration.HostTarget, TestConfiguration.Realm); + // Seed the default Kerberos cache with the TGT + UnixGssFakeNegotiateStream.GetDefaultKerberosCredentials(user, _fixture.password); + auth[0] = client.AuthenticateAsClientAsync(CredentialCache.DefaultNetworkCredentials, target); + auth[1] = server.AuthenticateAsServerAsync(); + + bool finished = Task.WaitAll(auth, TestConfiguration.PassingTestTimeoutMilliseconds); + Assert.True(finished, "Handshake completed in the allotted time"); + + // Expected Client property values: + Assert.True(client.IsAuthenticated, "client.IsAuthenticated"); + Assert.Equal(TokenImpersonationLevel.Identification, client.ImpersonationLevel); + Assert.True(client.IsEncrypted, "client.IsEncrypted"); + Assert.True(client.IsMutuallyAuthenticated, "client.IsMutuallyAuthenticated"); + Assert.False(client.IsServer, "client.IsServer"); + Assert.True(client.IsSigned, "client.IsSigned"); + Assert.False(client.LeaveInnerStreamOpen, "client.LeaveInnerStreamOpen"); + + IIdentity serverIdentity = client.RemoteIdentity; + Assert.Equal("Kerberos", serverIdentity.AuthenticationType); + Assert.True(serverIdentity.IsAuthenticated,"serverIdentity.IsAuthenticated"); + IdentityValidator.AssertHasName(serverIdentity, target); + } + } + + [Fact, OuterLoop] + [PlatformSpecific(PlatformID.Linux)] + public void NegotiateStream_EchoServer_ClientWriteRead_Successive_Sync_Success() + { + if (!_isKrbAvailable) + { + _output.WriteLine("skipping NegotiateStream_EchoServer_ClientWriteRead_Successive_Sync_Success"); + return; + } + + VirtualNetwork network = new VirtualNetwork(); + byte[] firstRecvBuffer = new byte[_firstMessage.Length]; + byte[] secondRecvBuffer = new byte[_secondMessage.Length]; + + using (var clientStream = new VirtualNetworkStream(network, isServer: false)) + using (var serverStream = new VirtualNetworkStream(network, isServer: true)) + using (var client = new NegotiateStream(clientStream)) + using (var server = new UnixGssFakeNegotiateStream(serverStream)) + { + Assert.False(client.IsAuthenticated, "client is not authenticated before AuthenticateAsClient call"); + + Task[] auth = new Task[2]; + string user = string.Format("{0}@{1}", TestConfiguration.KerberosUser, TestConfiguration.Realm); + string target = string.Format("{0}@{1}", TestConfiguration.HostTarget, TestConfiguration.Realm); + NetworkCredential credential = new NetworkCredential(user, _fixture.password); + auth[0] = client.AuthenticateAsClientAsync(credential, target); + auth[1] = server.AuthenticateAsServerAsync(); + bool finished = Task.WaitAll(auth, TestConfiguration.PassingTestTimeoutMilliseconds); + Assert.True(finished, "Handshake completed in the allotted time"); + + Task svrMsgTask = server.PollMessageAsync(2); + + client.Write(_firstMessage, 0, _firstMessage.Length); + client.Write(_secondMessage, 0, _secondMessage.Length); + client.Read(firstRecvBuffer, 0, firstRecvBuffer.Length); + client.Read(secondRecvBuffer, 0, secondRecvBuffer.Length); + Assert.True(_firstMessage.SequenceEqual(firstRecvBuffer), "first message received is as expected"); + Assert.True(_secondMessage.SequenceEqual(secondRecvBuffer), "second message received is as expected"); + finished = svrMsgTask.Wait(TestConfiguration.PassingTestTimeoutMilliseconds); + Assert.True(finished, "Message roundtrip completed in the allotted time"); + } + } + + [Fact, OuterLoop] + [PlatformSpecific(PlatformID.Linux)] + public void NegotiateStream_EchoServer_ClientWriteRead_Successive_Async_Success() + { + if (!_isKrbAvailable) + { + _output.WriteLine("skipping NegotiateStream_EchoServer_ClientWriteRead_Successive_Async_Success"); + return; + } + + VirtualNetwork network = new VirtualNetwork(); + byte[] firstRecvBuffer = new byte[_firstMessage.Length]; + byte[] secondRecvBuffer = new byte[_secondMessage.Length]; + + using (var clientStream = new VirtualNetworkStream(network, isServer: false)) + using (var serverStream = new VirtualNetworkStream(network, isServer: true)) + using (var client = new NegotiateStream(clientStream)) + using (var server = new UnixGssFakeNegotiateStream(serverStream)) + { + Assert.False(client.IsAuthenticated, "client is not authenticated before AuthenticateAsClient call"); + + Task[] auth = new Task[2]; + string user = string.Format("{0}@{1}", TestConfiguration.KerberosUser, TestConfiguration.Realm); + string target = string.Format("{0}@{1}", TestConfiguration.HostTarget, TestConfiguration.Realm); + NetworkCredential credential = new NetworkCredential(user, _fixture.password); + auth[0] = client.AuthenticateAsClientAsync(credential, target); + auth[1] = server.AuthenticateAsServerAsync(); + bool finished = Task.WaitAll(auth, TestConfiguration.PassingTestTimeoutMilliseconds); + Assert.True(finished, "Handshake completed in the allotted time"); + + Task serverTask = server.PollMessageAsync(2); + Task[] msgTasks = new Task[3]; + msgTasks[0] = client.WriteAsync(_firstMessage, 0, _firstMessage.Length).ContinueWith((t) => + client.WriteAsync(_secondMessage, 0, _secondMessage.Length)); + msgTasks[1] = client.ReadAsync(firstRecvBuffer, 0, firstRecvBuffer.Length).ContinueWith((t) => + client.ReadAsync(secondRecvBuffer, 0, secondRecvBuffer.Length)); + msgTasks[2] = serverTask; + finished = Task.WaitAll(msgTasks, TestConfiguration.PassingTestTimeoutMilliseconds); + Assert.True(finished, "Messages sent and received in the allotted time"); + Assert.True(_firstMessage.SequenceEqual(firstRecvBuffer), "The first message received is as expected"); + Assert.True(_secondMessage.SequenceEqual(secondRecvBuffer), "The second message received is as expected"); + } + } + + [Fact, OuterLoop] + [PlatformSpecific(PlatformID.Linux)] + public void NegotiateStream_StreamToStream_KerberosAuthDefaultCredentialsNoSeed_Failure() + { + if (!_isKrbAvailable) + { + _output.WriteLine("skipping NegotiateStream_StreamToStream_KerberosAuthDefaultCredentialsNoSeed_Failure"); + return; + } + + VirtualNetwork network = new VirtualNetwork(); + + using (var clientStream = new VirtualNetworkStream(network, isServer: false)) + using (var client = new NegotiateStream(clientStream)) + { + Assert.False(client.IsAuthenticated, "client is not authenticated before AuthenticateAsClient call"); + + string user = string.Format("{0}@{1}", TestConfiguration.KerberosUser, TestConfiguration.Realm); + string target = string.Format("{0}@{1}", TestConfiguration.HostTarget, TestConfiguration.Realm); + Assert.ThrowsAsync(() => client.AuthenticateAsClientAsync(CredentialCache.DefaultNetworkCredentials, target)); + } + } + + [Fact, OuterLoop] + [PlatformSpecific(PlatformID.Linux)] + public void NegotiateStream_StreamToStream_KerberosAuthInvalidUser_Failure() + { + if (!_isKrbAvailable) + { + _output.WriteLine("skipping NegotiateStream_StreamToStream_KerberosAuthInvalidUser_Failure"); + return; + } + + VirtualNetwork network = new VirtualNetwork(); + + using (var clientStream = new VirtualNetworkStream(network, isServer: false)) + using (var client = new NegotiateStream(clientStream)) + { + Assert.False(client.IsAuthenticated, "client is not authenticated by default"); + + string user = string.Format("{0}@{1}", TestConfiguration.KerberosUser, TestConfiguration.Realm); + string target = string.Format("{0}@{1}", TestConfiguration.HostTarget, TestConfiguration.Realm); + NetworkCredential credential = new NetworkCredential(user.Substring(1), _fixture.password); + Assert.Throws(() => + { + client.AuthenticateAsClientAsync(credential, target); + }); + } + } + + [Fact, OuterLoop] + [PlatformSpecific(PlatformID.Linux)] + public void NegotiateStream_StreamToStream_KerberosAuthInvalidPassword_Failure() + { + if (!_isKrbAvailable) + { + _output.WriteLine("skipping NegotiateStream_StreamToStream_KerberosAuthInvalidPassword_Failure"); + return; + } + + VirtualNetwork network = new VirtualNetwork(); + + using (var clientStream = new VirtualNetworkStream(network, isServer: false)) + using (var client = new NegotiateStream(clientStream)) + { + Assert.False(client.IsAuthenticated, "client stream is not authenticated by default"); + + string user = string.Format("{0}@{1}", TestConfiguration.KerberosUser, TestConfiguration.Realm); + string target = string.Format("{0}@{1}", TestConfiguration.HostTarget, TestConfiguration.Realm); + NetworkCredential credential = new NetworkCredential(user, _fixture.password.Substring(1)); + Assert.Throws(() => + { + client.AuthenticateAsClientAsync(credential, target); + }); + } + } + + [Fact, OuterLoop] + [PlatformSpecific(PlatformID.Linux)] + public void NegotiateStream_StreamToStream_KerberosAuthInvalidTarget_Failure() + { + if (!_isKrbAvailable) + { + _output.WriteLine("skipping NegotiateStream_StreamToStream_KerberosAuthInvalidTarget_Failure"); + return; + } + + VirtualNetwork network = new VirtualNetwork(); + + using (var clientStream = new VirtualNetworkStream(network, isServer: false)) + using (var client = new NegotiateStream(clientStream)) + { + Assert.False(client.IsAuthenticated, "client stream is not authenticated by default"); + + string user = string.Format("{0}@{1}", TestConfiguration.KerberosUser, TestConfiguration.Realm); + string target = string.Format("{0}@{1}", TestConfiguration.HostTarget, TestConfiguration.Realm); + NetworkCredential credential = new NetworkCredential(user, _fixture.password); + Assert.ThrowsAsync(() => client.AuthenticateAsClientAsync(credential, target.Substring(1))); + } + } + + public void Dispose() + { + try + { + _fixture.CheckAndClearCredentials(_output); + } + catch + { + } + } + } +} diff --git a/src/System.Net.Security/tests/FunctionalTests/NegotiateStreamStreamToStreamTest.cs b/src/System.Net.Security/tests/FunctionalTests/NegotiateStreamStreamToStreamTest.cs index ddb8da29a135..71054bb29756 100644 --- a/src/System.Net.Security/tests/FunctionalTests/NegotiateStreamStreamToStreamTest.cs +++ b/src/System.Net.Security/tests/FunctionalTests/NegotiateStreamStreamToStreamTest.cs @@ -261,12 +261,5 @@ public void NegotiateStream_StreamToStream_Successive_ClientWrite_Async_Success( Assert.True(_sampleMsg.SequenceEqual(recvBuf)); } } - - [Fact] - [PlatformSpecific(PlatformID.Linux | PlatformID.OSX)] - public void NegotiateStream_Ctor_Throws() - { - Assert.Throws(() => new NegotiateStream(new VirtualNetworkStream(null, isServer: false))); - } } } diff --git a/src/System.Net.Security/tests/FunctionalTests/Resources/Strings.resx b/src/System.Net.Security/tests/FunctionalTests/Resources/Strings.resx new file mode 100644 index 000000000000..ce944b34de88 --- /dev/null +++ b/src/System.Net.Security/tests/FunctionalTests/Resources/Strings.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Gss api operation failed with error : {0} ({1}). + + + GSSAPI operation failed with status: {0} (Minor status: {1}) + + + GSSAPI security context establishment failed with status: {0} (Minor status: {1}) + + + GSSAPI encryption or signing failed with status: {0} (Minor status: {1}) + + + GSSAPI decryption or signature verification failed with status: {0} (Minor status: {1}) + + + Insufficient buffer space. Required: {0} Actual: {1} + + diff --git a/src/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj b/src/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj index 453b2afb52ec..3e9d476068a2 100644 --- a/src/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj +++ b/src/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj @@ -10,6 +10,9 @@ {A55A2B9A-830F-4330-A0E7-02A9FB30ABD2} Library + + true + win/project.json @@ -19,7 +22,6 @@ unix/project.json unix/project.lock.json - @@ -27,11 +29,9 @@ - - @@ -47,10 +47,8 @@ - - Common\System\Net\HttpTestServers.cs @@ -82,28 +80,59 @@ Common\System\Threading\Tasks\TaskTimeoutExtensions.cs + - - + + + Always + + + Always + + + Always + + + Always + + + Always + + + + + + Common\Interop\Unix\Interop.Libraries.cs + + + Common\Interop\Unix\System.Net.Security.Native\Interop.GssApiException.cs + + + Common\Interop\Unix\System.Net.Security.Native\Interop.GssBuffer.cs + + + Common\Microsoft\Win32\SafeHandles\GssSafeHandles.cs + + + Common\Interop\Unix\System.Net.Security.Native\Interop.NetSecurityNative.cs + + + {89F37791-6254-4D60-AB96-ACD3CCA0E771} System.Net.Security - - - - diff --git a/src/System.Net.Security/tests/FunctionalTests/TestConfiguration.cs b/src/System.Net.Security/tests/FunctionalTests/TestConfiguration.cs index 4ff81f1d7b09..b8937efea9f2 100644 --- a/src/System.Net.Security/tests/FunctionalTests/TestConfiguration.cs +++ b/src/System.Net.Security/tests/FunctionalTests/TestConfiguration.cs @@ -20,6 +20,12 @@ internal static class TestConfiguration private const string CertificatePassword = "testcertificate"; private const string TestDataFolder = "TestData"; + public const string Realm = "TEST.COREFX.NET"; + public const string KerberosUser = "krb_user"; + public const string DefaultPassword = "password"; + public const string HostTarget = "TESTHOST/testfqdn.test.corefx.net"; + public const string HttpTarget = "TESTHTTP"; + public static X509Certificate2 GetServerCertificate() { X509Certificate2Collection certCollection = TestConfiguration.GetServerCertificateCollection(); diff --git a/src/System.Net.Security/tests/FunctionalTests/UnixGssFakeNegotiateStream.cs b/src/System.Net.Security/tests/FunctionalTests/UnixGssFakeNegotiateStream.cs new file mode 100644 index 000000000000..7deb97b65f1f --- /dev/null +++ b/src/System.Net.Security/tests/FunctionalTests/UnixGssFakeNegotiateStream.cs @@ -0,0 +1,166 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Security.Authentication; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; + +namespace System.Net.Security.Tests +{ + internal class UnixGssFakeNegotiateStream : NegotiateStream + { + private static Action s_serverLoop = ServerLoop; + private static Action s_msgLoop = MessageLoop; + private readonly UnixGssFakeStreamFramer _framer; + private SafeGssContextHandle _context; + private volatile int _dataMsgCount; + + public UnixGssFakeNegotiateStream(Stream innerStream) : base(innerStream) + { + _framer = new UnixGssFakeStreamFramer(innerStream); + _dataMsgCount = 0; + } + + public override Task AuthenticateAsServerAsync() + { + return Task.Factory.StartNew(s_serverLoop, this, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); + } + + public Task PollMessageAsync(int count) + { + _dataMsgCount = count; + return Task.Factory.StartNew(s_msgLoop, this, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); + } + + public static void GetDefaultKerberosCredentials(string username, string password) + { + // Fetch a Kerberos TGT which gets saved in the default cache + SafeGssCredHandle.Create(username, password, string.Empty).Dispose(); + } + + private static void ServerLoop(object state) + { + UnixGssFakeNegotiateStream thisRef = (UnixGssFakeNegotiateStream)state; + var header = new byte[5]; + bool handshakeDone = false; + do + { + byte[] inBuf = thisRef._framer.ReadHandshakeFrame(); + byte[] outBuf = null; + try + { + handshakeDone = EstablishSecurityContext(ref thisRef._context, inBuf, out outBuf); + thisRef._framer.WriteHandshakeFrame(outBuf, 0, outBuf.Length); + } + catch (Interop.NetSecurityNative.GssApiException e) + { + thisRef._framer.WriteHandshakeFrame(e); + handshakeDone = true; + } + } + while (!handshakeDone); + } + + private static void MessageLoop(object state) + { + UnixGssFakeNegotiateStream thisRef = (UnixGssFakeNegotiateStream)state; + while (thisRef._dataMsgCount > 0) + { + byte[] inBuf = thisRef._framer.ReadDataFrame(); + byte[] unwrapped = UnwrapMessage(thisRef._context, inBuf); + byte[] outMsg = WrapMessage(thisRef._context, unwrapped); + thisRef._framer.WriteDataFrame(outMsg, 0, outMsg.Length); + thisRef._dataMsgCount--; + } + } + + private static bool EstablishSecurityContext( + ref SafeGssContextHandle context, + byte[] buffer, + out byte[] outputBuffer) + { + outputBuffer = null; + + // 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. + if (context == null) + { + 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 == null) ? 0 : buffer.Length, + ref token); + + if ((status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) && (status != Interop.NetSecurityNative.Status.GSS_S_CONTINUE_NEEDED)) + { + throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); + } + + outputBuffer = token.ToByteArray(); + } + finally + { + token.Dispose(); + } + + return status == Interop.NetSecurityNative.Status.GSS_S_COMPLETE; + } + + private static byte[] UnwrapMessage(SafeGssContextHandle context, byte[] message) + { + Interop.NetSecurityNative.GssBuffer unwrapped = default(Interop.NetSecurityNative.GssBuffer); + Interop.NetSecurityNative.Status status; + + try + { + Interop.NetSecurityNative.Status minorStatus; + status = Interop.NetSecurityNative.UnwrapBuffer(out minorStatus, + context, message, 0, message.Length, ref unwrapped); + if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) + { + throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); + } + + return unwrapped.ToByteArray(); + } + finally + { + unwrapped.Dispose(); + } + } + + private static byte[] WrapMessage(SafeGssContextHandle context, byte[] message) + { + Interop.NetSecurityNative.GssBuffer wrapped = default(Interop.NetSecurityNative.GssBuffer); + Interop.NetSecurityNative.Status status; + + try + { + Interop.NetSecurityNative.Status minorStatus; + status = Interop.NetSecurityNative.WrapBuffer(out minorStatus, + context, false, message, 0, message.Length, ref wrapped); + if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE) + { + throw new Interop.NetSecurityNative.GssApiException(status, minorStatus); + } + + return wrapped.ToByteArray(); + } + finally + { + wrapped.Dispose(); + } + } + } +} diff --git a/src/System.Net.Security/tests/FunctionalTests/UnixGssFakeStreamFramer.cs b/src/System.Net.Security/tests/FunctionalTests/UnixGssFakeStreamFramer.cs new file mode 100644 index 000000000000..76ed04a2ffd6 --- /dev/null +++ b/src/System.Net.Security/tests/FunctionalTests/UnixGssFakeStreamFramer.cs @@ -0,0 +1,108 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; + +namespace System.Net.Security.Tests +{ + internal class UnixGssFakeStreamFramer + { + public const byte HandshakeDoneId = 20; + public const byte HandshakeErrId = 21; + public const byte DefaultMajorV = 1; + public const byte DefaultMinorV = 0; + + private readonly Stream _innerStream; + private readonly byte[] _header = new byte[5]; + private static readonly byte[] ErrorBuffer = new byte[] { 0, 0, 0, 0, 0x80, 0x09, 0x03, 0x0C }; // return LOGON_DENIED + + public UnixGssFakeStreamFramer(Stream innerStream) + { + _innerStream = innerStream; + } + + public void WriteDataFrame(byte[] buffer, int offset, int count) + { + // data messages have the format of |pay-load-size|pay-load...| + // where, pay-load-size = size of the payload as unsigned-int in little endian format + // reference: https://msdn.microsoft.com/en-us/library/cc236740.aspx + + byte[] prefix = BitConverter.GetBytes(Convert.ToUInt32(count)); + if (!BitConverter.IsLittleEndian) + { + Array.Reverse(prefix); + } + + _innerStream.Write(prefix, 0, prefix.Length); + _innerStream.Write(buffer, offset, count); + } + + public void WriteHandshakeFrame(byte[] buffer, int offset, int count) + { + WriteFrameHeader(count, isError:false); + if (count > 0) + { + _innerStream.Write(buffer, offset, count); + } + } + + public void WriteHandshakeFrame(Interop.NetSecurityNative.GssApiException e) + { + WriteFrameHeader(ErrorBuffer.Length, isError:true); + _innerStream.Write(ErrorBuffer, 0, ErrorBuffer.Length); + } + + public byte[] ReadHandshakeFrame() + { + // A handshake header described at https://msdn.microsoft.com/en-us/library/cc236739.aspx + // consists of 5 bytes: + // first byte is a message id (one of [HandshakeDoneId, HandshakeErrId, HandshakeInProgress]) + // second byte is Major version of protocol (0x01) + // third byte is Minor version of protocol (0) + // fourth byte is the high order byte of the payload size (expressed as an unsigned number - ushort) + // fifth byte is the low order byte of the payload size (expressed as unsigned number - ushort) + + _innerStream.Read(_header, 0, _header.Length); + byte[] inBuf = new byte[(_header[3] << 8) + _header[4]]; + _innerStream.Read(inBuf, 0, inBuf.Length); + return inBuf; + } + + public byte[] ReadDataFrame() + { + // data messages have the format of |pay-load-size|pay-load...| + // where, pay-load-size = size of the payload as unsigned-int in little endian format + // reference: https://msdn.microsoft.com/en-us/library/cc236740.aspx + + byte[] lenBytes = new byte[4]; + _innerStream.Read(lenBytes, 0, lenBytes.Length); + if (!BitConverter.IsLittleEndian) + { + Array.Reverse(lenBytes); + } + + int length = Convert.ToInt32(BitConverter.ToUInt32(lenBytes, startIndex: 0)); + byte[] data = new byte[length]; + _innerStream.Read(data, 0, length); + return data; + } + + private void WriteFrameHeader(int count, bool isError) + { + // A handshake header described at https://msdn.microsoft.com/en-us/library/cc236739.aspx + // consists of 5 bytes: + // first byte is a message id (one of [HandshakeDoneId, HandshakeErrId, HandshakeInProgress]) + // second byte is Major version of protocol (0x01) + // third byte is Minor version of protocol (0) + // fourth byte is the high order byte of the payload size (expressed as unsigned number - ushort) + // fifth byte is the low order byte of the payload size (expressed as unsigned number - ushort) + + _header[0] = isError ? HandshakeErrId : HandshakeDoneId; + _header[1] = DefaultMajorV; + _header[2] = DefaultMinorV; + _header[3] = (byte)((count >> 8) & 0xff); + _header[4] = (byte)(count & 0xff); + _innerStream.Write(_header, 0, _header.Length); + } + } +} diff --git a/src/System.Net.Security/tests/Scripts/kdc.conf b/src/System.Net.Security/tests/Scripts/kdc.conf new file mode 100644 index 000000000000..635da7ddd1b8 --- /dev/null +++ b/src/System.Net.Security/tests/Scripts/kdc.conf @@ -0,0 +1,16 @@ +[kdcdefaults] + kdc_ports = 750,88 + +[realms] + TEST.COREFX.NET = { + database_name = /var/lib/krb5kdc/principal + admin_keytab = FILE:/etc/krb5kdc/kadm5.keytab + acl_file = /etc/krb5kdc/kadm5.acl + key_stash_file = /etc/krb5kdc/stash + kdc_ports = 750,88 + max_life = 10h 0m 0s + max_renewable_life = 7d 0h 0m 0s + master_key_type = des3-hmac-sha1 + supported_enctypes = aes256-cts:normal arcfour-hmac:normal des3-hmac-sha1:normal des-cbc-crc:normal des:normal des:v4 des:norealm des:onlyrealm des:afs3 + default_principal_flags = +preauth + } diff --git a/src/System.Net.Security/tests/Scripts/kdc.conf.centos b/src/System.Net.Security/tests/Scripts/kdc.conf.centos new file mode 100644 index 000000000000..b704efa99c35 --- /dev/null +++ b/src/System.Net.Security/tests/Scripts/kdc.conf.centos @@ -0,0 +1,11 @@ +[kdcdefaults] + kdc_ports = 88 + kdc_tcp_ports = 88 + +[realms] + TEST.COREFX.NET = { + acl_file = /var/kerberos/krb5kdc/kadm5.acl + dict_file = /usr/share/dict/words + admin_keytab = /var/kerberos/krb5kdc/kadm5.keytab + supported_enctypes = aes256-cts:normal aes128-cts:normal des3-hmac-sha1:normal arcfour-hmac:normal camellia256-cts:normal camellia128-cts:normal des-hmac-sha1:normal des-cbc-md5:normal des-cbc-crc:normal + } diff --git a/src/System.Net.Security/tests/Scripts/kdc.conf.opensuse b/src/System.Net.Security/tests/Scripts/kdc.conf.opensuse new file mode 100644 index 000000000000..baf9aeb4d3d3 --- /dev/null +++ b/src/System.Net.Security/tests/Scripts/kdc.conf.opensuse @@ -0,0 +1,18 @@ +[kdcdefaults] + kdc_ports = 750,88 + +[realms] + TEST.COREFX.NET = { + database_name = /var/lib/kerberos/krb5kdc/principal + admin_keytab = FILE:/var/lib/kerberos/krb5kdc/kadm5.keytab + acl_file = /var/lib/kerberos/krb5kdc/kadm5.acl + dict_file = /var/lib/kerberos/krb5kdc/kadm5.dict + key_stash_file = /var/lib/kerberos/krb5kdc/.k5.TEST.COREFX.NET + kdc_ports = 750,88 + max_life = 10h 0m 0s + max_renewable_life = 7d 0h 0m 0s + } + +[logging] + kdc = FILE:/var/log/krb5/krb5kdc.log + admin_server = FILE:/var/log/krb5/kadmind.log diff --git a/src/System.Net.Security/tests/Scripts/kdc.conf.ubuntu b/src/System.Net.Security/tests/Scripts/kdc.conf.ubuntu new file mode 100644 index 000000000000..635da7ddd1b8 --- /dev/null +++ b/src/System.Net.Security/tests/Scripts/kdc.conf.ubuntu @@ -0,0 +1,16 @@ +[kdcdefaults] + kdc_ports = 750,88 + +[realms] + TEST.COREFX.NET = { + database_name = /var/lib/krb5kdc/principal + admin_keytab = FILE:/etc/krb5kdc/kadm5.keytab + acl_file = /etc/krb5kdc/kadm5.acl + key_stash_file = /etc/krb5kdc/stash + kdc_ports = 750,88 + max_life = 10h 0m 0s + max_renewable_life = 7d 0h 0m 0s + master_key_type = des3-hmac-sha1 + supported_enctypes = aes256-cts:normal arcfour-hmac:normal des3-hmac-sha1:normal des-cbc-crc:normal des:normal des:v4 des:norealm des:onlyrealm des:afs3 + default_principal_flags = +preauth + } diff --git a/src/System.Net.Security/tests/Scripts/krb5.conf b/src/System.Net.Security/tests/Scripts/krb5.conf new file mode 100644 index 000000000000..338810a5f13d --- /dev/null +++ b/src/System.Net.Security/tests/Scripts/krb5.conf @@ -0,0 +1,12 @@ +[libdefaults] + default_realm = TEST.COREFX.NET + +[realms] + TEST.COREFX.NET = { + kdc = localhost + admin_server = localhost + default_domain = test.corefx.net + } + +[domain_realm] + .test.corefx.net = TEST.COREFX.NET diff --git a/src/System.Net.Security/tests/Scripts/setup-kdc.sh b/src/System.Net.Security/tests/Scripts/setup-kdc.sh new file mode 100755 index 000000000000..844a40cca64f --- /dev/null +++ b/src/System.Net.Security/tests/Scripts/setup-kdc.sh @@ -0,0 +1,276 @@ +#!/usr/bin/env bash + +OS=`cat /etc/os-release | grep "^ID=" | sed 's/ID=//g' | sed 's/["]//g' | awk '{print $1}'` +echo -e "Operating System: ${OS}\n" + +realm="TEST.COREFX.NET" + +principal1="TESTHOST/testfqdn.test.corefx.net" +principal2="TESTHTTP" +krb_user="krb_user" +password="password" + +kadmin="kadmin.local" +krb5kdc="krb5kdc" +kdb5_util="kdb5_util" + +krb_conf="krb5.conf" +krb_conf_location="/etc/krb5.conf" +keytabfile="/etc/krb5.keytab" + +PROGNAME=$(basename $0) +usage() +{ + echo "This script must be run with super-user privileges." + echo "Usage: ${PROGNAME} [-h|--help] [-y|--yes] [-u|--uninstall]"; +} + +# Cleanup config files and uninstall KDC +clean_up() +{ + echo "Stopping KDC.." + if pgrep krb5kdc 2> /dev/null; then pkill krb5kdc 2> /dev/null ; fi + + echo "Removing config files" + if [ -f ${krb_conf_location} ]; then + rm -f ${krb_conf_location} + fi + + case ${OS} in + "ubuntu" | "debian") + kdc_conf_location="/etc/krb5kdc/kdc.conf" + dpkg -s krb5-kdc >/dev/null 2>&1 + if [ $? -eq 0 ]; then + echo "Uninstalling krb5-kdc" + apt-get -y purge krb5-kdc + fi + ;; + + "centos" | "rhel") + kdc_conf_location="/var/kerberos/krb5kdc/kdc.conf" + yum list installed krb5-server >/dev/null 2>&1 + if [ $? -eq 0 ]; then + echo "Uninstalling krb5-server" + yum -y remove krb5-server + fi + ;; + + "opensuse") + kdc_conf_location="/var/lib/kerberos/krb5kdc/kdc.conf" + zypper search --installed-only krb5-server >/dev/null 2>&1 + if [ $? -eq 0 ]; then + echo "Uninstalling krb5-server" + zypper --non-interactive remove krb5-server >/dev/null 2>&1 + fi + ;; + *) + echo "This is an unsupported operating system" + ;; + esac + + if [ -f ${kdc_conf_location} ]; then + rm -f ${kdc_conf_location} + fi + + echo "Cleanup completed" +} + +error_exit() +{ + echo "${1:-"Unknown Error"}" + echo "Aborting" + clean_up + exit 1 +} + +# Common function across linux distros to configure KDC post installation +configure_kdc() +{ + echo "Stopping KDC.." + if pgrep krb5kdc 2> /dev/null; then pkill krb5kdc ; fi + + # Remove database files if exist + rm -f ${database_files} + + add_principal_cmd="add_principal -pw ${password}" + # Create/copy krb5.conf and kdc.conf + echo "Copying krb5.conf and kdc.conf.." + cp ${krb_conf} ${krb_conf_location} || \ + error_exit "Cannot copy ${krb_conf} to ${krb_conf_location}" + + cp ${kdc_conf} ${kdc_conf_location} || \ + error_exit "Cannot copy ${kdc_conf} to ${kdc_conf_location}" + + echo "Creating KDC database for realm ${realm}.." + ${kdb5_util} create -r ${realm} -P ${password} -s || \ + error_exit "Cannot create KDC database for realm ${realm}" + + echo "Adding principal ${principal1}.." + ${kadmin} -q "${add_principal_cmd} ${principal1}@${realm}" || \ + error_exit "Cannot add ${principal1}" + + echo "Adding principal ${principal2}.." + ${kadmin} -q "${add_principal_cmd} ${principal2}@${realm}" || \ + error_exit "Cannot add ${principal2}" + + echo "Adding user ${krb_user}.." + ${kadmin} -q "${add_principal_cmd} ${krb_user}@${realm}" || \ + error_exit "Cannot add ${krb_user}" + + echo "Exporting keytab for ${principal1}" + ${kadmin} -q "ktadd -norandkey ${principal1}@${realm}" || \ + error_exit "Cannot export kytab for ${principal1}" + + echo "Exporting keytab for ${principal2}" + ${kadmin} -q "ktadd -norandkey ${principal2}@${realm}" || \ + error_exit "Cannot export kytab for ${principal2}" + + echo "Exporting keytab for ${krb_user}" + ${kadmin} -q "ktadd -norandkey ${krb_user}@${realm}" || \ + error_exit "Cannot export kytab for ${krb_user}" +} + +# check the invoker of this script +if [ $EUID -ne 0 ]; then + usage + exit 1 +fi + +# Parse command-line arguments +TEMP=`getopt -o p:hyu --long password:,help,yes,uninstall -n 'test.sh' -- "$@"` +[ $? -eq 0 ] || { + usage + exit 1 +} +eval set -- "$TEMP" +uninstall=0 +force=0 +while true; do + case $1 in + -h|--help) usage; exit 0;; + -y|--yes) force=1; shift ;; + -u|--uninstall) uninstall=1; shift;; + -p|--password) shift; password=$1; shift;; + --) shift; break;; + *) usage; exit 1;; + esac +done + +# Uninstallation +if [ $uninstall -eq 1 ]; then + if [ $force -eq 0 ]; then + echo "This will uninstall KDC from your machine and cleanup the related config files." + read -p "Do you want to continue? ([Y]es/[N]o)? " choice + case $(echo $choice | tr '[A-Z]' '[a-z]') in + y|yes) clean_up;; + *) echo "Skipping uninstallation";; + esac + else + clean_up + fi + exit 0 +fi + +# Installation +if [ $force -eq 0 ]; then + read -p "This will install KDC on your machine and create KDC principals. Do you want to continue? ([Y]es/[N]o)? " choice + case $(echo $choice | tr '[A-Z]' '[a-z]') in + y|yes) ;; + *) echo "Skipping installation"; exit 0;; + esac +fi + +case ${OS} in + "ubuntu" | "debian") + kdc_conf="kdc.conf.ubuntu" + kdc_conf_location="/etc/krb5kdc/kdc.conf" + database_files="/var/lib/krb5kdc/principal*" + + dpkg -s krb5-kdc >/dev/null 2>&1 + if [ $? -ne 0 ]; then + echo "Installing krb5-kdc.." + export DEBIAN_FRONTEND=noninteractive + apt-get -y install krb5-kdc krb5-admin-server + if [ $? -ne 0 ]; then + echo "Error occurred during installation, aborting" + exit 1 + fi + else + echo "krb5-kdc already installed.." + exit 2 + fi + + configure_kdc + + echo "Starting KDC.." + ${krb5kdc} + ;; + + "centos" | "rhel") + kdc_conf="kdc.conf.centos" + kdc_conf_location="/var/kerberos/krb5kdc/kdc.conf" + database_files="/var/kerberos/krb5kdc/principal*" + + yum list installed krb5-server >/dev/null 2>&1 + if [ $? -ne 0 ]; then + echo "Installing krb5-server.." + yum -y install krb5-server krb5-libs krb5-workstation + if [ $? -ne 0 ]; then + echo "Error occurred during installation, aborting" + exit 1 + fi + else + echo "krb5-server already installed.." + exit 2 + fi + + configure_kdc + + echo "Starting KDC.." + systemctl start krb5kdc.service + systemctl enable krb5kdc.service + ;; + + "opensuse") + # the following is a workaround for opensuse + # details at https://groups.google.com/forum/#!topic/comp.protocols.kerberos/3itzZQ4fETA + # and http://lists.opensuse.org/opensuse-factory/2013-10/msg00099.html + export KRB5CCNAME=$PWD + + krb5kdc="/usr/lib/mit/sbin/krb5kdc" + kadmin="/usr/lib/mit/sbin/kadmin.local" + kdb5_util="/usr/lib/mit/sbin/kdb5_util" + kdc_conf="kdc.conf.opensuse" + kdc_conf_location="/var/lib/kerberos/krb5kdc/kdc.conf" + database_files="/var/lib/kerberos/krb5kdc/principal*" + + zypper search --installed-only krb5-mini >/dev/null 2>&1 + if [ $? -eq 0 ]; then + echo "removing krb5-mini which conflicts with krb5-server and krb5-devel" + zypper --non-interactive remove krb5-mini + fi + + zypper search --installed-only krb5-server >/dev/null 2>&1 + if [ $? -ne 0 ]; then + echo "Installing krb5-server.." + zypper --non-interactive install krb5-server krb5-client krb5-devel + if [ $? -ne 0 ]; then + echo "Error occured during installation, aborting" + exit 1 + fi + else + echo "krb5-server already installed.." + exit 2 + fi + + configure_kdc + + echo "Starting KDC..${krb5kdc}" + ${krb5kdc} + ;; + *) + echo "This is an unsupported operating system" + ;; +esac + +chmod +r ${keytabfile}